This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/dashboard/templates/mobile_test.html

423 lines
14 KiB
HTML

{% extends "base.html" %}
{% block title %}Mobile Test — Timmy Time{% endblock %}
{% block content %}
<div class="container-fluid mc-content" style="height:auto; overflow:visible;">
<!-- ── Page header ─────────────────────────────────────────────────── -->
<div class="mt-hitl-header">
<div>
<span class="mt-title">// MOBILE TEST SUITE</span>
<span class="mt-sub">HUMAN-IN-THE-LOOP</span>
</div>
<div class="mt-score-wrap">
<span class="mt-score" id="score-display">0 / {{ total }}</span>
<span class="mt-score-label">PASSED</span>
</div>
</div>
<!-- ── Progress bar ────────────────────────────────────────────────── -->
<div class="mt-progress-wrap">
<div class="progress" style="height:6px; background:var(--bg-card); border-radius:3px;">
<div class="progress-bar mt-progress-bar"
id="progress-bar"
role="progressbar"
style="width:0%; background:var(--green);"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="{{ total }}"></div>
</div>
<div class="mt-progress-legend">
<span><span class="mt-dot green"></span>PASS</span>
<span><span class="mt-dot red"></span>FAIL</span>
<span><span class="mt-dot amber"></span>SKIP</span>
<span><span class="mt-dot" style="background:var(--text-dim);box-shadow:none;"></span>PENDING</span>
</div>
</div>
<!-- ── Reset / Back ────────────────────────────────────────────────── -->
<div class="mt-actions">
<a href="/" class="mc-btn-clear">← MISSION CONTROL</a>
<button class="mc-btn-clear" onclick="resetAll()" style="border-color:var(--red);color:var(--red);">RESET ALL</button>
</div>
<!-- ── Scenario cards ──────────────────────────────────────────────── -->
{% for category, items in categories.items() %}
<div class="mt-category-label">{{ category | upper }}</div>
{% for s in items %}
<div class="card mc-panel mt-card" id="card-{{ s.id }}" data-scenario="{{ s.id }}">
<div class="card-header mc-panel-header d-flex justify-content-between align-items-center">
<div>
<span class="mt-id-badge" id="badge-{{ s.id }}">{{ s.id }}</span>
<span class="mt-scenario-title">{{ s.title }}</span>
</div>
<span class="mt-state-chip" id="chip-{{ s.id }}">PENDING</span>
</div>
<div class="card-body p-3">
<div class="mt-steps-label">STEPS</div>
<ol class="mt-steps">
{% for step in s.steps %}
<li>{{ step }}</li>
{% endfor %}
</ol>
<div class="mt-expected-label">EXPECTED</div>
<div class="mt-expected">{{ s.expected }}</div>
<div class="mt-btn-row">
<button class="mt-btn mt-btn-pass" onclick="mark('{{ s.id }}', 'pass')">✓ PASS</button>
<button class="mt-btn mt-btn-fail" onclick="mark('{{ s.id }}', 'fail')">✗ FAIL</button>
<button class="mt-btn mt-btn-skip" onclick="mark('{{ s.id }}', 'skip')">— SKIP</button>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
<!-- ── Summary footer ──────────────────────────────────────────────── -->
<div class="card mc-panel mt-summary" id="summary">
<div class="card-header mc-panel-header">// SUMMARY</div>
<div class="card-body p-3" id="summary-body">
<p class="mt-summary-hint">Mark all scenarios above to see your final score.</p>
</div>
</div>
</div><!-- /container -->
<!-- ── Styles (scoped to this page) ────────────────────────────────────── -->
<style>
.mt-hitl-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: 16px 0 12px;
border-bottom: 1px solid var(--border);
margin-bottom: 12px;
}
.mt-title {
font-size: 14px;
font-weight: 700;
color: var(--text-bright);
letter-spacing: 0.18em;
display: block;
}
.mt-sub {
font-size: 10px;
color: var(--text-dim);
letter-spacing: 0.2em;
display: block;
margin-top: 2px;
}
.mt-score-wrap { text-align: right; }
.mt-score {
font-size: 22px;
font-weight: 700;
color: var(--green);
letter-spacing: 0.06em;
display: block;
}
.mt-score-label { font-size: 9px; color: var(--text-dim); letter-spacing: 0.2em; }
.mt-progress-wrap { margin-bottom: 10px; }
.mt-progress-legend {
display: flex;
gap: 16px;
font-size: 9px;
color: var(--text-dim);
letter-spacing: 0.12em;
margin-top: 6px;
}
.mt-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
margin-right: 4px;
vertical-align: middle;
}
.mt-dot.green { background: var(--green); box-shadow: 0 0 5px var(--green); }
.mt-dot.red { background: var(--red); box-shadow: 0 0 5px var(--red); }
.mt-dot.amber { background: var(--amber); box-shadow: 0 0 5px var(--amber); }
.mt-actions {
display: flex;
gap: 10px;
margin-bottom: 16px;
}
.mt-category-label {
font-size: 9px;
font-weight: 700;
color: var(--text-dim);
letter-spacing: 0.25em;
margin: 20px 0 8px;
padding-left: 2px;
}
.mt-card {
margin-bottom: 10px;
transition: border-color 0.2s;
}
.mt-card.state-pass { border-color: var(--green) !important; }
.mt-card.state-fail { border-color: var(--red) !important; }
.mt-card.state-skip { border-color: var(--amber) !important; opacity: 0.7; }
.mt-id-badge {
font-size: 9px;
font-weight: 700;
background: var(--border);
color: var(--text-dim);
border-radius: 2px;
padding: 2px 6px;
letter-spacing: 0.12em;
margin-right: 8px;
}
.mt-card.state-pass .mt-id-badge { background: var(--green-dim); color: var(--green); }
.mt-card.state-fail .mt-id-badge { background: var(--red-dim); color: var(--red); }
.mt-card.state-skip .mt-id-badge { background: var(--amber-dim); color: var(--amber); }
.mt-scenario-title {
font-size: 12px;
font-weight: 700;
color: var(--text-bright);
letter-spacing: 0.05em;
}
.mt-state-chip {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.15em;
color: var(--text-dim);
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: 2px;
white-space: nowrap;
}
.mt-card.state-pass .mt-state-chip { color: var(--green); border-color: var(--green); }
.mt-card.state-fail .mt-state-chip { color: var(--red); border-color: var(--red); }
.mt-card.state-skip .mt-state-chip { color: var(--amber); border-color: var(--amber); }
.mt-steps-label, .mt-expected-label {
font-size: 9px;
font-weight: 700;
color: var(--text-dim);
letter-spacing: 0.2em;
margin-bottom: 6px;
}
.mt-expected-label { margin-top: 12px; }
.mt-steps {
padding-left: 18px;
margin: 0;
font-size: 12px;
line-height: 1.8;
color: var(--text);
}
.mt-expected {
font-size: 12px;
line-height: 1.65;
color: var(--text-bright);
background: var(--bg-card);
border-left: 3px solid var(--border-glow);
padding: 8px 12px;
border-radius: 0 3px 3px 0;
}
.mt-btn-row {
display: flex;
gap: 8px;
margin-top: 14px;
}
.mt-btn {
flex: 1;
min-height: 44px;
border: 1px solid var(--border);
border-radius: 3px;
background: var(--bg-deep);
color: var(--text-dim);
font-family: var(--font);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.12em;
cursor: pointer;
touch-action: manipulation;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.mt-btn-pass:hover, .mt-btn-pass.active { background: var(--green-dim); color: var(--green); border-color: var(--green); }
.mt-btn-fail:hover, .mt-btn-fail.active { background: var(--red-dim); color: var(--red); border-color: var(--red); }
.mt-btn-skip:hover, .mt-btn-skip.active { background: var(--amber-dim); color: var(--amber); border-color: var(--amber); }
.mt-summary { margin-top: 24px; margin-bottom: 32px; }
.mt-summary-hint { color: var(--text-dim); font-size: 12px; margin: 0; }
.mt-summary-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid var(--border);
font-size: 12px;
}
.mt-summary-row:last-child { border-bottom: none; }
.mt-summary-score { font-size: 28px; font-weight: 700; color: var(--green); margin: 12px 0 4px; }
.mt-summary-pct { font-size: 13px; color: var(--text-dim); }
@media (max-width: 768px) {
.mt-btn-row { gap: 6px; }
.mt-btn { font-size: 10px; padding: 0 4px; }
}
</style>
<!-- ── HITL State Machine (sessionStorage) ─────────────────────────────── -->
<script>
const TOTAL = {{ total }};
const KEY = "timmy-mobile-test-results";
function loadResults() {
try { return JSON.parse(sessionStorage.getItem(KEY) || "{}"); }
catch { return {}; }
}
function saveResults(r) {
sessionStorage.setItem(KEY, JSON.stringify(r));
}
function mark(id, state) {
const results = loadResults();
results[id] = state;
saveResults(results);
applyState(id, state);
updateScore(results);
updateSummary(results);
}
function applyState(id, state) {
const card = document.getElementById("card-" + id);
const chip = document.getElementById("chip-" + id);
const labels = { pass: "PASS", fail: "FAIL", skip: "SKIP" };
card.classList.remove("state-pass", "state-fail", "state-skip");
if (state) card.classList.add("state-" + state);
chip.textContent = state ? labels[state] : "PENDING";
// highlight active button
card.querySelectorAll(".mt-btn").forEach(btn => btn.classList.remove("active"));
const activeBtn = card.querySelector(".mt-btn-" + state);
if (activeBtn) activeBtn.classList.add("active");
}
function updateScore(results) {
const passed = Object.values(results).filter(v => v === "pass").length;
const decided = Object.values(results).filter(v => v !== undefined).length;
document.getElementById("score-display").textContent = passed + " / " + TOTAL;
const pct = TOTAL ? (decided / TOTAL) * 100 : 0;
const bar = document.getElementById("progress-bar");
bar.style.width = pct + "%";
// colour the bar by overall health
const failCount = Object.values(results).filter(v => v === "fail").length;
bar.style.background = failCount > 0
? "var(--red)"
: passed === TOTAL ? "var(--green)" : "var(--amber)";
}
function updateSummary(results) {
const passed = Object.values(results).filter(v => v === "pass").length;
const failed = Object.values(results).filter(v => v === "fail").length;
const skipped = Object.values(results).filter(v => v === "skip").length;
const decided = passed + failed + skipped;
const summaryBody = document.getElementById("summary-body");
if (decided < TOTAL) {
summaryBody.innerHTML = '';
const p = document.createElement('p');
p.className = 'mt-summary-hint';
p.textContent = (TOTAL - decided) + ' scenario(s) still pending.';
summaryBody.appendChild(p);
return;
}
const pct = TOTAL ? Math.round((passed / TOTAL) * 100) : 0;
const color = failed > 0 ? "var(--red)" : "var(--green)";
// Safely build summary UI using DOM API to avoid XSS from potentially untrusted variables
summaryBody.innerHTML = '';
const scoreDiv = document.createElement('div');
scoreDiv.className = 'mt-summary-score';
scoreDiv.style.color = color;
scoreDiv.textContent = passed + ' / ' + TOTAL;
summaryBody.appendChild(scoreDiv);
const pctDiv = document.createElement('div');
pctDiv.className = 'mt-summary-pct';
pctDiv.textContent = pct + '% pass rate';
summaryBody.appendChild(pctDiv);
const statsContainer = document.createElement('div');
statsContainer.style.marginTop = '16px';
const createRow = (label, value, colorVar) => {
const row = document.createElement('div');
row.className = 'mt-summary-row';
const labelSpan = document.createElement('span');
labelSpan.textContent = label;
const valSpan = document.createElement('span');
valSpan.style.color = 'var(--' + colorVar + ')';
valSpan.style.fontWeight = '700';
valSpan.textContent = value;
row.appendChild(labelSpan);
row.appendChild(valSpan);
return row;
};
statsContainer.appendChild(createRow('PASSED', passed, 'green'));
statsContainer.appendChild(createRow('FAILED', failed, 'red'));
statsContainer.appendChild(createRow('SKIPPED', skipped, 'amber'));
summaryBody.appendChild(statsContainer);
const statusMsg = document.createElement('p');
statusMsg.style.marginTop = '12px';
statusMsg.style.fontSize = '11px';
if (failed > 0) {
statusMsg.style.color = 'var(--red)';
statusMsg.textContent = '⚠ ' + failed + ' failure(s) need attention before release.';
} else {
statusMsg.style.color = 'var(--green)';
statusMsg.textContent = 'All tested scenarios passed — ship it.';
}
summaryBody.appendChild(statusMsg);
}
function resetAll() {
if (!confirm("Reset all test results?")) return;
sessionStorage.removeItem(KEY);
const results = {};
document.querySelectorAll("[data-scenario]").forEach(card => {
const id = card.dataset.scenario;
applyState(id, null);
});
updateScore(results);
const summaryBody = document.getElementById("summary-body");
summaryBody.innerHTML = '';
const p = document.createElement('p');
p.className = 'mt-summary-hint';
p.textContent = 'Mark all scenarios above to see your final score.';
summaryBody.appendChild(p);
}
// Restore saved state on load
(function init() {
const results = loadResults();
Object.entries(results).forEach(([id, state]) => applyState(id, state));
updateScore(results);
updateSummary(results);
})();
</script>
{% endblock %}