Files
HiActs-Conference-Poll/templates/admin.html
T
beantownbabeandClaude Sonnet 4.6 27a59671fc Initial commit: HiActs Conference Poll app
Flask poll app for the Hi-Acts 3rd Network Meeting at DESY Hamburg,
10 June 2026. Includes 11-question poll, live D3 bubble chart display,
7-page PDF analysis report, admin dashboard, and CSV export.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 15:38:17 +02:00

296 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<!--
admin.html — Admin / Raw Results Page
Internal use only. Not linked from any attendee-facing page.
Shows all raw responses per question with bar charts.
ACCESS: yourdomain.com/admin
WARNING: This page has no password protection.
The web dev team should restrict access to this URL in production
so that conference attendees cannot accidentally stumble onto it.
RESET BUTTON: clears ALL poll data permanently. Use with caution.
-->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin - HiActs Poll</title>
<style>
/* ── RESET ── */
* { box-sizing: border-box; margin: 0; padding: 0; }
/* ── BODY: side-by-side layout, full height ── */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f0f4f8; color: #2d3748;
display: flex; flex-direction: row;
height: 100vh; overflow: hidden;
}
/* ── SIDEBAR: event branding and navigation ── */
.sidebar {
width: 300px; flex-shrink: 0;
background: white; border-right: 1px solid #dde5ee;
display: flex; flex-direction: column; height: 100vh;
}
.event-header {
flex-shrink: 0;
background: linear-gradient(160deg, #0f2744 0%, #1a4a8a 60%, #1d6fa8 100%);
padding: 28px 22px 24px; color: white; text-align: center;
}
.event-badge { font-size: 10px; font-weight: 700; letter-spacing: 1.4px; text-transform: uppercase; opacity: 0.6; margin-bottom: 14px; }
.event-title { font-size: 28px; font-weight: 900; line-height: 1.12; margin-bottom: 6px; letter-spacing: -0.3px; }
.event-title span { color: #5db8f5; }
.event-subtitle { font-size: 11.5px; font-weight: 500; opacity: 0.7; margin-bottom: 16px; line-height: 1.4; }
.event-meta { display: flex; align-items: center; justify-content: center; gap: 6px; font-size: 10.5px; opacity: 0.55; letter-spacing: 0.3px; }
.event-meta-dot { width: 3px; height: 3px; background: white; border-radius: 50%; opacity: 0.5; }
.message-area { flex-shrink: 0; padding: 20px 22px 0; }
.message-narrative { font-size: 13px; color: #334155; line-height: 1.7; }
.message-narrative p { margin-bottom: 12px; }
.message-narrative p:last-child { margin-bottom: 0; }
.message-narrative strong { color: #1a365d; font-weight: 600; }
.sidebar-mid { flex: 1; display: flex; align-items: center; padding: 0 22px 20px; }
.sidebar-nav { width: 100%; display: flex; flex-direction: column; gap: 6px; }
.snav-btn {
display: block; text-decoration: none; font-size: 12px; font-weight: 600;
color: #4a5568; background: #f7fafc; border: 1px solid #dde5ee;
border-radius: 8px; padding: 7px 12px; text-align: center;
transition: background 0.12s, color 0.12s;
}
.snav-btn:hover { background: #eff6ff; color: #2563eb; border-color: #bfdbfe; }
.snav-btn.active { background: #eff6ff; color: #2563eb; border-color: #2563eb; font-weight: 700; }
/* ── MAIN COLUMN: scrollable content area ── */
.main-col { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* Top bar with total response counter */
.top-bar {
flex-shrink: 0; display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center; padding: 10px 24px;
background: white; border-bottom: 1px solid #dde5ee; gap: 12px;
}
.top-bar-center { text-align: center; }
.top-bar-center h1 { font-size: 20px; font-weight: 700; color: #1a365d; }
.top-bar-center .meta { font-size: 15px; color: #8a9baf; margin-top: 4px; }
.top-bar-right { display: flex; align-items: center; justify-content: flex-end; gap: 14px; }
/* Green pulsing live badge */
.live-badge { display: flex; align-items: center; gap: 6px; background: #f0fdf4; border: 1px solid #86efac; border-radius: 20px; padding: 4px 12px; }
.live-dot { width: 7px; height: 7px; background: #16a34a; border-radius: 50%; animation: blink 1.4s infinite; }
.live-text { font-size: 11px; font-weight: 700; color: #16a34a; letter-spacing: 0.8px; text-transform: uppercase; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
/* Large response counter */
.counter-block { text-align: right; }
.counter-number {
font-size: 32px; font-weight: 900; line-height: 1;
background: linear-gradient(135deg, #2563eb, #059669);
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
}
.counter-label { font-size: 10px; color: #8a9baf; text-transform: uppercase; letter-spacing: 1px; }
/* Scrollable content area below the top bar */
.content { flex: 1; overflow-y: auto; padding: 14px 20px 30px; }
/* 2-column grid of question result blocks */
.question-blocks-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
/* Section label */
.section-title {
font-size: 10px; font-weight: 700; color: #8a9baf;
text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px;
}
/* ── QUESTION BLOCK: one card per question showing answer breakdown ── */
.question-block {
background: white; border-radius: 12px;
padding: 14px 16px; margin-bottom: 10px; border: 1px solid #e8eef4;
}
.q-header { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 10px; }
.q-title { font-size: 13px; font-weight: 700; color: #1a365d; }
.q-count { font-size: 12px; color: #8a9baf; }
/* Three-column layout: answer labels | progress bars | leading answer circle */
.q-body { display: grid; grid-template-columns: 1fr 110px 116px; gap: 0 10px; align-items: center; }
.q-answers-col { display: flex; flex-direction: column; }
.q-answer-label { font-size: 12px; color: #2d3748; padding: 5px 0; border-bottom: 1px solid #f0f4f8; line-height: 1.3; min-height: 28px; display: flex; align-items: center; }
.q-answer-label:last-child { border-bottom: none; }
.q-bars-col { display: flex; flex-direction: column; }
.q-bar-row { display: flex; align-items: center; gap: 6px; padding: 5px 0; border-bottom: 1px solid #f0f4f8; min-height: 28px; }
.q-bar-row:last-child { border-bottom: none; }
.q-bar-track { flex: 1; height: 7px; background: #e8eef4; border-radius: 4px; overflow: hidden; }
.q-bar-fill { height: 100%; border-radius: 4px; }
.q-bar-num { font-size: 11px; font-weight: 700; color: #1a365d; width: 16px; text-align: right; flex-shrink: 0; }
/* Circular badge showing the top answer for this question */
.q-circle-col { display: flex; align-items: center; justify-content: center; }
.q-top-circle { width: 108px; height: 108px; border-radius: 50%; display: flex; align-items: center; justify-content: center; text-align: center; padding: 14px; }
.q-top-circle-text { font-size: 11px; font-weight: 700; line-height: 1.4; color: white; }
.no-answers { color: #a0aec0; font-size: 13px; }
</style>
</head>
<body>
<!-- ── SIDEBAR ── -->
<div class="sidebar">
<div class="event-header">
<div class="event-badge">Hi-Acts · 3rd Network Meeting</div>
<div class="event-title">Innovation<br><span>Challenge</span><br>Arena</div>
<div class="event-subtitle">Competing Ideas. Shared Solutions.</div>
<div class="event-meta">DESY Hamburg <span class="event-meta-dot"></span> 10 June 2026</div>
</div>
<div class="message-area">
<div class="message-narrative">
<p>This room brings together industry partners, researchers and innovation experts united by one goal. Turning a shared challenge into a shared solution.</p>
<p>The challenge is how to standardise <strong>software and data for high-throughput beamline experiments</strong>, enabling smooth exchange between facilities, reproducible science, and FAIR data by default.</p>
</div>
</div>
<div class="sidebar-mid">
<div class="sidebar-nav">
<a href="/display" class="snav-btn">Display / Demo</a>
<a href="/" class="snav-btn">Attendee Poll</a>
<a href="/display" class="snav-btn">Live Results</a>
<a href="/admin" class="snav-btn active">Admin</a>
</div>
</div>
</div>
<!-- ── MAIN CONTENT ── -->
<div class="main-col">
<!-- Top bar with live response counter -->
<div class="top-bar">
<div></div>
<div class="top-bar-center">
<h1>Every perspective in this room matters.</h1>
<div class="meta">Help us gain a bit more insight.</div>
</div>
<div class="top-bar-right">
<div class="live-badge">
<span class="live-dot"></span>
<span class="live-text">Live</span>
</div>
<div class="counter-block">
<!-- Updated by JavaScript every 3 seconds -->
<div class="counter-number" id="totalCount">0</div>
<div class="counter-label">responses</div>
</div>
</div>
</div>
<div class="content">
<!-- Action buttons row: CSV export + reset -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:14px; gap:10px;">
<div style="display:flex; gap:10px;">
<!-- Download all responses as a CSV for Tableau or Excel -->
<a href="/admin/export" style="background:#2563eb;color:white;border:none;border-radius:9px;padding:9px 20px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;text-decoration:none;display:inline-block;">Download CSV</a>
<a href="/admin/report" style="background:#059669;color:white;border:none;border-radius:9px;padding:9px 20px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;text-decoration:none;display:inline-block;">Download PDF Report</a>
</div>
<button onclick="resetData()" style="background:#e53e3e;color:white;border:none;border-radius:9px;padding:9px 20px;font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;">Reset All Data</button>
</div>
<!-- Raw response blocks — one card per question, built by Jinja2 template engine -->
<div class="section-title">Raw Responses</div>
<div class="question-blocks-grid">
<!-- Loop through each question and its answer counts (provided by app.py) -->
{% for item in processed %}
{% set max_count = item.counts.values() | list | max if item.counts else 1 %}
<div class="question-block">
<div class="q-header">
<div class="q-title">Q{{ item.question.id }}: {{ item.question.text }}</div>
<div class="q-count">{{ item.total }} answer{{ 's' if item.total != 1 else '' }}</div>
</div>
{% if item.counts %}
{% set top_items = item.counts.items() | list %}
{% set q_colors = ['#2563eb','#059669','#dc2626','#d97706','#7c3aed','#ea580c','#0891b2','#be185d','#65a30d','#7c3aed'] %}
{% set q_color = q_colors[item.question.id - 1] %}
<div class="q-body">
<!-- Answer labels column -->
<div class="q-answers-col">
{% for text, count in top_items[:5] %}
<div class="q-answer-label">{{ text }}</div>
{% endfor %}
</div>
<!-- Progress bars column -->
<div class="q-bars-col">
{% for text, count in top_items[:5] %}
<div class="q-bar-row">
<div class="q-bar-track">
<div class="q-bar-fill" style="width:{{ (count / max_count * 100) | int }}%;background:{{ q_color }}"></div>
</div>
<span class="q-bar-num">{{ count }}</span>
</div>
{% endfor %}
</div>
<!-- Leading answer circle -->
<div class="q-circle-col">
<div class="q-top-circle" style="background:{{ q_color }}">
<span class="q-top-circle-text">{{ top_items[0][0] }}</span>
</div>
</div>
</div>
{% else %}
<div class="no-answers">No answers yet</div>
{% endif %}
</div>
{% endfor %}
</div><!-- end question-blocks-grid -->
<!-- Email list — collected from attendees who requested the full analysis -->
<div style="margin-top:28px;">
<div class="section-title">Emails collected (full report requests)</div>
<div style="background:white;border-radius:12px;border:1px solid #e8eef4;padding:16px 20px;">
{% if emails %}
<div style="font-size:13px;color:#4a5568;margin-bottom:10px;">{{ emails | length }} address{{ 'es' if emails | length != 1 else '' }} collected</div>
{% for email in emails %}
<div style="font-size:13px;color:#1a365d;padding:6px 0;border-bottom:1px solid #f0f4f8;font-family:'SF Mono','Fira Code',monospace;">{{ email }}</div>
{% endfor %}
{% else %}
<div style="font-size:13px;color:#a0aec0;">No emails collected yet. They appear here when attendees request the full report on the analysis page.</div>
{% endif %}
</div>
</div>
<!-- Attendee comments — free-text submitted at the end of the poll -->
<div style="margin-top:20px;">
<div class="section-title">Attendee comments ({{ comments | length }})</div>
<div style="background:white;border-radius:12px;border:1px solid #e8eef4;padding:16px 20px;">
{% if comments %}
{% for comment in comments %}
<div style="font-size:13px;color:#2d3748;padding:8px 0;border-bottom:1px solid #f0f4f8;line-height:1.5;">{{ comment }}</div>
{% endfor %}
{% else %}
<div style="font-size:13px;color:#a0aec0;">No comments submitted yet.</div>
{% endif %}
</div>
</div>
</div><!-- end content -->
</div><!-- end main-col -->
<script>
/* Updates the total response counter every 3 seconds without reloading the page */
function refresh() {
fetch('/api/results').then(r => r.json()).then(data => {
document.getElementById('totalCount').textContent = data.total_responses;
});
}
refresh();
setInterval(refresh, 3000);
/* Clears all poll data after a confirmation prompt — cannot be undone */
function resetData() {
if (!confirm('Reset ALL poll data? This cannot be undone.')) return;
fetch('/admin/reset', { method: 'POST' }).then(() => location.reload());
}
</script>
</body>
</html>