Auto — Service Bay Board
A kanban-style service bay board for an auto repair shop, organizing live work orders into Waiting, In Progress, Done, and On Hold columns. Each job card shows the vehicle, customer, license plate, requested service, diagnostic code, assigned technician, ETA, and a labor-plus-parts total with a live progress bar. A KPI header tracks active jobs, waiting count, average progress, and open revenue, while drag-and-drop, tech assignment, search, and filters drive the flow.
MCP
Code
:root {
--garage: #141518;
--garage-2: #1f2127;
--steel: #5b6470;
--steel-l: #8a929d;
--orange: #ff6a13;
--orange-d: #e2540a;
--orange-50: #fff0e6;
--ink: #16181c;
--ink-2: #3b4049;
--muted: #737a85;
--bg: #f3f4f6;
--surface: #ffffff;
--line: rgba(20, 21, 24, 0.1);
--line-2: rgba(20, 21, 24, 0.18);
--ok: #2f9e6f;
--warn: #e0962a;
--danger: #d4493e;
--waiting: #e0962a;
--inprogress: #2b7fff;
--done: #2f9e6f;
--hold: #d4493e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 18px;
--sh-sm: 0 1px 2px rgba(20, 21, 24, 0.06), 0 1px 3px rgba(20, 21, 24, 0.08);
--sh-md: 0 4px 14px rgba(20, 21, 24, 0.1);
--sh-lg: 0 12px 34px rgba(20, 21, 24, 0.16);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.tnum { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
.shell {
max-width: 1240px;
margin: 0 auto;
padding: 18px clamp(12px, 3vw, 24px) 56px;
}
/* ---------- Top bar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
background: linear-gradient(135deg, var(--garage), var(--garage-2));
color: #fff;
border-radius: var(--r-lg);
padding: 14px 18px;
box-shadow: var(--sh-md);
}
.brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
.brand-mark {
width: 42px; height: 42px;
display: grid; place-items: center;
border-radius: var(--r-md);
background: var(--orange);
color: #fff;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18), 0 4px 12px rgba(255, 106, 19, 0.4);
flex: none;
}
.brand-txt { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.brand-txt strong { font-size: 16px; font-weight: 700; letter-spacing: -0.01em; }
.brand-txt span { font-size: 12px; color: var(--steel-l); }
.topbar-meta { display: flex; align-items: center; gap: 10px; flex: none; }
.clock {
font-variant-numeric: tabular-nums;
font-weight: 700;
font-size: 17px;
letter-spacing: 0.02em;
background: rgba(255, 255, 255, 0.08);
padding: 5px 11px;
border-radius: var(--r-sm);
}
.status-dot {
width: 9px; height: 9px; border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.6);
animation: pulse 2.4s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.55); }
70% { box-shadow: 0 0 0 7px rgba(47, 158, 111, 0); }
100% { box-shadow: 0 0 0 0 rgba(47, 158, 111, 0); }
}
.topbar-sub { font-size: 12.5px; color: var(--steel-l); }
/* ---------- KPIs ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-top: 14px;
}
.kpi {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 13px 15px;
box-shadow: var(--sh-sm);
display: flex;
flex-direction: column;
gap: 3px;
}
.kpi-label { font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.kpi-val { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; line-height: 1.1; }
.kpi-foot { font-size: 11.5px; color: var(--steel); }
.kpi-foot.kpi-warn { color: var(--warn); font-weight: 600; }
/* ---------- Toolbar ---------- */
.toolbar {
display: flex;
align-items: center;
gap: 12px;
margin: 16px 0 14px;
flex-wrap: wrap;
}
.seg {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 4px;
gap: 3px;
box-shadow: var(--sh-sm);
}
.seg-btn {
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
background: transparent;
border: 0;
padding: 7px 13px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background .15s, color .15s;
}
.seg-btn:hover { background: var(--bg); }
.seg-btn.is-on { background: var(--garage); color: #fff; }
.seg-btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.search {
flex: 1;
min-width: 200px;
display: flex;
align-items: center;
gap: 8px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 0 13px;
box-shadow: var(--sh-sm);
color: var(--muted);
}
.search input {
font: inherit;
font-size: 14px;
color: var(--ink);
border: 0;
background: transparent;
padding: 11px 0;
width: 100%;
outline: none;
}
.search:focus-within { border-color: var(--orange); box-shadow: 0 0 0 3px var(--orange-50); }
/* ---------- Board ---------- */
.board {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
align-items: start;
}
.col {
background: rgba(255, 255, 255, 0.55);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 10px;
min-height: 140px;
transition: background .15s, border-color .15s;
}
.col.drag-over {
border-color: var(--orange);
background: var(--orange-50);
box-shadow: inset 0 0 0 2px var(--orange);
}
.col-head {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 6px 11px;
}
.col-tag {
width: 10px; height: 10px; border-radius: 3px; flex: none;
}
.col[data-status="waiting"] .col-tag { background: var(--waiting); }
.col[data-status="in-progress"] .col-tag { background: var(--inprogress); }
.col[data-status="done"] .col-tag { background: var(--done); }
.col[data-status="hold"] .col-tag { background: var(--hold); }
.col-title { font-size: 13px; font-weight: 700; letter-spacing: -0.01em; text-transform: uppercase; }
.col-count {
margin-left: auto;
font-size: 12px;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 999px;
min-width: 22px;
height: 22px;
display: grid;
place-items: center;
padding: 0 6px;
}
.col-list { display: flex; flex-direction: column; gap: 10px; min-height: 24px; }
.col-empty {
font-size: 12px;
color: var(--muted);
text-align: center;
padding: 14px 6px;
border: 1.5px dashed var(--line-2);
border-radius: var(--r-md);
}
/* ---------- Card ---------- */
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--sh-sm);
overflow: hidden;
cursor: grab;
transition: box-shadow .15s, transform .12s, border-color .15s;
}
.card:hover { box-shadow: var(--sh-md); border-color: var(--line-2); }
.card:active { cursor: grabbing; }
.card.dragging { opacity: .55; transform: scale(.98); box-shadow: var(--sh-lg); }
.card-photo {
height: 60px;
position: relative;
display: flex;
align-items: flex-end;
justify-content: space-between;
padding: 8px 10px;
color: #fff;
}
.card-photo .bay {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
background: rgba(20, 21, 24, 0.55);
backdrop-filter: blur(2px);
padding: 3px 8px;
border-radius: 999px;
}
.card-photo .plate {
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: 0.08em;
background: #f6e9b0;
color: #1d1c12;
border: 1px solid rgba(0,0,0,.25);
padding: 2px 7px;
border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(255,255,255,.4);
}
.card-body { padding: 10px 12px 12px; }
.card-vehicle { font-size: 14.5px; font-weight: 700; letter-spacing: -0.01em; }
.card-cust { font-size: 12.5px; color: var(--muted); margin-top: 1px; }
.card-svc {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 9px;
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
}
.code-chip {
font-size: 11px;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--danger);
background: rgba(212, 73, 62, 0.1);
border: 1px solid rgba(212, 73, 62, 0.25);
padding: 1px 6px;
border-radius: 5px;
}
.meta-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 10px;
font-size: 12px;
}
.tech-pick {
font: inherit;
font-size: 12px;
font-weight: 600;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 5px 8px;
cursor: pointer;
max-width: 130px;
}
.tech-pick:focus-visible { outline: 2px solid var(--orange); outline-offset: 1px; }
.eta { color: var(--steel); font-weight: 600; font-variant-numeric: tabular-nums; }
.eta b { color: var(--ink); }
.bar {
height: 7px;
border-radius: 999px;
background: var(--bg);
overflow: hidden;
margin-top: 11px;
border: 1px solid var(--line);
}
.bar > span {
display: block;
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--orange-d), var(--orange));
transition: width .5s ease;
}
.card[data-status="done"] .bar > span { background: linear-gradient(90deg, #1f7a55, var(--done)); }
.card[data-status="hold"] .bar > span { background: repeating-linear-gradient(45deg, var(--hold), var(--hold) 6px, #b73a31 6px, #b73a31 12px); }
.card-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 10px;
}
.price { font-size: 14px; font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; }
.price small { font-size: 10.5px; font-weight: 600; color: var(--muted); display: block; letter-spacing: 0.03em; }
.move {
display: inline-flex;
gap: 5px;
}
.move button {
font: inherit;
font-size: 12px;
font-weight: 700;
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink);
width: 30px;
height: 30px;
border-radius: var(--r-sm);
cursor: pointer;
display: grid;
place-items: center;
transition: background .14s, border-color .14s, color .14s;
}
.move button:hover { background: var(--garage); color: #fff; border-color: var(--garage); }
.move button:disabled { opacity: .35; cursor: not-allowed; }
.move button:focus-visible { outline: 2px solid var(--orange); outline-offset: 1px; }
.card.hidden { display: none; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 22px;
transform: translate(-50%, 20px);
background: var(--garage);
color: #fff;
font-size: 13px;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--sh-lg);
opacity: 0;
pointer-events: none;
transition: opacity .25s, transform .25s;
z-index: 50;
max-width: 90vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast b { color: var(--orange); }
/* ---------- Responsive ---------- */
@media (max-width: 1000px) {
.board { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 700px) {
.kpis { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.shell { padding: 12px 12px 48px; }
.board { grid-template-columns: 1fr; }
.topbar-sub { display: none; }
.seg { width: 100%; overflow-x: auto; }
.search { min-width: 100%; }
.kpi-val { font-size: 22px; }
}(function () {
"use strict";
var TECHS = ["Unassigned", "D. Marsh", "R. Okoye", "L. Vance", "T. Bauer"];
var STATUSES = [
{ id: "waiting", label: "Waiting" },
{ id: "in-progress", label: "In Progress" },
{ id: "done", label: "Done" },
{ id: "hold", label: "On Hold" }
];
var PHOTOS = [
"linear-gradient(135deg,#3b4654,#202732)",
"linear-gradient(135deg,#5a3a2a,#2a1d16)",
"linear-gradient(135deg,#2a4a4a,#15282a)",
"linear-gradient(135deg,#46324f,#241829)",
"linear-gradient(135deg,#1f3a5f,#11202f)",
"linear-gradient(135deg,#4a4030,#241f16)"
];
var JOBS = [
{ id: "WO-4821", vehicle: "2019 Ford F-150", customer: "M. Delgado", plate: "GTR-2291", svc: "Brake pads + rotors", code: "", tech: "D. Marsh", eta: "11:40a", progress: 65, status: "in-progress", bay: "Bay 1", labor: 240, parts: 318, photo: 0 },
{ id: "WO-4822", vehicle: "2021 Honda CR-V", customer: "S. Pham", plate: "BVX-7740", svc: "Oil + tire rotation", code: "", tech: "R. Okoye", eta: "10:15a", progress: 90, status: "in-progress", bay: "Bay 2", labor: 70, parts: 54, photo: 1 },
{ id: "WO-4823", vehicle: "2017 Subaru Outback", customer: "J. Whitaker", plate: "KQM-1083", svc: "Misfire diagnostic", code: "P0301", tech: "L. Vance", eta: "12:30p", progress: 35, status: "in-progress", bay: "Bay 4", labor: 160, parts: 92, photo: 2 },
{ id: "WO-4824", vehicle: "2020 Toyota Camry", customer: "A. Rosen", plate: "WHL-5526", svc: "AC recharge + leak check", code: "", tech: "Unassigned", eta: "1:00p", progress: 0, status: "waiting", bay: "—", labor: 130, parts: 88, photo: 3 },
{ id: "WO-4825", vehicle: "2015 Jeep Wrangler", customer: "C. Nakamura", plate: "TRX-9914", svc: "Alignment + suspension", code: "C1234", tech: "Unassigned", eta: "2:15p", progress: 0, status: "waiting", bay: "—", labor: 290, parts: 165, photo: 4 },
{ id: "WO-4826", vehicle: "2018 BMW 330i", customer: "P. Okafor", plate: "DSL-4402", svc: "Coolant flush", code: "", tech: "Unassigned", eta: "3:00p", progress: 0, status: "waiting", bay: "—", labor: 95, parts: 60, photo: 5 },
{ id: "WO-4818", vehicle: "2016 Chevy Malibu", customer: "K. Bright", plate: "FNX-2210", svc: "Battery + alternator", code: "P0562", tech: "T. Bauer", eta: "—", progress: 100, status: "done", bay: "Bay 6", labor: 180, parts: 410, photo: 1 },
{ id: "WO-4819", vehicle: "2022 Tesla Model 3", customer: "D. Forsythe", plate: "EVX-0077", svc: "Tire replacement (x4)", code: "", tech: "R. Okoye", eta: "—", progress: 100, status: "done", bay: "Bay 3", labor: 110, parts: 880, photo: 2 },
{ id: "WO-4820", vehicle: "2014 Audi A4", customer: "L. Castellano", plate: "QTR-6618", svc: "Timing belt — parts on order", code: "P0016", tech: "D. Marsh", eta: "Wed", progress: 40, status: "hold", bay: "Bay 8", labor: 520, parts: 340, photo: 3 }
];
var board = document.getElementById("board");
var toastEl = document.getElementById("toast");
var searchEl = document.getElementById("search");
var dragId = null;
var techFilter = "all";
var queryStr = "";
function money(n) { return "$" + n.toLocaleString("en-US"); }
var toastTimer;
function toast(msg) {
toastEl.innerHTML = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2200);
}
function nextBay() {
for (var i = 1; i <= 8; i++) {
var b = "Bay " + i;
if (!JOBS.some(function (j) { return j.bay === b; })) return b;
}
return "Bay 1";
}
function cardHTML(j) {
var techOptions = TECHS.map(function (t) {
return '<option value="' + t + '"' + (t === j.tech ? " selected" : "") + ">" + t + "</option>";
}).join("");
var codeChip = j.code ? '<span class="code-chip" title="Diagnostic code">' + j.code + "</span>" : "";
var idx = STATUSES.map(function (s) { return s.id; }).indexOf(j.status);
return (
'<article class="card" draggable="true" data-id="' + j.id + '" data-status="' + j.status + '" data-tech="' + j.tech + '">' +
'<div class="card-photo" style="background:' + PHOTOS[j.photo] + '">' +
'<span class="bay">' + (j.bay === "—" ? j.id : j.bay) + "</span>" +
'<span class="plate">' + j.plate + "</span>" +
"</div>" +
'<div class="card-body">' +
'<div class="card-vehicle">' + j.vehicle + "</div>" +
'<div class="card-cust">' + j.customer + " · " + j.id + "</div>" +
'<div class="card-svc">' + codeChip + "<span>" + j.svc + "</span></div>" +
'<div class="meta-row">' +
'<select class="tech-pick" aria-label="Assign technician">' + techOptions + "</select>" +
'<span class="eta">ETA <b>' + j.eta + "</b></span>" +
"</div>" +
'<div class="bar" role="progressbar" aria-valuenow="' + j.progress + '" aria-valuemin="0" aria-valuemax="100"><span style="width:' + j.progress + '%"></span></div>' +
'<div class="card-foot">' +
'<span class="price">' + money(j.labor + j.parts) + "<small>LABOR + PARTS</small></span>" +
'<span class="move">' +
'<button class="mv-prev" title="Move left" aria-label="Move to previous status"' + (idx <= 0 ? " disabled" : "") + ">←</button>" +
'<button class="mv-next" title="Move right" aria-label="Move to next status"' + (idx >= STATUSES.length - 1 ? " disabled" : "") + ">→</button>" +
"</span>" +
"</div>" +
"</div>" +
"</article>"
);
}
function render() {
board.innerHTML = STATUSES.map(function (s) {
return (
'<section class="col" data-status="' + s.id + '" aria-label="' + s.label + '">' +
'<div class="col-head"><span class="col-tag"></span><span class="col-title">' + s.label +
'</span><span class="col-count" data-count="' + s.id + '">0</span></div>' +
'<div class="col-list" data-list="' + s.id + '"></div>' +
"</section>"
);
}).join("");
STATUSES.forEach(function (s) {
var list = board.querySelector('[data-list="' + s.id + '"]');
var jobs = JOBS.filter(function (j) { return j.status === s.id; });
list.innerHTML = jobs.map(cardHTML).join("") ||
'<div class="col-empty">No jobs</div>';
});
wire();
applyFilters();
updateKPIs();
}
function wire() {
board.querySelectorAll(".card").forEach(function (card) {
card.addEventListener("dragstart", function () {
dragId = card.dataset.id;
card.classList.add("dragging");
});
card.addEventListener("dragend", function () {
dragId = null;
card.classList.remove("dragging");
});
card.querySelector(".tech-pick").addEventListener("change", function (e) {
var job = find(card.dataset.id);
job.tech = e.target.value;
card.dataset.tech = job.tech;
if (job.tech !== "Unassigned" && job.status === "waiting") {
job.status = "in-progress";
if (job.bay === "—") job.bay = nextBay();
toast("<b>" + job.id + "</b> assigned to " + job.tech + " → In Progress");
render();
return;
}
toast("<b>" + job.id + "</b> → " + job.tech);
applyFilters();
updateKPIs();
});
card.querySelector(".mv-prev").addEventListener("click", function () { move(card.dataset.id, -1); });
card.querySelector(".mv-next").addEventListener("click", function () { move(card.dataset.id, 1); });
});
board.querySelectorAll(".col").forEach(function (col) {
col.addEventListener("dragover", function (e) {
e.preventDefault();
col.classList.add("drag-over");
});
col.addEventListener("dragleave", function () { col.classList.remove("drag-over"); });
col.addEventListener("drop", function (e) {
e.preventDefault();
col.classList.remove("drag-over");
if (dragId) setStatus(dragId, col.dataset.status);
});
});
}
function find(id) { return JOBS.filter(function (j) { return j.id === id; })[0]; }
function move(id, dir) {
var job = find(id);
var idx = STATUSES.map(function (s) { return s.id; }).indexOf(job.status);
var next = idx + dir;
if (next < 0 || next >= STATUSES.length) return;
setStatus(id, STATUSES[next].id);
}
function setStatus(id, status) {
var job = find(id);
if (job.status === status) return;
job.status = status;
if (status === "in-progress") {
if (job.progress === 0) job.progress = 10;
if (job.bay === "—") job.bay = nextBay();
} else if (status === "done") {
job.progress = 100;
job.eta = "—";
} else if (status === "waiting") {
job.progress = 0;
job.bay = "—";
}
var label = STATUSES.filter(function (s) { return s.id === status; })[0].label;
toast("<b>" + job.id + "</b> moved to " + label);
render();
}
function applyFilters() {
board.querySelectorAll(".card").forEach(function (card) {
var job = find(card.dataset.id);
var techOk = techFilter === "all" || job.tech === techFilter;
var hay = (job.vehicle + " " + job.plate + " " + job.customer + " " + job.id + " " + job.svc + " " + job.code).toLowerCase();
var qOk = !queryStr || hay.indexOf(queryStr) !== -1;
card.classList.toggle("hidden", !(techOk && qOk));
});
STATUSES.forEach(function (s) {
var list = board.querySelector('[data-list="' + s.id + '"]');
var visible = list.querySelectorAll(".card:not(.hidden)").length;
var total = JOBS.filter(function (j) { return j.status === s.id; }).length;
board.querySelector('[data-count="' + s.id + '"]').textContent =
(techFilter === "all" && !queryStr) ? total : visible;
});
}
function updateKPIs() {
var active = JOBS.filter(function (j) { return j.status === "in-progress"; });
var waiting = JOBS.filter(function (j) { return j.status === "waiting"; });
var open = JOBS.filter(function (j) { return j.status !== "done"; });
var avg = active.length
? Math.round(active.reduce(function (a, j) { return a + j.progress; }, 0) / active.length)
: 0;
var rev = open.reduce(function (a, j) { return a + j.labor + j.parts; }, 0);
document.getElementById("kpiActive").textContent = active.length;
document.getElementById("kpiWaiting").textContent = waiting.length;
document.getElementById("kpiProgress").textContent = avg + "%";
document.getElementById("kpiRevenue").textContent = money(rev);
}
/* Toolbar */
document.querySelectorAll(".seg-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".seg-btn").forEach(function (b) {
b.classList.remove("is-on");
b.setAttribute("aria-pressed", "false");
});
btn.classList.add("is-on");
btn.setAttribute("aria-pressed", "true");
techFilter = btn.dataset.tech;
applyFilters();
});
});
searchEl.addEventListener("input", function (e) {
queryStr = e.target.value.trim().toLowerCase();
applyFilters();
});
/* Clock */
function tick() {
var d = new Date();
var h = d.getHours();
var m = d.getMinutes();
var ap = h >= 12 ? "PM" : "AM";
var hh = h % 12 || 12;
document.getElementById("clock").textContent =
hh + ":" + (m < 10 ? "0" + m : m) + " " + ap;
}
tick();
setInterval(tick, 10000);
/* Live progress drift on in-progress jobs */
setInterval(function () {
var changed = false;
JOBS.forEach(function (j) {
if (j.status === "in-progress" && j.progress < 98) {
j.progress = Math.min(98, j.progress + Math.floor(Math.random() * 3));
changed = true;
}
});
if (changed) {
board.querySelectorAll('.card[data-status="in-progress"]').forEach(function (card) {
var job = find(card.dataset.id);
var bar = card.querySelector(".bar > span");
var role = card.querySelector(".bar");
if (bar) bar.style.width = job.progress + "%";
if (role) role.setAttribute("aria-valuenow", job.progress);
});
updateKPIs();
}
}, 3200);
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Auto — Service Bay Board</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 13l2-5a2 2 0 0 1 1.9-1.4h10.2A2 2 0 0 1 19 8l2 5"/>
<path d="M3 13h18v4a1 1 0 0 1-1 1h-1a2 2 0 0 1-4 0H9a2 2 0 0 1-4 0H4a1 1 0 0 1-1-1z"/>
<circle cx="7" cy="17.5" r="1.4" fill="currentColor"/>
<circle cx="17" cy="17.5" r="1.4" fill="currentColor"/>
</svg>
</div>
<div class="brand-txt">
<strong>Ironwood Auto Works</strong>
<span>Service Bay Board · Live</span>
</div>
</div>
<div class="topbar-meta">
<span class="clock" id="clock" aria-live="off">--:--</span>
<span class="status-dot" aria-hidden="true"></span>
<span class="topbar-sub">8 bays online</span>
</div>
</header>
<section class="kpis" aria-label="Shop KPIs">
<div class="kpi">
<span class="kpi-label">Active Jobs</span>
<strong class="kpi-val" id="kpiActive">0</strong>
<span class="kpi-foot">across 4 bays</span>
</div>
<div class="kpi">
<span class="kpi-label">Waiting</span>
<strong class="kpi-val" id="kpiWaiting">0</strong>
<span class="kpi-foot kpi-warn">needs assignment</span>
</div>
<div class="kpi">
<span class="kpi-label">Avg Progress</span>
<strong class="kpi-val tnum" id="kpiProgress">0%</strong>
<span class="kpi-foot">in-progress only</span>
</div>
<div class="kpi">
<span class="kpi-label">Open Revenue</span>
<strong class="kpi-val tnum" id="kpiRevenue">$0</strong>
<span class="kpi-foot">labor + parts</span>
</div>
</section>
<section class="toolbar" aria-label="Filters">
<div class="seg" role="group" aria-label="Filter by technician">
<button class="seg-btn is-on" data-tech="all" aria-pressed="true">All techs</button>
<button class="seg-btn" data-tech="D. Marsh" aria-pressed="false">D. Marsh</button>
<button class="seg-btn" data-tech="R. Okoye" aria-pressed="false">R. Okoye</button>
<button class="seg-btn" data-tech="L. Vance" aria-pressed="false">L. Vance</button>
</div>
<div class="search">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input type="search" id="search" placeholder="Search vehicle, plate, VIN, customer…" aria-label="Search jobs" />
</div>
</section>
<main class="board" id="board" aria-label="Service status columns">
<!-- columns injected by script -->
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Service Bay Board
A status-forward dashboard for a busy repair shop. Work orders flow across four columns — Waiting, In Progress, Done, and On Hold — with each card surfacing the vehicle, customer, license plate, requested service, any diagnostic trouble codes (like P0301), the assigned bay, ETA, and a tabular labor-plus-parts total. A KPI strip across the top keeps the service writer oriented with active job count, jobs waiting on assignment, average in-progress completion, and total open revenue.
Every card is interactive. Drag a job between columns or nudge it with the arrow buttons to advance its status; assigning a technician to a waiting job automatically pulls it into a free bay and flips it to In Progress. The shop-wide search matches against vehicle, plate, VIN fragment, customer, or service description, and the technician segmented control filters the board down to a single tech’s queue. Progress bars on active jobs drift upward in real time to mimic a live shop, and a toast confirms each move.
The layout is mobile-first and collapses from four columns to two to a single stacked column as the viewport narrows, so it stays usable on a tech’s phone at the bay or a writer’s tablet at the counter.
Illustrative UI only — fictional shop/dealership, not a real service system.