Rewrite dashboard and overlay, single /events stream, static client served by the backend

Backend: one SSE stream for messages, selection, poll and connection status; reconnect with backoff; no on-disk Innertube session cache; mock only with MOCK_CHAT=1; binds to 127.0.0.1; serves client/out so production runs on one port. Full URLs for links YouTube truncates in chat.

Client: components split out, Lucide icons, stream title and status, on-air strip, search, pause, keyboard shortcuts, overlay settings dialog. Overlay themes, position, size, animation and auto-hide via URL, sized for 1080p and scaled with the source. New blueprint theme for the brand background.

Removed Tailwind, better-sqlite3, zod, the timezone helpers, Cline memory bank and AGENTS.md. Added ESLint, node:test tests and CI.
This commit is contained in:
2026-09-02 22:05:58 +03:00
parent 19b412a82a
commit 6bcbd5400b
44 changed files with 6582 additions and 3395 deletions
+34 -10
View File
@@ -1,26 +1,27 @@
export type Badge = {
type: 'moderator' | 'member' | 'verified' | 'custom';
type: 'moderator' | 'member' | 'verified' | 'owner' | 'custom';
label?: string;
icon?: string;
imageUrl?: string; // For custom membership badges
imageUrl?: string;
};
export type SuperChatInfo = {
amount: string;
currency: string;
color: string;
stickerUrl?: string; // For super stickers
stickerAlt?: string; // Accessibility label for super stickers
stickerUrl?: string;
stickerAlt?: string;
};
export type MessageRun = {
text?: string;
/** Full destination URL when YouTube rendered this run as a link. The visible text may be truncated. */
url?: string;
emojiUrl?: string;
emojiAlt?: string;
};
export type Poll = {
id: string; // live_chat_poll_id from YouTube
id: string;
active: boolean;
};
@@ -30,7 +31,7 @@ export type ChatMessage = {
authorPhoto?: string;
authorChannelId?: string;
text: string;
runs?: MessageRun[]; // structured runs for emojis
runs?: MessageRun[];
publishedAt: string;
badges?: Badge[];
isModerator?: boolean;
@@ -39,7 +40,30 @@ export type ChatMessage = {
superChat?: SuperChatInfo;
membershipGift?: boolean;
membershipGiftPurchase?: boolean;
membershipLevel?: string; // e.g., "New member", "Member (6 months)", etc.
giftCount?: number; // Number of memberships gifted
leaderboardRank?: number; // YouTube leaderboard rank (1, 2, 3, etc.)
membershipLevel?: string;
giftCount?: number;
leaderboardRank?: number;
};
export type ConnectionState = 'disconnected' | 'connecting' | 'live' | 'reconnecting';
export type ConnectionStatus = {
state: ConnectionState;
liveId: string | null;
title?: string | null;
error?: string | null;
};
/**
* Events pushed on the single `/events` SSE stream.
* `init` is sent once per connection with the full current state; the rest are deltas.
*/
export type ServerEvent =
| { type: 'init'; status: ConnectionStatus; messages: ChatMessage[]; selection: ChatMessage | null; poll: Poll | null }
| { type: 'message'; message: ChatMessage }
| { type: 'selection'; message: ChatMessage | null }
| { type: 'poll'; poll: Poll | null }
| { type: 'status'; status: ConnectionStatus }
| { type: 'clear' };
export type ServerEventType = ServerEvent['type'];