Four campuses run different Sunday schedules and take their resi feed from one of two broadcast origins. When a service goes wrong — a machine crashes, a feed is unusable — the booth has to know which service at which campus can be altered, and which campuses are riding on the one that broke. That lived in people's heads and in messages. The times come from the church website rather than from configuration, because the site is what stays current when services change through the year. Each campus page carries a "Times" block; the module reads it and re-reads it on a timer. Two things on those pages are not services and are excluded on purpose: the main-office hours in every page's footer, and the "Office Hours" block Apollo Beach carries inside its own Times section. The parse is bounded to the Times heading and stops at either. A mark on a service — issue, replay, do not use, with a note, and for a resi campus which of the origin's services it is taking — is the booth's shared answer, so it lives in the module's server state and reaches every tile over SSE. Per-tile settings would let two campuses disagree about whether the 9:30 is going out. Marks are written to the data directory so a restart mid-morning does not lose them, and they expire after 36 hours so last week's never greet a new Sunday. Campuses added by hand in admin are a stopgap for a site that has not caught up yet: as soon as the website publishes that campus, the site's times take over and the admin health line says the manual line can be removed. Verified: the parser against all five live campus pages, and 31 unit tests plus 16 client checks, each proven to fail under mutation — both parse guards, the merge rule, dependent recording, and three client behaviours. The suite is run here under JavaScriptCore, since this machine has no Node. Not verified here: the module guide's resilience checklist needs a running ProdDash — upstream loss and recovery, two tiles side by side, and tile removal leaving no timers behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
298 lines
7.3 KiB
CSS
298 lines
7.3 KiB
CSS
/* Service Times — scoped under its tiles via [data-module="service-times"],
|
|
styled with the shell's CSS variables only so themes reach into it. */
|
|
|
|
[data-module="service-times"] .st-board {
|
|
position: absolute;
|
|
inset: 0;
|
|
overflow: auto;
|
|
padding: 8px;
|
|
color: var(--text);
|
|
font-size: 1em;
|
|
}
|
|
|
|
[data-module="service-times"] .st-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
|
|
gap: 8px;
|
|
align-items: start;
|
|
}
|
|
|
|
[data-module="service-times"] .st-grid-list {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
/* ── a campus ─────────────────────────────────────────────────────── */
|
|
|
|
[data-module="service-times"] .st-campus {
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: color-mix(in srgb, var(--panel) 92%, var(--text) 8%);
|
|
padding: 7px 8px 8px;
|
|
min-width: 0;
|
|
}
|
|
|
|
[data-module="service-times"] .st-is-broadcast {
|
|
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
|
|
}
|
|
|
|
[data-module="service-times"] .st-campus-head {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 5px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
[data-module="service-times"] .st-campus-name {
|
|
margin: 0;
|
|
font-size: 1.02em;
|
|
font-weight: 700;
|
|
letter-spacing: 0.01em;
|
|
color: var(--text);
|
|
}
|
|
|
|
[data-module="service-times"] .st-role {
|
|
font-size: 0.72em;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
[data-module="service-times"] .st-role-broadcast {
|
|
color: var(--accent-text, var(--text));
|
|
background: var(--accent);
|
|
border-radius: 999px;
|
|
padding: 1px 7px;
|
|
}
|
|
|
|
[data-module="service-times"] .st-role-manual {
|
|
color: var(--muted);
|
|
border: 1px dashed var(--border);
|
|
border-radius: 999px;
|
|
padding: 0 6px;
|
|
}
|
|
|
|
[data-module="service-times"] .st-role-unset {
|
|
color: var(--warn);
|
|
}
|
|
|
|
[data-module="service-times"] .st-feeds {
|
|
margin: 0 0 5px;
|
|
font-size: 0.76em;
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* ── a service ────────────────────────────────────────────────────── */
|
|
|
|
[data-module="service-times"] .st-rows {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
}
|
|
|
|
[data-module="service-times"] .st-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
gap: 6px;
|
|
width: 100%;
|
|
text-align: left;
|
|
font: inherit;
|
|
color: var(--text);
|
|
background: color-mix(in srgb, var(--text) 5%, transparent);
|
|
border: 1px solid transparent;
|
|
border-left: 3px solid color-mix(in srgb, var(--text) 22%, transparent);
|
|
border-radius: 5px;
|
|
padding: 4px 7px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
[data-module="service-times"] .st-row:hover,
|
|
[data-module="service-times"] .st-row:focus-visible {
|
|
border-color: var(--border);
|
|
border-left-color: var(--accent);
|
|
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
}
|
|
|
|
[data-module="service-times"] .st-row.st-open {
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
[data-module="service-times"] .st-time {
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
[data-module="service-times"] .st-day {
|
|
font-size: 0.74em;
|
|
color: var(--muted);
|
|
}
|
|
|
|
[data-module="service-times"] .st-chips {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
margin-left: auto;
|
|
}
|
|
|
|
[data-module="service-times"] .st-chip {
|
|
font-size: 0.7em;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.03em;
|
|
border-radius: 999px;
|
|
padding: 1px 7px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
[data-module="service-times"] .st-chip-issue,
|
|
[data-module="service-times"] .st-chip-warn {
|
|
color: var(--warn);
|
|
background: color-mix(in srgb, var(--warn) 18%, transparent);
|
|
}
|
|
|
|
[data-module="service-times"] .st-chip-replay {
|
|
color: var(--accent);
|
|
background: color-mix(in srgb, var(--accent) 18%, transparent);
|
|
}
|
|
|
|
[data-module="service-times"] .st-chip-skip {
|
|
color: var(--danger);
|
|
background: color-mix(in srgb, var(--danger) 18%, transparent);
|
|
}
|
|
|
|
[data-module="service-times"] .st-chip-source,
|
|
[data-module="service-times"] .st-chip-quiet {
|
|
color: var(--muted);
|
|
background: color-mix(in srgb, var(--text) 10%, transparent);
|
|
text-transform: none;
|
|
letter-spacing: 0;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Marked services read at a glance from across the booth. */
|
|
[data-module="service-times"] .st-row.st-issue { border-left-color: var(--warn); }
|
|
[data-module="service-times"] .st-row.st-replay { border-left-color: var(--accent); }
|
|
[data-module="service-times"] .st-row.st-skip {
|
|
border-left-color: var(--danger);
|
|
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
|
}
|
|
[data-module="service-times"] .st-row.st-skip .st-time {
|
|
text-decoration: line-through;
|
|
text-decoration-thickness: 2px;
|
|
}
|
|
[data-module="service-times"] .st-row.st-affected {
|
|
border-left-color: var(--danger);
|
|
border-color: color-mix(in srgb, var(--danger) 45%, transparent);
|
|
}
|
|
|
|
[data-module="service-times"] .st-note {
|
|
flex: 1 0 100%;
|
|
font-size: 0.8em;
|
|
color: var(--muted);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/* ── messages ─────────────────────────────────────────────────────── */
|
|
|
|
[data-module="service-times"] .st-empty,
|
|
[data-module="service-times"] .st-updated {
|
|
margin: 4px 2px 0;
|
|
font-size: 0.8em;
|
|
color: var(--muted);
|
|
}
|
|
|
|
[data-module="service-times"] .st-warn {
|
|
margin: 6px 2px 0;
|
|
font-size: 0.8em;
|
|
color: var(--warn);
|
|
}
|
|
|
|
/* ── the mark editor ──────────────────────────────────────────────── */
|
|
|
|
[data-module="service-times"] .st-editor {
|
|
position: sticky;
|
|
bottom: 0;
|
|
margin-top: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
padding: 8px;
|
|
border: 1px solid var(--accent);
|
|
border-radius: 8px;
|
|
background: var(--panel);
|
|
box-shadow: 0 6px 20px rgb(0 0 0 / 0.28);
|
|
}
|
|
|
|
[data-module="service-times"] .st-editor-title {
|
|
font-weight: 700;
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
[data-module="service-times"] .st-editor-status {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
}
|
|
|
|
[data-module="service-times"] .st-pick {
|
|
font: inherit;
|
|
font-size: 0.8em;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
background: color-mix(in srgb, var(--text) 8%, transparent);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 3px 9px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
[data-module="service-times"] .st-pick:hover {
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
[data-module="service-times"] .st-pick.active,
|
|
[data-module="service-times"] .st-save {
|
|
color: var(--accent-text, var(--text));
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
[data-module="service-times"] .st-editor-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
[data-module="service-times"] .st-editor-label {
|
|
font-size: 0.72em;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--muted);
|
|
}
|
|
|
|
[data-module="service-times"] .st-editor-input {
|
|
font: inherit;
|
|
font-size: 0.85em;
|
|
color: var(--text);
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 4px 7px;
|
|
min-width: 0;
|
|
}
|
|
|
|
[data-module="service-times"] .st-editor-input:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
[data-module="service-times"] .st-editor-actions {
|
|
display: flex;
|
|
gap: 5px;
|
|
justify-content: flex-end;
|
|
}
|