feat(web): subtitle lane under the signal chain
This commit is contained in:
@@ -446,6 +446,8 @@
|
|||||||
<p class="module-detail readout" data-role="detail">rpc · grab and seed</p>
|
<p class="module-detail readout" data-role="detail">rpc · grab and seed</p>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="chain-lane" id="chain-subs" aria-label="subtitle lane" hidden></section>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="deck-group" id="group-roots" hidden aria-labelledby="label-roots">
|
<section class="deck-group" id="group-roots" hidden aria-labelledby="label-roots">
|
||||||
|
|||||||
@@ -9,12 +9,28 @@ export interface Check {
|
|||||||
detail?: string;
|
detail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One enabled subtitle provider (#200). */
|
||||||
|
export interface ProviderCheck {
|
||||||
|
id: string;
|
||||||
|
status: CheckStatus;
|
||||||
|
detail?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The subtitle lane: providers in use, the selected engine, the binaries. */
|
||||||
|
export interface SubtitleHealth {
|
||||||
|
providers: ProviderCheck[];
|
||||||
|
translation?: Check | null;
|
||||||
|
alass: Check;
|
||||||
|
ffmpeg: Check;
|
||||||
|
}
|
||||||
|
|
||||||
export interface HealthReport {
|
export interface HealthReport {
|
||||||
status: "ok" | "degraded";
|
status: "ok" | "degraded";
|
||||||
version: string;
|
version: string;
|
||||||
prowlarr: Check;
|
prowlarr: Check;
|
||||||
transmission: Check;
|
transmission: Check;
|
||||||
tmdb: Check;
|
tmdb: Check;
|
||||||
|
subtitles: SubtitleHealth;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Probe =
|
export type Probe =
|
||||||
|
|||||||
+51
-1
@@ -1,4 +1,4 @@
|
|||||||
import { type CheckStatus, type Probe, probeHealth } from "./health";
|
import { type Check, type CheckStatus, type Probe, type SubtitleHealth, probeHealth } from "./health";
|
||||||
import {
|
import {
|
||||||
fetchLibrary,
|
fetchLibrary,
|
||||||
type LibrarySeries,
|
type LibrarySeries,
|
||||||
@@ -171,6 +171,52 @@ function setModule(refs: ModuleRefs, state: string, tone: Tone, word: string, de
|
|||||||
refs.detail.textContent = detail ?? refs.defaultDetail;
|
refs.detail.textContent = detail ?? refs.defaultDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One lamp of the subtitle lane (#200): same module shape as the chain,
|
||||||
|
* rendered per poll — which providers are in use is a settings row, not
|
||||||
|
* markup. */
|
||||||
|
function subtitleModule(name: string, role: string, check: Check): HTMLElement {
|
||||||
|
const article = document.createElement("article");
|
||||||
|
article.className = "module";
|
||||||
|
|
||||||
|
const head = document.createElement("header");
|
||||||
|
head.className = "module-head";
|
||||||
|
const lamp = document.createElement("span");
|
||||||
|
lamp.className = "lamp";
|
||||||
|
lamp.dataset.state = check.status;
|
||||||
|
const title = document.createElement("h2");
|
||||||
|
title.className = "module-name";
|
||||||
|
title.textContent = name;
|
||||||
|
const status = document.createElement("span");
|
||||||
|
status.className = "module-status readout";
|
||||||
|
status.dataset.tone = TONE_BY_STATUS[check.status];
|
||||||
|
status.textContent = check.status;
|
||||||
|
head.append(lamp, title, status);
|
||||||
|
|
||||||
|
const roleLine = document.createElement("p");
|
||||||
|
roleLine.className = "module-role";
|
||||||
|
roleLine.textContent = role;
|
||||||
|
const detail = document.createElement("p");
|
||||||
|
detail.className = "module-detail readout";
|
||||||
|
detail.textContent = check.detail ?? "";
|
||||||
|
|
||||||
|
article.append(head, roleLine, detail);
|
||||||
|
return article;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSubtitles(subLane: HTMLElement, subs: SubtitleHealth) {
|
||||||
|
const entries: { name: string; role: string; check: Check }[] = subs.providers.map(
|
||||||
|
(provider) => ({ name: provider.id, role: "subtitles in", check: provider }),
|
||||||
|
);
|
||||||
|
if (subs.translation) {
|
||||||
|
entries.push({ name: "translate", role: "engine out", check: subs.translation });
|
||||||
|
}
|
||||||
|
entries.push({ name: "alass", role: "timing sync", check: subs.alass });
|
||||||
|
entries.push({ name: "ffmpeg", role: "track extraction", check: subs.ffmpeg });
|
||||||
|
|
||||||
|
subLane.replaceChildren(...entries.map((entry) => subtitleModule(entry.name, entry.role, entry.check)));
|
||||||
|
subLane.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
function must<T extends Element>(selector: string): T {
|
function must<T extends Element>(selector: string): T {
|
||||||
const element = document.querySelector<T>(selector);
|
const element = document.querySelector<T>(selector);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
@@ -181,6 +227,7 @@ function must<T extends Element>(selector: string): T {
|
|||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const chain = must<HTMLElement>(".chain");
|
const chain = must<HTMLElement>(".chain");
|
||||||
|
const subLane = must<HTMLElement>("#chain-subs");
|
||||||
const masterLamp = must<HTMLElement>("#master-lamp");
|
const masterLamp = must<HTMLElement>("#master-lamp");
|
||||||
const version = must<HTMLElement>("#version");
|
const version = must<HTMLElement>("#version");
|
||||||
|
|
||||||
@@ -235,6 +282,8 @@ function main() {
|
|||||||
for (const name of ["tmdb", "prowlarr", "master"] as const) {
|
for (const name of ["tmdb", "prowlarr", "master"] as const) {
|
||||||
setTrace(name, null);
|
setTrace(name, null);
|
||||||
}
|
}
|
||||||
|
subLane.hidden = true;
|
||||||
|
subLane.replaceChildren();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +308,7 @@ function main() {
|
|||||||
setTrace(name, TONE_BY_STATUS[check.status]);
|
setTrace(name, TONE_BY_STATUS[check.status]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
renderSubtitles(subLane, report.subtitles);
|
||||||
}
|
}
|
||||||
|
|
||||||
let inFlight = false;
|
let inFlight = false;
|
||||||
|
|||||||
@@ -315,6 +315,17 @@ body {
|
|||||||
|
|
||||||
/* ---- modules --------------------------------------------------------- */
|
/* ---- modules --------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* The subtitle lane (#200): one lamp per upstream actually in use, rendered
|
||||||
|
by the health poll under the main chain. Same module vocabulary, no bus —
|
||||||
|
these feed the reconcile loop, not each other. */
|
||||||
|
.chain-lane {
|
||||||
|
margin-top: var(--space-6);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
.module {
|
.module {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
|
|||||||
Reference in New Issue
Block a user