init: initial commit

- ticker / QR ad overlay / break slide graphics (1920×1080) with all
  geometry derived from one shared module (src/shared/obs-geometry.mjs)
- OBS scene-collection generator (scripts/generate-obs-scenes.mjs) and a
  read-only in-dashboard setup verifier, both driven by the same module
- en/sr dashboard i18n selected via bundle config; configschema.json with
  neutral first-run defaults seeded from optional branding config
- install.sh / systemd user service / update.sh for venue deployment
- reference OBS scene collection + profile from a real venue as fixtures
This commit is contained in:
2026-07-15 17:43:05 +02:00
commit 4bb64a8ca3
71 changed files with 18186 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
export type Corner = "tr" | "bl";
export interface Ad {
id: string;
/** headline shown in the wide strip */
rowText: string;
/** text shown in the tall column */
columnText: string;
/** asset URL of the image shown in the column, null = no image */
columnImageUrl: string | null;
/** URL encoded into the QR code */
qrUrl: string;
enabled: boolean;
}
export interface AdState {
visible: boolean;
corner: Corner;
adId: string | null;
}
+9
View File
@@ -0,0 +1,9 @@
/** One file in a NodeCG `assets:<category>` replicant. */
export interface AssetFile {
base: string;
category: string;
ext: string;
name: string;
sum: string;
url: string;
}
+57
View File
@@ -0,0 +1,57 @@
export interface ObsSceneNames {
main: string;
mainAdTr: string;
mainAdBl: string;
break: string;
breakAdTr: string;
breakAdBl: string;
}
export interface ObsTransitionNames {
/** transition selected before ad shrink/hop switches (the Move transition) */
default: string;
/** transition selected before MAIN↔BREAK toggles (the stinger) */
break: string;
}
/**
* Names of the shared OBS sources/nested scenes, for the setup verifier and
* scene generator. Defaults live in src/shared/obs-geometry.mjs
* (DEFAULT_SOURCE_NAMES); override here when an existing OBS setup uses
* different names.
*/
export interface ObsSourceNames {
adOverlay: string;
ticker: string;
breakSlide: string;
programScene: string;
breakScene: string;
programPlaceholder: string;
}
export interface ObsConnectionConfig {
enabled: boolean;
url: string;
password: string;
scenes: ObsSceneNames;
transitions: ObsTransitionNames;
sources: ObsSourceNames;
}
/** First-run replicant seeds — only applied when nothing is persisted yet. */
export interface BrandingConfig {
tickerBlurb?: string;
breakHeading?: string;
sampleQrUrl?: string;
}
export interface BundleConfig {
/** dashboard language; defaults to "en" */
locale?: string;
branding?: BrandingConfig;
obs?: Partial<ObsConnectionConfig> & {
scenes?: Partial<ObsSceneNames>;
transitions?: Partial<ObsTransitionNames>;
sources?: Partial<ObsSourceNames>;
};
}
+32
View File
@@ -0,0 +1,32 @@
import type { Corner } from "./ads";
export interface ChoreoConfig {
normalAdIntervalMinutes: number;
adDurationSeconds: number;
/** pause between ad bursts while in break mode */
breakGapSeconds: number;
breakAdDurationSeconds: number;
breakAdsPerBurst: number;
}
export interface SchedulerState {
mode: "normal" | "break";
phase: "live" | "ad";
lastCorner: Corner;
lastAdIndex: number;
burstRemaining: number;
/** epoch ms of the next automatic transition, for dashboard countdown */
nextTransitionAt: number | null;
}
export interface BreakSlide {
heading: string;
nextActName: string;
nextActFrom: string;
/** invert the band logo's colors (dark logo on the dark background) */
logoInverted: boolean;
/** `base` of the assets:break-logo file to show; null = first uploaded */
logoName: string | null;
/** rendered logo height in px (width follows the aspect ratio) */
logoHeightPx: number;
}
+6
View File
@@ -0,0 +1,6 @@
export * from "./ticker";
export * from "./ads";
export * from "./assets";
export * from "./choreo";
export * from "./obs";
export * from "./bundleConfig";
+25
View File
@@ -0,0 +1,25 @@
export interface ObsStatus {
connected: boolean;
currentScene: string | null;
lastError: string | null;
attempts: number;
}
export type ObsCheckStatus = "pass" | "warn" | "fail" | "skip";
export interface ObsCheckResult {
id: string;
label: string;
status: ObsCheckStatus;
/** what was actually found (shown under the label) */
detail?: string;
/** human instructions for making the check pass */
fix?: string;
}
export interface ObsVerifyReport {
/** epoch ms of the last run, null before the first run */
ranAt: number | null;
running: boolean;
checks: ObsCheckResult[];
}
+16
View File
@@ -0,0 +1,16 @@
export interface TickerMessage {
id: string;
text: string;
/** epoch ms when the message was added (or last refreshed) */
addedAt: number;
/** null = permanent message, never swept */
ttlSeconds: number | null;
}
export interface TickerConfig {
/** fixed blurb shown at every loop point */
loopBlurb: string;
separator: string;
defaultTtlSeconds: number;
speedPxPerSec: number;
}