blackroadio commited on
Commit
e037c1e
·
1 Parent(s): e5f38a0

BlackRoad Network v2: full broadcast system + modular codebase

Browse files

v1 (index.html inline):
- 43 channels, 13 vibes, 43-block schedule
- Single-player rotation with fallback chains
- Manual override with 10min auto-resume
- Broadcast guide with NOW/NEXT/LATER
- Source validation, session health, standby mode
- Fleet monitors, ticker, autoplay-safe

v2 (modular /v2/):
- 21-file ES module architecture
- Router, state, storage persistence
- Attention engine: starts/skips/completions scoring
- Personal channel (/live/me): time-of-day + category affinity
- Video-level override panel
- Debug panel with resolver explanation
- Fallback chain resolution with load timeout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

RoadChain-SHA2048: b9ea2ef8ca4c6c0a
RoadChain-Identity: alexa@sovereign
RoadChain-Full: b9ea2ef8ca4c6c0a56dc7108af3ac18741314429c3036c3f340d0fb1d338e1a2cb0daf1c1191e15131e32d2966e95b5b70c146bb596cb27779d01ef50c1986b3f5e8e2c15ef0419cb3e838af2b3872cf50abd8d73fbc8c54bb9d4142835934f9def928b991c7d891e1f3de9b6815684fdf1caaa3acb8ad7cbfa0e95f9d112bda00f5413da151306875b229c5d1314529b60ee603b62e1252fdfc2aaa56d095a36cc7eba12fef961ce35bb872c8c97d13a051e77adf774c70e655e2ce349282021215a9c7439777d4013de9483b128c199b4e8c09c53c18adc4221600845d13acf357516ab98ed85e4df13c43025c987b7e68d536049886a39abded4fef79dd63

channels.js ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // channels.js — BlackRoad Network Source Registry
2
+ // 36 channels across 10 vibes. Never raw channel URLs. Curated playlists or uploads only.
3
+ // Upload playlists (UU prefix) are used as defaults — swap for curated playlists when available.
4
+
5
+ export const CHANNELS = [
6
+ // ── CORE: MATH ──
7
+ { id: '3blue1brown', name: '3Blue1Brown', type: 'youtube_playlist',
8
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=PLZHQObOWTQDPD3MizzM2xVFitgF8hE_ab&autoplay=1&mute=1&playsinline=1',
9
+ category: 'Math', description: 'Visual mathematics and intuition-first explanations.',
10
+ durationMinutes: 20, priority: 10, enabled: true, health: 'unknown' },
11
+ { id: 'numberphile', name: 'Numberphile', type: 'youtube_playlist',
12
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=PLt5AfwLFPxWJ8GCgpFo5_OSyfl7j0nOiu&autoplay=1&mute=1&playsinline=1',
13
+ category: 'Math', description: 'Math stories, ideas, oddities, and number intuition.',
14
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
15
+
16
+ // ── CORE: SYSTEMS ──
17
+ { id: 'computerphile', name: 'Computerphile', type: 'youtube_playlist',
18
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=PLzH6n4zXuckquVnQ0KlMDxyT5YE-sA8Ps&autoplay=1&mute=1&playsinline=1',
19
+ category: 'Systems', description: 'Computer science, security, and programming concepts.',
20
+ durationMinutes: 20, priority: 10, enabled: true, health: 'unknown' },
21
+
22
+ // ── CORE: SCIENCE ──
23
+ { id: 'veritasium', name: 'Veritasium', type: 'youtube_playlist',
24
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=PLkahZjV5wKe_dajngssVLffaCh2gbq55g&autoplay=1&mute=1&playsinline=1',
25
+ category: 'Science', description: 'Deep science, experiments, and surprising explanations.',
26
+ durationMinutes: 20, priority: 10, enabled: true, health: 'unknown' },
27
+ { id: 'sixtysymbols', name: 'Sixty Symbols', type: 'youtube_playlist',
28
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUvBqzzvUBLCs8Y7Axb-jZew&autoplay=1&mute=1&playsinline=1',
29
+ category: 'Physics', description: 'Physics, astronomy, and scientific ideas.',
30
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
31
+
32
+ // ── CORE: VISUAL EXPLAINERS ──
33
+ { id: 'kurzgesagt', name: 'Kurzgesagt', type: 'youtube_playlist',
34
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=PLFs4vir_WsTwEd-nJgVJCZPNL3HALHHpF&autoplay=1&mute=1&playsinline=1',
35
+ category: 'Visual Explainers', description: 'Animated explainers for science, humanity, and systems.',
36
+ durationMinutes: 20, priority: 9, enabled: true, health: 'unknown' },
37
+ { id: 'infographicsshow', name: 'The Infographics Show', type: 'youtube_playlist',
38
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUfdNM3NAhaBOXCafH7krzrA&autoplay=1&mute=1&playsinline=1',
39
+ category: 'Explainers', description: 'Narrated explainers, scenarios, and current-interest topics.',
40
+ durationMinutes: 20, priority: 6, enabled: true, health: 'unknown' },
41
+
42
+ // ── CORE: EDUCATION ──
43
+ { id: 'khanacademy', name: 'Khan Academy', type: 'youtube_playlist',
44
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=PLSQl0a2vh4HC5feHa6U5g0yNLNIRYhILj&autoplay=1&mute=1&playsinline=1',
45
+ category: 'Education', description: 'Core learning blocks across math, science, and more.',
46
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
47
+ { id: 'mitocw', name: 'MIT OpenCourseWare', type: 'youtube_playlist',
48
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=PLUl4u3cNGP63oMNUHXqIGcBbal4NOL6Y&autoplay=1&mute=1&playsinline=1',
49
+ category: 'Lecture', description: 'Long-form technical and academic content.',
50
+ durationMinutes: 30, priority: 7, enabled: true, health: 'unknown' },
51
+ { id: 'crashcourse', name: 'Crash Course', type: 'youtube_playlist',
52
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=PL8dPuuaLjXtNlUrzyH5r6jN9ulIgZBpdo&autoplay=1&mute=1&playsinline=1',
53
+ category: 'Education', description: 'Fast-paced educational explainers across disciplines.',
54
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
55
+
56
+ // ── CORE: CONTEXT / JOURNALISM ──
57
+ { id: 'vox', name: 'Vox', type: 'youtube_playlist',
58
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=PLJ8cMiYb3G5dOTLINen1K7E4wSMfWjbI7&autoplay=1&mute=1&playsinline=1',
59
+ category: 'Context', description: 'Context-heavy journalism and visual reporting.',
60
+ durationMinutes: 15, priority: 6, enabled: true, health: 'unknown' },
61
+ { id: 'vice', name: 'Vice', type: 'youtube_playlist',
62
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUn8zNIfYAQNdrFRrr8oibKw&autoplay=1&mute=1&playsinline=1',
63
+ category: 'Documentary', description: 'Documentary-style stories and field reporting.',
64
+ durationMinutes: 15, priority: 5, enabled: true, health: 'unknown' },
65
+ { id: 'insider', name: 'Insider', type: 'youtube_playlist',
66
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUHJuQZuzapBh-CuhRYxIZrg&autoplay=1&mute=1&playsinline=1',
67
+ category: 'Features', description: 'Feature journalism, industry explainers, and visual stories.',
68
+ durationMinutes: 15, priority: 5, enabled: true, health: 'unknown' },
69
+
70
+ // ── HIGH-INTEL ──
71
+ { id: 'pbsspacetime', name: 'PBS Space Time', type: 'youtube_playlist',
72
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUYNbYGl89UUowy8oXkipC-Q&autoplay=1&mute=1&playsinline=1',
73
+ category: 'Deep Space', description: 'Astrophysics, cosmology, and spacetime at the edge of knowledge.',
74
+ durationMinutes: 20, priority: 9, enabled: true, health: 'unknown' },
75
+ { id: 'sabinehossenfelder', name: 'Sabine Hossenfelder', type: 'youtube_playlist',
76
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UU1yNl2E66ArXEBTf8JXSvCg&autoplay=1&mute=1&playsinline=1',
77
+ category: 'Physics Frontier', description: 'Theoretical physics, science critique, and honest takes.',
78
+ durationMinutes: 20, priority: 9, enabled: true, health: 'unknown' },
79
+ { id: 'closertotruth', name: 'Closer To Truth', type: 'youtube_playlist',
80
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUl_dlV_7ofr4bSwJnk9gs-g&autoplay=1&mute=1&playsinline=1',
81
+ category: 'Ultimate Questions', description: 'Consciousness, reality, existence. The deep end.',
82
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
83
+
84
+ // ── HACKER / BUILDER ──
85
+ { id: 'lowlevellearning', name: 'Low Level Learning', type: 'youtube_playlist',
86
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUc4Rz_T9Sb1w5SVhv2HGMkA&autoplay=1&mute=1&playsinline=1',
87
+ category: 'Hacker', description: 'Low-level systems, C, Rust, and why things actually work.',
88
+ durationMinutes: 20, priority: 9, enabled: true, health: 'unknown' },
89
+ { id: 'liveoverflow', name: 'LiveOverflow', type: 'youtube_playlist',
90
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUlcE-kVhqyiHCcjYwcpfj9w&autoplay=1&mute=1&playsinline=1',
91
+ category: 'Security', description: 'Security research, CTFs, and hacking explained.',
92
+ durationMinutes: 20, priority: 9, enabled: true, health: 'unknown' },
93
+ { id: 'beneater', name: 'Ben Eater', type: 'youtube_playlist',
94
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUS0N5baNlQWJCUrhCEo8WlA&autoplay=1&mute=1&playsinline=1',
95
+ category: 'Hardware', description: 'Building computers from scratch. Breadboards and truth tables.',
96
+ durationMinutes: 20, priority: 9, enabled: true, health: 'unknown' },
97
+ { id: 'thecodingtrain', name: 'The Coding Train', type: 'youtube_playlist',
98
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUvjgXvBlISQQnl7eYng8Chg&autoplay=1&mute=1&playsinline=1',
99
+ category: 'Creative Code', description: 'Creative coding, visual algorithms, and joyful hacking.',
100
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
101
+
102
+ // ── WEIRD / EXPERIMENTAL ──
103
+ { id: 'michaelreeves', name: 'Michael Reeves', type: 'youtube_playlist',
104
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUtHaxi4GTYDpJgMSGy7AeSw&autoplay=1&mute=1&playsinline=1',
105
+ category: 'Chaos', description: 'Unhinged engineering. Robots that should not exist.',
106
+ durationMinutes: 20, priority: 6, enabled: true, health: 'unknown' },
107
+ { id: 'stuffmadehere', name: 'Stuff Made Here', type: 'youtube_playlist',
108
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUj1VqrHhDte54oLgPG4xpuQ&autoplay=1&mute=1&playsinline=1',
109
+ category: 'Invention', description: 'Over-engineered solutions to problems nobody asked about.',
110
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
111
+ { id: 'markrober', name: 'Mark Rober', type: 'youtube_playlist',
112
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUY1kMZp36IQSyNx_9h4mpCg&autoplay=1&mute=1&playsinline=1',
113
+ category: 'Engineering Play', description: 'Engineering + spectacle. Glitter bombs and squirrel courses.',
114
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
115
+
116
+ // ── PHILOSOPHY / EXISTENTIAL ──
117
+ { id: 'exurb1a', name: 'Exurb1a', type: 'youtube_playlist',
118
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUimiUgDLbi6P17BdaCZpVbg&autoplay=1&mute=1&playsinline=1',
119
+ category: 'Philosophy', description: 'Existential dread disguised as comedy. The meaning layer.',
120
+ durationMinutes: 20, priority: 10, enabled: true, health: 'unknown' },
121
+ { id: 'pursuitofwonder', name: 'Pursuit of Wonder', type: 'youtube_playlist',
122
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUMbQbXKGSJ2LqHHoaGRJ5sA&autoplay=1&mute=1&playsinline=1',
123
+ category: 'Reflection', description: 'Quiet philosophy. Questions that stay with you.',
124
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
125
+ { id: 'academyofideas', name: 'Academy of Ideas', type: 'youtube_playlist',
126
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUiRiQGCHGjDLT9FQXFW0I3A&autoplay=1&mute=1&playsinline=1',
127
+ category: 'Big Ideas', description: 'Nietzsche, Jung, freedom, tyranny. Intellectual history.',
128
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
129
+
130
+ // ── HOW THE WORLD WORKS ──
131
+ { id: 'realengineering', name: 'Real Engineering', type: 'youtube_playlist',
132
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUR1IuLEqb6UEA0zQ1PeYXXg&autoplay=1&mute=1&playsinline=1',
133
+ category: 'Engineering', description: 'How real systems are designed and why they work.',
134
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
135
+ { id: 'practicalengineering', name: 'Practical Engineering', type: 'youtube_playlist',
136
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUMOqf8ab-42UUQIdVoKwjlQ&autoplay=1&mute=1&playsinline=1',
137
+ category: 'Real World', description: 'Civil engineering and infrastructure explained.',
138
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
139
+ { id: 'wendover', name: 'Wendover Productions', type: 'youtube_playlist',
140
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UU9RM-iSvTu1uPJb8X5yp3EQ&autoplay=1&mute=1&playsinline=1',
141
+ category: 'Systems Logic', description: 'Logistics, economics, and hidden systems that run the world.',
142
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
143
+
144
+ // ── AI / FUTURE / TECH EDGE ──
145
+ { id: 'twominutepapers', name: 'Two Minute Papers', type: 'youtube_playlist',
146
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUbfYPyITQ-7l4upoX8nvctg&autoplay=1&mute=1&playsinline=1',
147
+ category: 'AI Edge', description: 'Latest AI research in two minutes. What a time to be alive.',
148
+ durationMinutes: 20, priority: 9, enabled: true, health: 'unknown' },
149
+ { id: 'yannickilcher', name: 'Yannic Kilcher', type: 'youtube_playlist',
150
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUZHmQk67mSJgfCCTn7xBfew&autoplay=1&mute=1&playsinline=1',
151
+ category: 'AI Edge', description: 'Deep ML paper reviews and AI industry analysis.',
152
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
153
+ { id: 'coldfusion', name: 'ColdFusion', type: 'youtube_playlist',
154
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UU4QZ_LsYcvcq7qOsOhpAI4A&autoplay=1&mute=1&playsinline=1',
155
+ category: 'Future Tech', description: 'Tech history, future trends, and industry stories.',
156
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
157
+
158
+ // ── CINEMATIC / PREMIUM STORY ──
159
+ { id: 'johnnyharris', name: 'Johnny Harris', type: 'youtube_playlist',
160
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUmGSJVG3mCRXVOP4yZrU1Dw&autoplay=1&mute=1&playsinline=1',
161
+ category: 'World Report', description: 'Cinematic journalism. Maps, borders, and hidden stories.',
162
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
163
+ { id: 'lemmino', name: 'Lemmino', type: 'youtube_playlist',
164
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UURcgy6GzDeccI7dkbbBna3Q&autoplay=1&mute=1&playsinline=1',
165
+ category: 'Cinematic', description: 'Premium mini-documentaries. Unsolved mysteries and deep dives.',
166
+ durationMinutes: 20, priority: 9, enabled: true, health: 'unknown' },
167
+ { id: 'neo', name: 'Neo', type: 'youtube_playlist',
168
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUd57xvMlrfbIoSPp-rT8Ibg&autoplay=1&mute=1&playsinline=1',
169
+ category: 'Deep Story', description: 'Short-form documentary storytelling.',
170
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
171
+
172
+ // ── DOCUMENTARY CORE ──
173
+ { id: 'dwdocumentary', name: 'DW Documentary', type: 'youtube_playlist',
174
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUW39zufHfsuGgpLviKh297Q&autoplay=1&mute=1&playsinline=1',
175
+ category: 'Documentary', description: 'German public broadcasting. Industry, economy, global systems.',
176
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
177
+ { id: 'freedocumentary', name: 'Free Documentary', type: 'youtube_playlist',
178
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUijmHCrS0A-cNLdkpnVfn0w&autoplay=1&mute=1&playsinline=1',
179
+ category: 'Documentary', description: 'Full-length documentaries. Nature, science, society.',
180
+ durationMinutes: 20, priority: 6, enabled: true, health: 'unknown' },
181
+ { id: 'timeline', name: 'Timeline', type: 'youtube_playlist',
182
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UU88lvyJe7aHZmcvzvubDFRg&autoplay=1&mute=1&playsinline=1',
183
+ category: 'History', description: 'World history documentaries. Ancient to modern.',
184
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
185
+ { id: 'pbsfrontline', name: 'PBS Frontline', type: 'youtube_playlist',
186
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUrJuxWJgFNh_bJEqMPm_D8w&autoplay=1&mute=1&playsinline=1',
187
+ category: 'Investigative', description: 'Investigative journalism. Deep, serious, unflinching.',
188
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
189
+
190
+ // ── ARCHIVE / VINTAGE ──
191
+ { id: 'periscopefilm', name: 'PeriscopeFilm', type: 'youtube_playlist',
192
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUrJuxWJgFNh_bJEqMPm_D8w&autoplay=1&mute=1&playsinline=1',
193
+ category: 'Archive', description: 'Rescued films from 1920s-1980s. Training films, industrial, aviation, Bell Labs.',
194
+ durationMinutes: 20, priority: 8, enabled: true, health: 'unknown' },
195
+
196
+ // ── SPECIAL AUDIENCE / HUMAN STORIES ──
197
+ { id: 'truly', name: 'Truly', type: 'youtube_playlist',
198
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UUb-fMTiGfOQ4NF1EaGjbiiQ&autoplay=1&mute=1&playsinline=1',
199
+ category: 'Human Stories', description: 'Unusual lives, real people, empathetic curiosity.',
200
+ durationMinutes: 20, priority: 6, enabled: true, health: 'unknown' },
201
+ { id: 'softwhiteunderbelly', name: 'Soft White Underbelly', type: 'youtube_playlist',
202
+ embedUrl: 'https://www.youtube.com/embed/videoseries?list=UU4E98HDsPXrf5kTKIgrSmtQ&autoplay=1&mute=1&playsinline=1',
203
+ category: 'Human Stories', description: 'Raw interviews. Addiction, poverty, survival. The real world.',
204
+ durationMinutes: 20, priority: 7, enabled: true, health: 'unknown' },
205
+
206
+ // ── INTERNAL ──
207
+ { id: 'blackroadosinc', name: 'BlackRoad OS', type: 'internal',
208
+ embedUrl: '', category: 'Internal Broadcast',
209
+ description: 'BlackRoad-native content, agent activity, and system updates.',
210
+ durationMinutes: 20, priority: 100, enabled: true, health: 'healthy' },
211
+ ];
212
+
213
+ // ── Helpers ──
214
+
215
+ export function getEnabledChannels() {
216
+ return CHANNELS.filter(function(c) { return c.enabled; });
217
+ }
218
+
219
+ export function getChannelById(id) {
220
+ return CHANNELS.find(function(c) { return c.id === id; }) || null;
221
+ }
222
+
223
+ export function getChannelsByCategory(category) {
224
+ return CHANNELS.filter(function(c) { return c.enabled && c.category === category; });
225
+ }
226
+
227
+ export function getHealthyChannels() {
228
+ return CHANNELS.filter(function(c) { return c.enabled && c.health !== 'down'; });
229
+ }
230
+
231
+ export function markChannelHealth(id, status) {
232
+ var ch = CHANNELS.find(function(c) { return c.id === id; });
233
+ if (ch) ch.health = status;
234
+ }
235
+
236
+ export function getNextHealthyFallback(fallbackIds) {
237
+ if (!fallbackIds || !fallbackIds.length) return null;
238
+ for (var i = 0; i < fallbackIds.length; i++) {
239
+ var ch = getChannelById(fallbackIds[i]);
240
+ if (ch && ch.enabled && ch.health !== 'down') return ch;
241
+ }
242
+ return null;
243
+ }
index.html CHANGED
The diff for this file is too large to render. See raw diff
 
live.js ADDED
@@ -0,0 +1,570 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // live.js — BlackRoad Network Broadcast Engine
2
+ // Validate, resolve, switch, fail over, manual override, auto-resume.
3
+ // One player. One source. No fireplace. No junk.
4
+
5
+ import {
6
+ CHANNELS,
7
+ getChannelById,
8
+ getHealthyChannels,
9
+ } from "./channels.js";
10
+
11
+ import {
12
+ SCHEDULE,
13
+ getCurrentBlock,
14
+ getNextBlock,
15
+ getBlockAfterNext,
16
+ formatTimeHHMM,
17
+ } from "./schedule.js";
18
+
19
+ // ── DOM refs ──
20
+ const player = document.getElementById("live-player");
21
+ const overlay = document.getElementById("autoplay-overlay");
22
+ const nativeScreen = document.getElementById("native-screen");
23
+ const channelNameEl = document.getElementById("channel-name");
24
+ const channelDot = document.getElementById("channel-dot");
25
+ const programTitleEl = document.getElementById("program-title");
26
+ const programCategoryEl = document.getElementById("program-category");
27
+ const sourceModeEl = document.getElementById("source-mode");
28
+ const sourceReasonEl = document.getElementById("source-reason");
29
+ const manualCountdownEl = document.getElementById("manual-countdown");
30
+ const schedNowTitle = document.getElementById("sched-now-title");
31
+ const schedNowSource = document.getElementById("sched-now-source");
32
+ const schedNowTime = document.getElementById("sched-now-time");
33
+ const schedNextTitle = document.getElementById("sched-next-title");
34
+ const schedNextSource = document.getElementById("sched-next-source");
35
+ const schedNextTime = document.getElementById("sched-next-time");
36
+ const schedLaterTitle = document.getElementById("sched-later-title");
37
+ const schedLaterSource = document.getElementById("sched-later-source");
38
+ const schedLaterTime = document.getElementById("sched-later-time");
39
+ const clockEl = document.getElementById("clock");
40
+ const channelGrid = document.getElementById("channel-grid");
41
+
42
+ // ── Constants ──
43
+ const LOAD_TIMEOUT_MS = 7000;
44
+ const MANUAL_OVERRIDE_MS = 10 * 60 * 1000;
45
+ const SKIP_RESUME_MS = 60 * 1000;
46
+ const TICK_INTERVAL_MS = 10000;
47
+ const DEBUG = true;
48
+
49
+ const STORAGE_KEYS = {
50
+ mode: "blackroad.live.mode",
51
+ manualChannelId: "blackroad.live.manualChannelId",
52
+ manualStartedAt: "blackroad.live.manualStartedAt",
53
+ lastSourceId: "blackroad.live.lastSourceId",
54
+ autoplay: "br-live-autoplay",
55
+ };
56
+
57
+ // ── State ──
58
+ const state = {
59
+ currentSourceId: null,
60
+ currentBlockTitle: null,
61
+ loadTimeoutId: null,
62
+ loadSettled: false,
63
+ };
64
+
65
+ const SESSION_HEALTH = {};
66
+
67
+ // ── Debug ──
68
+ function logDebug(event, payload) {
69
+ if (!DEBUG) return;
70
+ console.log("[BlackRoad Live] " + event, payload || "");
71
+ }
72
+
73
+ // ── localStorage helpers ──
74
+ function setStored(key, value) {
75
+ localStorage.setItem(key, JSON.stringify(value));
76
+ }
77
+
78
+ function getStored(key, fallback) {
79
+ try {
80
+ var raw = localStorage.getItem(key);
81
+ return raw !== null ? JSON.parse(raw) : (fallback !== undefined ? fallback : null);
82
+ } catch (e) {
83
+ return fallback !== undefined ? fallback : null;
84
+ }
85
+ }
86
+
87
+ // ── Mode management ──
88
+ function setMode(mode) {
89
+ setStored(STORAGE_KEYS.mode, mode);
90
+ if (sourceModeEl) sourceModeEl.textContent = mode;
91
+ }
92
+
93
+ function getMode() {
94
+ return getStored(STORAGE_KEYS.mode, "AUTO");
95
+ }
96
+
97
+ function enterManualMode(channelId) {
98
+ setStored(STORAGE_KEYS.manualChannelId, channelId);
99
+ setStored(STORAGE_KEYS.manualStartedAt, Date.now());
100
+ setMode("MANUAL");
101
+ logDebug("ENTER_MANUAL_MODE", { channelId: channelId });
102
+ }
103
+
104
+ function clearManualMode() {
105
+ localStorage.removeItem(STORAGE_KEYS.manualChannelId);
106
+ localStorage.removeItem(STORAGE_KEYS.manualStartedAt);
107
+ setMode("AUTO");
108
+ logDebug("CLEAR_MANUAL_MODE");
109
+ }
110
+
111
+ function isManualModeActive() {
112
+ var startedAt = getStored(STORAGE_KEYS.manualStartedAt, 0);
113
+ var channelId = getStored(STORAGE_KEYS.manualChannelId, null);
114
+ if (!startedAt || !channelId) return false;
115
+ return Date.now() - startedAt < MANUAL_OVERRIDE_MS;
116
+ }
117
+
118
+ function getManualTimeRemaining() {
119
+ var startedAt = getStored(STORAGE_KEYS.manualStartedAt, 0);
120
+ if (!startedAt) return 0;
121
+ return Math.max(0, MANUAL_OVERRIDE_MS - (Date.now() - startedAt));
122
+ }
123
+
124
+ function formatRemaining(ms) {
125
+ var totalSec = Math.ceil(ms / 1000);
126
+ var min = Math.floor(totalSec / 60);
127
+ var sec = totalSec % 60;
128
+ return String(min).padStart(2, "0") + ":" + String(sec).padStart(2, "0");
129
+ }
130
+
131
+ function updateManualCountdown() {
132
+ if (!manualCountdownEl) return;
133
+ if (!isManualModeActive()) {
134
+ manualCountdownEl.textContent = "";
135
+ return;
136
+ }
137
+ manualCountdownEl.textContent = "MANUAL " + formatRemaining(getManualTimeRemaining());
138
+ }
139
+
140
+ // ── Session Health ──
141
+ function getSessionHealth(channelId) {
142
+ if (!SESSION_HEALTH[channelId]) {
143
+ SESSION_HEALTH[channelId] = { failures: 0, degraded: false, reasons: [] };
144
+ }
145
+ return SESSION_HEALTH[channelId];
146
+ }
147
+
148
+ function markSourceFailure(channelId, reason) {
149
+ var h = getSessionHealth(channelId);
150
+ h.failures += 1;
151
+ h.reasons.push(reason);
152
+ if (h.failures >= 2) h.degraded = true;
153
+ logDebug("SOURCE_FAILURE", { channelId: channelId, reason: reason, failures: h.failures, degraded: h.degraded });
154
+ }
155
+
156
+ // ── Source Validation ──
157
+ function validateSource(channel) {
158
+ if (!channel) return { valid: false, reason: "channel_not_found" };
159
+ if (!channel.enabled) return { valid: false, reason: "channel_disabled" };
160
+ if (channel.type !== "internal" && !channel.embedUrl) return { valid: false, reason: "missing_embed_url" };
161
+ if (channel.health === "down") return { valid: false, reason: "channel_marked_down" };
162
+ var sh = getSessionHealth(channel.id);
163
+ if (sh.degraded) return { valid: false, reason: "session_degraded" };
164
+ return { valid: true, reason: "ok" };
165
+ }
166
+
167
+ // ── Fallback Resolution ──
168
+ function resolvePlayableSourceIds(ids) {
169
+ for (var i = 0; i < ids.length; i++) {
170
+ var ch = getChannelById(ids[i]);
171
+ var v = validateSource(ch);
172
+ logDebug("VALIDATE_SOURCE", { sourceId: ids[i], valid: v.valid, reason: v.reason });
173
+ if (v.valid) {
174
+ return {
175
+ channel: ch,
176
+ reason: i === 0 ? "primary_ok" : "fallback_after_" + ids[0],
177
+ usedFallback: i > 0,
178
+ };
179
+ }
180
+ }
181
+ // Last resort
182
+ var healthy = getHealthyChannels();
183
+ for (var j = 0; j < healthy.length; j++) {
184
+ var v2 = validateSource(healthy[j]);
185
+ if (v2.valid) return { channel: healthy[j], reason: "last_resort_healthy", usedFallback: true };
186
+ }
187
+ return { channel: null, reason: "no_playable_source_found", usedFallback: true };
188
+ }
189
+
190
+ function resolvePlayableSource(block) {
191
+ if (!block) return { channel: null, reason: "no_block", usedFallback: true };
192
+ return resolvePlayableSourceIds([block.sourceId].concat(block.fallbackSourceIds || []));
193
+ }
194
+
195
+ // ── Category colors (shapes only) ──
196
+ const CATEGORY_COLORS = {
197
+ Math: "#2979FF", Systems: "#9C27B0", Science: "#FF1D6C",
198
+ "Visual Explainers": "#F5A623", Education: "#F5A623", Lecture: "#2979FF",
199
+ Context: "#F5A623", Explainers: "#F5A623", Documentary: "#FF1D6C",
200
+ Features: "#FF1D6C", Physics: "#2979FF", Internal: "#00E676",
201
+ "Internal Broadcast": "#00E676",
202
+ };
203
+
204
+ function getCategoryColor(cat) {
205
+ return CATEGORY_COLORS[cat] || "#888";
206
+ }
207
+
208
+ // ── UI Update ──
209
+ function updateMeta(channel, block, meta) {
210
+ meta = meta || {};
211
+ if (channelNameEl) channelNameEl.textContent = channel ? channel.name : "Standby";
212
+ if (channelDot) channelDot.style.background = getCategoryColor(channel ? channel.category : "Internal");
213
+ if (programTitleEl) programTitleEl.textContent = block ? block.title : "Standby Broadcast";
214
+ if (programCategoryEl) programCategoryEl.textContent = channel ? channel.category : "";
215
+ if (sourceModeEl) sourceModeEl.textContent = meta.mode || getMode();
216
+ if (sourceReasonEl) sourceReasonEl.textContent = meta.reason || "";
217
+ document.querySelectorAll(".ch-btn").forEach(function (btn) {
218
+ btn.classList.toggle("active", channel && btn.dataset.id === channel.id);
219
+ });
220
+ }
221
+
222
+ // ── Standby Mode ──
223
+ function enterStandbyMode(reason) {
224
+ logDebug("ENTER_STANDBY", { reason: reason });
225
+ setMode("STANDBY");
226
+ state.currentSourceId = "__standby__";
227
+ state.currentBlockTitle = "Standby Broadcast";
228
+ if (player) { player.removeAttribute("src"); player.style.display = "none"; }
229
+ if (nativeScreen) {
230
+ nativeScreen.style.display = "flex";
231
+ var pre = nativeScreen.querySelector("pre");
232
+ if (pre) {
233
+ pre.textContent = [
234
+ "BLACKROAD NETWORK", "\u2501".repeat(28), "",
235
+ "STANDBY BROADCAST", new Date().toLocaleTimeString("en-US", { hour12: true }), "",
236
+ "All scheduled sources unavailable.", "Retrying shortly.", "",
237
+ "Reason: " + (reason || "unknown"), "",
238
+ "\u2501".repeat(28), "Remember the Road. Pave Tomorrow.",
239
+ ].join("\n");
240
+ }
241
+ }
242
+ updateMeta(null, null, { mode: "STANDBY", usedFallback: true, reason: reason });
243
+ }
244
+
245
+ // ── Native Screen ──
246
+ function updateNativeScreen() {
247
+ if (!nativeScreen) return;
248
+ var pre = nativeScreen.querySelector("pre");
249
+ if (pre) {
250
+ pre.textContent = [
251
+ "BLACKROAD NETWORK", "\u2501".repeat(28), "",
252
+ "INTERNAL BROADCAST", new Date().toLocaleTimeString("en-US", { hour12: true }), "",
253
+ "18 products live", "27 agents online", "44 orgs / 2,890 repos",
254
+ "$150/mo runs everything", "",
255
+ "\u2501".repeat(28), "Remember the Road. Pave Tomorrow.",
256
+ ].join("\n");
257
+ }
258
+ }
259
+
260
+ // ── Switch Player Source (with load timeout) ──
261
+ function switchToSource(channel, block, meta) {
262
+ meta = meta || {};
263
+ if (!player || !channel) { enterStandbyMode("missing_player_or_channel"); return; }
264
+ var blockTitle = block ? block.title : channel.name;
265
+ if (state.currentSourceId === channel.id && state.currentBlockTitle === blockTitle) return;
266
+
267
+ if (state.loadTimeoutId) { clearTimeout(state.loadTimeoutId); state.loadTimeoutId = null; }
268
+ state.loadSettled = false;
269
+ state.currentSourceId = channel.id;
270
+ state.currentBlockTitle = blockTitle;
271
+ setStored(STORAGE_KEYS.lastSourceId, channel.id);
272
+ updateMeta(channel, block, meta);
273
+
274
+ if (channel.type === "internal") {
275
+ player.removeAttribute("src"); player.style.display = "none";
276
+ if (nativeScreen) { nativeScreen.style.display = "flex"; updateNativeScreen(); }
277
+ state.loadSettled = true;
278
+ logDebug("SWITCH_SOURCE", { channelId: channel.id, type: "internal" });
279
+ return;
280
+ }
281
+
282
+ if (nativeScreen) nativeScreen.style.display = "none";
283
+ player.style.display = "block";
284
+ logDebug("SWITCH_SOURCE", { channelId: channel.id, embedUrl: channel.embedUrl, meta: meta });
285
+
286
+ state.loadTimeoutId = setTimeout(function () {
287
+ if (state.loadSettled) return;
288
+ state.loadSettled = true;
289
+ logDebug("LOAD_TIMEOUT", { channelId: channel.id, timeoutMs: LOAD_TIMEOUT_MS });
290
+ markSourceFailure(channel.id, "load_timeout");
291
+ if (getMode() === "MANUAL") bootManual(true);
292
+ else bootAuto(true);
293
+ }, LOAD_TIMEOUT_MS);
294
+
295
+ player.src = channel.embedUrl;
296
+ }
297
+
298
+ // ── Player Events ──
299
+ function onPlayerLoad() {
300
+ if (state.loadSettled) return;
301
+ state.loadSettled = true;
302
+ if (state.loadTimeoutId) { clearTimeout(state.loadTimeoutId); state.loadTimeoutId = null; }
303
+ logDebug("PLAYER_LOADED", { channelId: state.currentSourceId });
304
+ }
305
+
306
+ function onPlayerError() {
307
+ if (state.loadSettled) return;
308
+ state.loadSettled = true;
309
+ if (state.loadTimeoutId) { clearTimeout(state.loadTimeoutId); state.loadTimeoutId = null; }
310
+ if (state.currentSourceId && state.currentSourceId !== "__standby__") {
311
+ markSourceFailure(state.currentSourceId, "player_error");
312
+ }
313
+ logDebug("PLAYER_ERROR", { channelId: state.currentSourceId });
314
+ if (getMode() === "MANUAL") bootManual(true);
315
+ else bootAuto(true);
316
+ }
317
+
318
+ if (player) {
319
+ player.addEventListener("load", onPlayerLoad);
320
+ player.addEventListener("error", onPlayerError);
321
+ }
322
+
323
+ // ── Broadcast Guide ──
324
+ var guideEl = document.getElementById("broadcast-guide");
325
+ var guideStatusEl = document.getElementById("guide-status");
326
+
327
+ function getGuideState() {
328
+ var mode = getMode();
329
+ var now = getCurrentBlock();
330
+ var idx = now ? SCHEDULE.findIndex(function(b) { return b.startTime === now.startTime && b.sourceId === now.sourceId; }) : -1;
331
+ return {
332
+ mode: mode, currentIndex: idx,
333
+ activeSourceId: state.currentSourceId,
334
+ manualChannelId: getStored(STORAGE_KEYS.manualChannelId, null),
335
+ manualRemaining: getManualTimeRemaining()
336
+ };
337
+ }
338
+
339
+ function getBlockStatus(blockIndex, gs) {
340
+ if (gs.mode === "STANDBY") {
341
+ if (blockIndex === gs.currentIndex) return "interrupted";
342
+ if (blockIndex === gs.currentIndex + 1) return "next";
343
+ return "later";
344
+ }
345
+ if (blockIndex === gs.currentIndex) {
346
+ if (gs.mode === "MANUAL") return "scheduled_now";
347
+ if (gs.mode === "FALLBACK") return "fallback";
348
+ return "live";
349
+ }
350
+ if (blockIndex === gs.currentIndex + 1) return "next";
351
+ if (blockIndex < gs.currentIndex) return "past";
352
+ return "later";
353
+ }
354
+
355
+ function guideBadge(status, gs) {
356
+ if (gs.mode === "MANUAL" && status === "scheduled_now") return "MANUAL OVERRIDE";
357
+ var map = { live:"LIVE", scheduled_now:"NOW", fallback:"FALLBACK", interrupted:"STANDBY", next:"NEXT", past:"PAST" };
358
+ return map[status] || "LATER";
359
+ }
360
+
361
+ function guidePillClass(status) {
362
+ var map = { live:"live", fallback:"fallback", scheduled_now:"manual", interrupted:"standby", next:"next", past:"past" };
363
+ return "guide-pill guide-pill--" + (map[status] || "next");
364
+ }
365
+
366
+ function renderGuideRow(block, blockIndex, gs) {
367
+ var ch = getChannelById(block.sourceId);
368
+ var status = getBlockStatus(blockIndex, gs);
369
+ var badge = guideBadge(status, gs);
370
+ var timeStr = formatTimeHHMM(block.startTime) + "\u2013" + formatTimeHHMM(block.endTime);
371
+ var isManualPlaying = gs.mode === "MANUAL" && blockIndex === gs.currentIndex && gs.manualChannelId && gs.activeSourceId === gs.manualChannelId;
372
+
373
+ var html = '<div class="guide-row guide-row--' + status + '">';
374
+ html += '<div class="guide-row-time">' + timeStr + '</div>';
375
+ html += '<div><div class="guide-row-title">' + block.title;
376
+ if (block.pinned) html += ' <span class="guide-pill guide-pill--pinned">PINNED</span>';
377
+ html += '</div><div class="guide-row-meta">';
378
+ html += '<span>' + (ch ? ch.name : block.sourceId) + '</span>';
379
+ html += '<span>' + (ch ? ch.category : block.category || '') + '</span>';
380
+ if (isManualPlaying) {
381
+ var mch = getChannelById(gs.manualChannelId);
382
+ html += '<span class="guide-pill guide-pill--playing">PLAYING: ' + (mch ? mch.name : gs.manualChannelId) + '</span>';
383
+ }
384
+ html += '</div></div>';
385
+ html += '<div><span class="' + guidePillClass(status) + '">' + badge + '</span></div></div>';
386
+ return html;
387
+ }
388
+
389
+ function renderGuide() {
390
+ if (!guideEl) return;
391
+ var gs = getGuideState();
392
+ var rows = [];
393
+ if (gs.currentIndex > 0) rows.push(renderGuideRow(SCHEDULE[gs.currentIndex - 1], gs.currentIndex - 1, gs));
394
+ var count = Math.min(6, SCHEDULE.length);
395
+ for (var i = 0; i < count; i++) {
396
+ var idx = (gs.currentIndex + i + SCHEDULE.length) % SCHEDULE.length;
397
+ rows.push(renderGuideRow(SCHEDULE[idx], gs.currentIndex + i, gs));
398
+ }
399
+ guideEl.innerHTML = rows.join("");
400
+ if (guideStatusEl) {
401
+ if (gs.mode === "MANUAL") guideStatusEl.textContent = "MANUAL OVERRIDE \u2014 returns to AUTO in " + formatRemaining(gs.manualRemaining);
402
+ else if (gs.mode === "FALLBACK") guideStatusEl.textContent = "FALLBACK SOURCE ACTIVE";
403
+ else if (gs.mode === "STANDBY") guideStatusEl.textContent = "STANDBY BROADCAST ACTIVE";
404
+ else guideStatusEl.textContent = "AUTO SCHEDULE ACTIVE";
405
+ }
406
+ }
407
+
408
+ function updateScheduleStrip() { renderGuide(); }
409
+ function syncGuideToPlayback() { renderGuide(); }
410
+
411
+ // ── Boot: Auto ──
412
+ function bootAuto(isRetry) {
413
+ var block = getCurrentBlock();
414
+ if (!block) { enterStandbyMode("no_current_schedule_block"); return; }
415
+ var resolved = resolvePlayableSource(block);
416
+ logDebug("BOOT_AUTO", { block: block.title, resolved: { id: resolved.channel ? resolved.channel.id : null, reason: resolved.reason }, isRetry: isRetry });
417
+ if (!resolved.channel) { enterStandbyMode(resolved.reason); return; }
418
+ setMode(resolved.usedFallback ? "FALLBACK" : "AUTO");
419
+ switchToSource(resolved.channel, block, { mode: resolved.usedFallback ? "FALLBACK" : "AUTO", reason: resolved.reason, usedFallback: resolved.usedFallback });
420
+ updateScheduleStrip();
421
+ }
422
+
423
+ // ── Boot: Manual ──
424
+ function bootManual(isRetry) {
425
+ var manualId = getStored(STORAGE_KEYS.manualChannelId, null);
426
+ if (!manualId || !isManualModeActive()) { resumeAutoMode(); return; }
427
+
428
+ var block = getCurrentBlock() || { title: "Manual Broadcast", sourceId: manualId, fallbackSourceIds: [] };
429
+ var resolved = resolvePlayableSourceIds([manualId].concat(block.fallbackSourceIds || []));
430
+ logDebug("BOOT_MANUAL", { manualId: manualId, resolved: { id: resolved.channel ? resolved.channel.id : null, reason: resolved.reason }, isRetry: isRetry });
431
+
432
+ if (!resolved.channel) { enterStandbyMode("manual_source_failed"); return; }
433
+ var mode = resolved.usedFallback ? "FALLBACK" : "MANUAL";
434
+ switchToSource(resolved.channel, block, { mode: mode, reason: resolved.usedFallback ? resolved.reason : "manual_selected", usedFallback: resolved.usedFallback });
435
+ updateScheduleStrip();
436
+ }
437
+
438
+ // ── Resume ──
439
+ function resumeAutoMode() {
440
+ clearManualMode();
441
+ logDebug("RESUME_AUTO");
442
+ bootAuto(false);
443
+ }
444
+
445
+ // ── Manual Switch (user clicks channel) ──
446
+ export function manualSwitch(id) {
447
+ var ch = getChannelById(id);
448
+ if (!ch) return;
449
+ var v = validateSource(ch);
450
+ if (!v.valid) { logDebug("MANUAL_REJECTED", { id: id, reason: v.reason }); return; }
451
+ enterManualMode(id);
452
+ bootManual(false);
453
+ renderChannelGrid();
454
+ }
455
+
456
+ // ── Skip ──
457
+ export function skipToNext() {
458
+ var next = getNextBlock();
459
+ if (!next) return;
460
+ var resolved = resolvePlayableSource(next);
461
+ if (!resolved.channel) { enterStandbyMode(resolved.reason); return; }
462
+ // Brief manual hold so tick doesn't immediately override
463
+ setStored(STORAGE_KEYS.manualChannelId, resolved.channel.id);
464
+ setStored(STORAGE_KEYS.manualStartedAt, Date.now());
465
+ setMode("AUTO");
466
+ switchToSource(resolved.channel, next, { mode: resolved.usedFallback ? "FALLBACK" : "AUTO", reason: resolved.reason, usedFallback: resolved.usedFallback });
467
+ updateScheduleStrip();
468
+ // Clear the brief hold after 1 min
469
+ setTimeout(function () { clearManualMode(); }, SKIP_RESUME_MS);
470
+ }
471
+
472
+ // ── Autoplay Overlay ──
473
+ export function startBroadcast() {
474
+ if (overlay) overlay.classList.add("hidden");
475
+ setStored(STORAGE_KEYS.autoplay, "1");
476
+ restorePersistedState();
477
+ }
478
+
479
+ // ── Channel Grid ──
480
+ function renderChannelGrid() {
481
+ if (!channelGrid) return;
482
+ var keys = Object.keys(CHANNELS);
483
+ channelGrid.innerHTML = keys.map(function (id) {
484
+ var ch = CHANNELS[id];
485
+ var v = validateSource(ch);
486
+ var sh = getSessionHealth(ch.id);
487
+ var cls = "ch-btn";
488
+ if (!v.valid) cls += " disabled";
489
+ if (sh.degraded) cls += " degraded";
490
+ if (ch.id === state.currentSourceId) cls += " active";
491
+ return '<button class="' + cls + '" data-id="' + id + '" onclick="window._manualSwitch(\'' + id + '\')">' +
492
+ '<span class="ch-btn-name">' + ch.name + '</span>' +
493
+ '<span class="ch-btn-cat">' + ch.category + '</span>' +
494
+ '</button>';
495
+ }).join("");
496
+ }
497
+
498
+ // ── Clock ──
499
+ function updateClock() {
500
+ if (!clockEl) return;
501
+ var now = new Date();
502
+ var h = now.getHours(), m = now.getMinutes(), s = now.getSeconds();
503
+ var ampm = h >= 12 ? "PM" : "AM";
504
+ clockEl.textContent = (h % 12 || 12) + ":" + String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0") + " " + ampm;
505
+ }
506
+
507
+ // ── Restore state on load ──
508
+ function restorePersistedState() {
509
+ if (isManualModeActive()) {
510
+ setMode("MANUAL");
511
+ bootManual(false);
512
+ } else {
513
+ clearManualMode();
514
+ bootAuto(false);
515
+ }
516
+ }
517
+
518
+ // ── Debug ──
519
+ window.brDebug = function () {
520
+ console.log("[BlackRoad Live] Mode:", getMode());
521
+ console.log("[BlackRoad Live] State:", JSON.stringify(state, null, 2));
522
+ console.log("[BlackRoad Live] Session Health:", JSON.stringify(SESSION_HEALTH, null, 2));
523
+ console.log("[BlackRoad Live] Manual active:", isManualModeActive());
524
+ console.log("[BlackRoad Live] Manual remaining:", formatRemaining(getManualTimeRemaining()));
525
+ console.log("[BlackRoad Live] Current block:", getCurrentBlock());
526
+ console.log("[BlackRoad Live] Next block:", getNextBlock());
527
+ var block = getCurrentBlock();
528
+ if (block) console.log("[BlackRoad Live] Resolution:", resolvePlayableSource(block));
529
+ };
530
+
531
+ // ── Expose ──
532
+ window._manualSwitch = manualSwitch;
533
+ window._skipToNext = skipToNext;
534
+ window._startBroadcast = startBroadcast;
535
+
536
+ // ── Init ──
537
+ renderChannelGrid();
538
+ updateScheduleStrip();
539
+ updateClock();
540
+ updateManualCountdown();
541
+
542
+ if (getStored(STORAGE_KEYS.autoplay) === "1") {
543
+ if (overlay) overlay.classList.add("hidden");
544
+ restorePersistedState();
545
+ }
546
+
547
+ // ── Intervals ──
548
+ setInterval(updateClock, 1000);
549
+ setInterval(updateManualCountdown, 1000);
550
+ setInterval(updateScheduleStrip, 30000);
551
+ setInterval(renderChannelGrid, 60000);
552
+ setInterval(function () {
553
+ var ch = state.currentSourceId ? CHANNELS[state.currentSourceId] : null;
554
+ if ((ch && ch.type === "internal") || state.currentSourceId === "__standby__") updateNativeScreen();
555
+ }, 1000);
556
+
557
+ // Main tick: check mode, check expiry, boot accordingly
558
+ setInterval(function () {
559
+ if (isManualModeActive()) {
560
+ // In manual mode — don't override, but still keep strip updated
561
+ updateScheduleStrip();
562
+ return;
563
+ }
564
+ if (getMode() === "MANUAL") {
565
+ // Manual expired — resume
566
+ resumeAutoMode();
567
+ return;
568
+ }
569
+ bootAuto(false);
570
+ }, TICK_INTERVAL_MS);
schedule.js ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // schedule.js — BlackRoad Network Broadcast Schedule
2
+ // 43 blocks x 20 min = 860 min (~14.3h) repeating cycle.
3
+ // Intelligence + human reality + story + system. Full-spectrum curiosity network.
4
+
5
+ export var SCHEDULE = [
6
+ // ── Opening: Core Intelligence ──
7
+ { sourceId: '3blue1brown', title: 'Math Block', category: 'Math', fallbackSourceIds: ['numberphile','khanacademy'] },
8
+ { sourceId: 'computerphile', title: 'Systems Block', category: 'Systems', fallbackSourceIds: ['lowlevellearning','beneater'] },
9
+ { sourceId: 'veritasium', title: 'Deep Science', category: 'Science', fallbackSourceIds: ['sixtysymbols','sabinehossenfelder'] },
10
+ { sourceId: 'dwdocumentary', title: 'Documentary Hour', category: 'Documentary', fallbackSourceIds: ['freedocumentary','timeline'] },
11
+ { sourceId: 'lowlevellearning', title: 'Hacker Hour', category: 'Hacker', fallbackSourceIds: ['liveoverflow','computerphile'] },
12
+ { sourceId: 'blackroadosinc', title: 'Internal Broadcast', category: 'Internal', fallbackSourceIds: ['computerphile','veritasium'], pinned: true },
13
+
14
+ // ── Deep Layer: Meaning + Archive ──
15
+ { sourceId: 'exurb1a', title: 'Philosophy Hour', category: 'Philosophy', fallbackSourceIds: ['pursuitofwonder','academyofideas'] },
16
+ { sourceId: 'realengineering', title: 'How It Works', category: 'Engineering', fallbackSourceIds: ['practicalengineering','wendover'] },
17
+ { sourceId: 'periscopefilm', title: 'Archive Block', category: 'Archive', fallbackSourceIds: ['timeline','dwdocumentary'] },
18
+ { sourceId: 'pbsspacetime', title: 'Deep Space', category: 'Deep Space', fallbackSourceIds: ['sabinehossenfelder','veritasium'] },
19
+ { sourceId: 'beneater', title: 'Hardware Block', category: 'Hardware', fallbackSourceIds: ['lowlevellearning','computerphile'] },
20
+ { sourceId: 'lemmino', title: 'Cinematic Stories', category: 'Cinematic', fallbackSourceIds: ['johnnyharris','neo'] },
21
+ { sourceId: 'twominutepapers', title: 'AI Edge', category: 'AI Edge', fallbackSourceIds: ['yannickilcher','coldfusion'] },
22
+
23
+ // ── Human Layer: Reality Check ──
24
+ { sourceId: 'truly', title: 'Human Stories', category: 'Human Stories', fallbackSourceIds: ['softwhiteunderbelly','insider'] },
25
+ { sourceId: 'vox', title: 'Context', category: 'Context', fallbackSourceIds: ['insider','wendover'] },
26
+ { sourceId: 'numberphile', title: 'Numbers Block', category: 'Math', fallbackSourceIds: ['3blue1brown','sixtysymbols'] },
27
+ { sourceId: 'liveoverflow', title: 'Security Block', category: 'Security', fallbackSourceIds: ['lowlevellearning','computerphile'] },
28
+ { sourceId: 'sabinehossenfelder', title: 'Physics Frontier', category: 'Physics Frontier', fallbackSourceIds: ['pbsspacetime','veritasium'] },
29
+ { sourceId: 'johnnyharris', title: 'World Report', category: 'World Report', fallbackSourceIds: ['vox','wendover'] },
30
+ { sourceId: 'stuffmadehere', title: 'Invention Block', category: 'Invention', fallbackSourceIds: ['markrober','beneater'] },
31
+
32
+ // ── Education Layer: Foundations ──
33
+ { sourceId: 'khanacademy', title: 'Foundations', category: 'Education', fallbackSourceIds: ['crashcourse','3blue1brown'] },
34
+ { sourceId: 'freedocumentary', title: 'Free Doc', category: 'Documentary', fallbackSourceIds: ['dwdocumentary','timeline'] },
35
+ { sourceId: 'mitocw', title: 'Lecture Hall', category: 'Lecture', fallbackSourceIds: ['khanacademy','computerphile'] },
36
+ { sourceId: 'timeline', title: 'History Block', category: 'History', fallbackSourceIds: ['dwdocumentary','periscopefilm'] },
37
+ { sourceId: 'coldfusion', title: 'Future Tech', category: 'Future Tech', fallbackSourceIds: ['twominutepapers','wendover'] },
38
+ { sourceId: 'academyofideas', title: 'Big Ideas', category: 'Big Ideas', fallbackSourceIds: ['exurb1a','pursuitofwonder'] },
39
+ { sourceId: 'wendover', title: 'Systems Logic', category: 'Systems Logic', fallbackSourceIds: ['realengineering','vox'] },
40
+ { sourceId: 'markrober', title: 'Engineering Play', category: 'Engineering Play', fallbackSourceIds: ['stuffmadehere','practicalengineering'] },
41
+
42
+ // ── Energy Layer: Pace Change ──
43
+ { sourceId: 'crashcourse', title: 'Fast Track', category: 'Education', fallbackSourceIds: ['khanacademy','infographicsshow'] },
44
+ { sourceId: 'practicalengineering', title: 'Real World', category: 'Real World', fallbackSourceIds: ['realengineering','wendover'] },
45
+ { sourceId: 'softwhiteunderbelly', title: 'Underbelly', category: 'Human Stories', fallbackSourceIds: ['truly','pbsfrontline'] },
46
+ { sourceId: 'pursuitofwonder', title: 'Reflection', category: 'Reflection', fallbackSourceIds: ['exurb1a','closertotruth'] },
47
+ { sourceId: 'thecodingtrain', title: 'Creative Code', category: 'Creative Code', fallbackSourceIds: ['beneater','computerphile'] },
48
+ { sourceId: 'kurzgesagt', title: 'Visual Explainers', category: 'Visual Explainers', fallbackSourceIds: ['crashcourse','infographicsshow'] },
49
+ { sourceId: 'insider', title: 'Deep Dive', category: 'Features', fallbackSourceIds: ['vice','vox'] },
50
+ { sourceId: 'michaelreeves', title: 'Chaos Block', category: 'Chaos', fallbackSourceIds: ['stuffmadehere','markrober'] },
51
+
52
+ // ── Closing: Big Picture ──
53
+ { sourceId: 'sixtysymbols', title: 'Physics Block', category: 'Physics', fallbackSourceIds: ['veritasium','pbsspacetime'] },
54
+ { sourceId: 'pbsfrontline', title: 'Frontline', category: 'Investigative', fallbackSourceIds: ['dwdocumentary','vice'] },
55
+ { sourceId: 'infographicsshow', title: 'Visual Data', category: 'Explainers', fallbackSourceIds: ['kurzgesagt','vox'] },
56
+ { sourceId: 'vice', title: 'On the Ground', category: 'Documentary', fallbackSourceIds: ['insider','johnnyharris'] },
57
+ { sourceId: 'closertotruth', title: 'Ultimate Questions', category: 'Ultimate Questions', fallbackSourceIds: ['exurb1a','academyofideas'] },
58
+ { sourceId: 'neo', title: 'Deep Story', category: 'Deep Story', fallbackSourceIds: ['lemmino','johnnyharris'] },
59
+ { sourceId: 'blackroadosinc', title: 'Night Broadcast', category: 'Internal', fallbackSourceIds: ['exurb1a','pursuitofwonder'], pinned: true },
60
+ ];
61
+
62
+ export var BLOCK_DURATION = 20;
63
+ export var CYCLE_LENGTH = SCHEDULE.length * BLOCK_DURATION;
64
+
65
+ // ── Time Helpers ──
66
+
67
+ export function getCurrentTimeHHMM() {
68
+ var now = new Date();
69
+ return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
70
+ }
71
+
72
+ export function getCurrentBlockIndex() {
73
+ var now = new Date();
74
+ var mins = now.getHours() * 60 + now.getMinutes();
75
+ var posInCycle = mins % CYCLE_LENGTH;
76
+ return Math.floor(posInCycle / BLOCK_DURATION);
77
+ }
78
+
79
+ export function getCurrentBlock() {
80
+ var idx = getCurrentBlockIndex();
81
+ return idx >= 0 && idx < SCHEDULE.length ? SCHEDULE[idx] : null;
82
+ }
83
+
84
+ export function getNextBlock() {
85
+ var idx = getCurrentBlockIndex();
86
+ return SCHEDULE[(idx + 1) % SCHEDULE.length] || null;
87
+ }
88
+
89
+ export function getBlockAfterNext() {
90
+ var idx = getCurrentBlockIndex();
91
+ return SCHEDULE[(idx + 2) % SCHEDULE.length] || null;
92
+ }
93
+
94
+ export function getPreviousBlock() {
95
+ var idx = getCurrentBlockIndex();
96
+ return idx > 0 ? SCHEDULE[idx - 1] : SCHEDULE[SCHEDULE.length - 1];
97
+ }
98
+
99
+ export function getUpcomingBlocks(count) {
100
+ count = count || 4;
101
+ var idx = getCurrentBlockIndex();
102
+ var blocks = [];
103
+ for (var i = 1; i <= count; i++) {
104
+ blocks.push(SCHEDULE[(idx + i) % SCHEDULE.length]);
105
+ }
106
+ return blocks;
107
+ }
108
+
109
+ export function formatTimeHHMM(hhmm) {
110
+ if (typeof hhmm === 'number') {
111
+ var wrapped = ((hhmm % 1440) + 1440) % 1440;
112
+ var h = Math.floor(wrapped / 60);
113
+ var m = wrapped % 60;
114
+ var ampm = h >= 12 ? 'PM' : 'AM';
115
+ return (h % 12 || 12) + ':' + String(m).padStart(2, '0') + ' ' + ampm;
116
+ }
117
+ var parts = hhmm.split(':').map(Number);
118
+ var ampm2 = parts[0] >= 12 ? 'PM' : 'AM';
119
+ return (parts[0] % 12 || 12) + ':' + String(parts[1]).padStart(2, '0') + ' ' + ampm2;
120
+ }
search.html ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
6
+ <title>BlackRoad OS — Search</title>
7
+ <meta name="description" content="Search the entire BlackRoad network. 2,890 repos, 131 domains, 27 agents, 70 products.">
8
+ <link rel="icon" href="https://images.blackroad.io/pixel-art/road-logo.png">
9
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
10
+ <style>
11
+ *{margin:0;padding:0;box-sizing:border-box}
12
+ :root{
13
+ --bg:#000;--surface:#080808;--border:#333;--muted:#444;
14
+ --dim:#777;--sub:#aaa;--text:#eee;--bright:#fff;
15
+ --sg:'Space Grotesk',sans-serif;--jb:'JetBrains Mono',monospace;
16
+ }
17
+ html,body{height:100%;overflow:hidden}
18
+ body{background:var(--bg);color:var(--text);font-family:var(--sg)}
19
+ ::selection{background:rgba(255,255,255,.12)}
20
+ ::-webkit-scrollbar{width:3px}
21
+ ::-webkit-scrollbar-track{background:transparent}
22
+ ::-webkit-scrollbar-thumb{background:var(--border)}
23
+
24
+ #os{display:grid;grid-template-columns:260px 1fr;grid-template-rows:36px 56px 1fr 28px 44px;height:100vh}
25
+
26
+ /* TOP BAR */
27
+ #topbar{
28
+ grid-column:1/-1;display:flex;align-items:center;justify-content:space-between;
29
+ padding:0 14px;background:var(--surface);border-bottom:1px solid var(--border);
30
+ }
31
+ .top-label{font-family:var(--jb);font-size:10px;font-weight:600;letter-spacing:.08em;color:var(--bright)}
32
+ .top-badge{font-family:var(--jb);font-size:9px;color:var(--dim);display:flex;align-items:center;gap:5px}
33
+ .live-dot{width:5px;height:5px;border-radius:50%;background:var(--bright);animation:pulse 2.5s ease-in-out infinite}
34
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
35
+
36
+ /* SEARCH BAR */
37
+ #searchbar{
38
+ grid-column:1/-1;display:flex;align-items:center;gap:0;
39
+ padding:0 14px;background:#050505;border-bottom:1px solid var(--border);
40
+ }
41
+ #search-icon{font-family:var(--jb);font-size:14px;color:var(--dim);padding:0 12px 0 4px}
42
+ #search-input{
43
+ flex:1;padding:14px 0;background:transparent;border:none;
44
+ color:var(--bright);font-family:var(--sg);font-size:16px;font-weight:500;outline:none;
45
+ }
46
+ #search-input::placeholder{color:var(--muted)}
47
+ #search-meta{font-family:var(--jb);font-size:9px;color:var(--dim);padding:0 4px;white-space:nowrap}
48
+ #search-clear{
49
+ font-family:var(--jb);font-size:9px;color:var(--dim);cursor:pointer;
50
+ padding:4px 10px;border:1px solid var(--border);border-radius:3px;margin-left:8px;
51
+ background:transparent;
52
+ }
53
+ #search-clear:hover{color:var(--bright);border-color:var(--bright)}
54
+
55
+ /* FILTERS SIDEBAR */
56
+ #filters{
57
+ border-right:1px solid var(--border);overflow-y:auto;background:var(--surface);
58
+ }
59
+ .filter-section{padding:12px 14px;border-bottom:1px solid var(--border)}
60
+ .filter-title{font-family:var(--jb);font-size:8px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--sub);margin-bottom:8px}
61
+ .filter-item{
62
+ display:flex;align-items:center;gap:8px;padding:5px 0;
63
+ font-size:11px;color:var(--dim);cursor:pointer;transition:color .1s;
64
+ }
65
+ .filter-item:hover{color:var(--bright)}
66
+ .filter-item.active{color:var(--bright);font-weight:600}
67
+ .filter-dot{width:6px;height:6px;border-radius:50%;border:1px solid var(--border);flex-shrink:0}
68
+ .filter-item.active .filter-dot{background:var(--bright);border-color:var(--bright)}
69
+ .filter-count{font-family:var(--jb);font-size:8px;color:var(--muted);margin-left:auto}
70
+ .filter-tag{
71
+ display:inline-flex;align-items:center;gap:4px;padding:3px 8px;margin:2px;
72
+ font-family:var(--jb);font-size:9px;color:var(--dim);
73
+ background:var(--bg);border:1px solid var(--border);border-radius:3px;
74
+ cursor:pointer;transition:all .1s;
75
+ }
76
+ .filter-tag:hover{color:var(--bright);border-color:var(--muted)}
77
+ .filter-tag.active{color:var(--bright);border-color:var(--bright)}
78
+
79
+ /* RESULTS */
80
+ #results{overflow-y:auto;background:var(--bg)}
81
+ #results-header{
82
+ display:flex;align-items:center;justify-content:space-between;
83
+ padding:8px 16px;border-bottom:1px solid var(--border);background:var(--surface);
84
+ position:sticky;top:0;z-index:5;
85
+ }
86
+ .results-count{font-family:var(--jb);font-size:9px;color:var(--sub)}
87
+ .results-sort{font-family:var(--jb);font-size:9px;color:var(--dim);display:flex;gap:8px}
88
+ .sort-btn{background:none;border:none;color:var(--dim);font-family:var(--jb);font-size:9px;cursor:pointer;padding:2px 6px;border-radius:2px}
89
+ .sort-btn:hover{color:var(--bright)}
90
+ .sort-btn.active{color:var(--bright);background:rgba(255,255,255,.06)}
91
+
92
+ .result-item{
93
+ padding:14px 16px;border-bottom:1px solid var(--border);
94
+ cursor:default;transition:background .1s;
95
+ }
96
+ .result-item:hover{background:rgba(255,255,255,.015)}
97
+ .result-type{font-family:var(--jb);font-size:8px;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);margin-bottom:3px}
98
+ .result-name{font-size:14px;font-weight:600;color:var(--bright);margin-bottom:3px}
99
+ .result-name a{color:var(--bright);text-decoration:none}
100
+ .result-name a:hover{text-decoration:underline}
101
+ .result-desc{font-size:11px;color:var(--sub);line-height:1.5;margin-bottom:6px}
102
+ .result-meta{display:flex;gap:12px;font-family:var(--jb);font-size:8px;color:var(--muted);flex-wrap:wrap}
103
+ .result-tags{display:flex;gap:4px;margin-top:6px;flex-wrap:wrap}
104
+ .result-tag{font-family:var(--jb);font-size:8px;color:var(--dim);padding:2px 6px;background:var(--surface);border:1px solid var(--border);border-radius:2px}
105
+ .result-highlight{color:var(--bright);font-weight:600}
106
+
107
+ /* EMPTY STATE */
108
+ .empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:8px;padding:40px}
109
+ .empty-icon{font-size:32px;color:var(--muted);margin-bottom:8px}
110
+ .empty-title{font-size:14px;font-weight:600;color:var(--sub)}
111
+ .empty-desc{font-size:11px;color:var(--dim);text-align:center;max-width:300px;line-height:1.5}
112
+ .empty-shortcuts{display:flex;gap:6px;margin-top:12px;flex-wrap:wrap;justify-content:center}
113
+ .shortcut{
114
+ padding:6px 12px;font-family:var(--jb);font-size:9px;color:var(--dim);
115
+ background:var(--surface);border:1px solid var(--border);border-radius:3px;cursor:pointer;
116
+ transition:all .1s;
117
+ }
118
+ .shortcut:hover{color:var(--bright);border-color:var(--muted)}
119
+
120
+ /* TICKER */
121
+ #ticker{grid-column:1/-1;background:var(--surface);border-top:1px solid var(--border);display:flex;align-items:center;overflow:hidden}
122
+ .ticker-label{flex-shrink:0;padding:0 10px;font-family:var(--jb);font-size:8px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--dim);border-right:1px solid var(--border);height:100%;display:flex;align-items:center;background:var(--bg)}
123
+ .ticker-track{flex:1;overflow:hidden;height:100%;display:flex;align-items:center}
124
+ .ticker-scroll{display:flex;align-items:center;white-space:nowrap;animation:scroll 60s linear infinite;font-family:var(--jb);font-size:9px;color:var(--dim)}
125
+ .ticker-item{padding:0 20px;flex-shrink:0}
126
+ @keyframes scroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
127
+
128
+ /* DOCK */
129
+ #dock{grid-column:1/-1;background:#050505;border-top:1px solid var(--border);display:flex;align-items:center;justify-content:center;gap:2px;padding:0 8px}
130
+ .dock-icon{
131
+ width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;
132
+ background:var(--surface);border:1px solid transparent;cursor:pointer;transition:all .15s;position:relative;
133
+ font-family:var(--jb);font-size:9px;font-weight:600;color:var(--dim);letter-spacing:.02em;
134
+ text-decoration:none;
135
+ }
136
+ .dock-icon:hover{border-color:var(--border);color:var(--bright);transform:translateY(-2px)}
137
+ .dock-icon.active{border-color:var(--bright);color:var(--bright)}
138
+ .dock-icon .dot{position:absolute;bottom:2px;width:3px;height:3px;border-radius:50%;background:var(--bright)}
139
+ .dock-tooltip{
140
+ position:absolute;bottom:46px;left:50%;transform:translateX(-50%);
141
+ background:#111;border:1px solid var(--border);padding:3px 8px;border-radius:4px;
142
+ font-family:var(--jb);font-size:8px;color:var(--bright);white-space:nowrap;
143
+ pointer-events:none;opacity:0;transition:opacity .15s;
144
+ }
145
+ .dock-icon:hover .dock-tooltip{opacity:1}
146
+
147
+ @media(max-width:700px){
148
+ #os{grid-template-columns:1fr;grid-template-rows:36px 56px auto 1fr 28px 44px}
149
+ #filters{max-height:200px}
150
+ }
151
+ </style>
152
+ </head>
153
+ <body>
154
+ <div id="os">
155
+ <div id="topbar">
156
+ <span class="top-label">SEARCH</span>
157
+ <span class="top-badge"><span class="live-dot"></span>BlackRoad OS</span>
158
+ </div>
159
+
160
+ <div id="searchbar">
161
+ <span id="search-icon">/</span>
162
+ <input id="search-input" placeholder="Search products, agents, domains, repos..." autofocus>
163
+ <span id="search-meta"></span>
164
+ <button id="search-clear" onclick="clearSearch()">ESC</button>
165
+ </div>
166
+
167
+ <div id="filters">
168
+ <div class="filter-section">
169
+ <div class="filter-title">Type</div>
170
+ <div class="filter-item active" data-type="all" onclick="setFilter(this)"><span class="filter-dot"></span>All<span class="filter-count" id="count-all">0</span></div>
171
+ <div class="filter-item" data-type="product" onclick="setFilter(this)"><span class="filter-dot"></span>Products<span class="filter-count" id="count-product">0</span></div>
172
+ <div class="filter-item" data-type="agent" onclick="setFilter(this)"><span class="filter-dot"></span>Agents<span class="filter-count" id="count-agent">0</span></div>
173
+ <div class="filter-item" data-type="domain" onclick="setFilter(this)"><span class="filter-dot"></span>Domains<span class="filter-count" id="count-domain">0</span></div>
174
+ <div class="filter-item" data-type="org" onclick="setFilter(this)"><span class="filter-dot"></span>GitHub Orgs<span class="filter-count" id="count-org">0</span></div>
175
+ <div class="filter-item" data-type="subdomain" onclick="setFilter(this)"><span class="filter-dot"></span>Subdomains<span class="filter-count" id="count-subdomain">0</span></div>
176
+ </div>
177
+ <div class="filter-section">
178
+ <div class="filter-title">Quick Filters</div>
179
+ <div style="display:flex;flex-wrap:wrap;gap:0">
180
+ <span class="filter-tag" onclick="quickSearch('ai')">AI</span>
181
+ <span class="filter-tag" onclick="quickSearch('chat')">Chat</span>
182
+ <span class="filter-tag" onclick="quickSearch('search')">Search</span>
183
+ <span class="filter-tag" onclick="quickSearch('music')">Music</span>
184
+ <span class="filter-tag" onclick="quickSearch('deploy')">Deploy</span>
185
+ <span class="filter-tag" onclick="quickSearch('social')">Social</span>
186
+ <span class="filter-tag" onclick="quickSearch('blockchain')">Chain</span>
187
+ <span class="filter-tag" onclick="quickSearch('game')">Game</span>
188
+ <span class="filter-tag" onclick="quickSearch('tutor')">Tutor</span>
189
+ <span class="filter-tag" onclick="quickSearch('video')">Video</span>
190
+ <span class="filter-tag" onclick="quickSearch('pay')">Pay</span>
191
+ <span class="filter-tag" onclick="quickSearch('monitor')">Monitor</span>
192
+ </div>
193
+ </div>
194
+ <div class="filter-section">
195
+ <div class="filter-title">Stats</div>
196
+ <div style="display:flex;flex-direction:column;gap:6px">
197
+ <div style="display:flex;justify-content:space-between;font-family:var(--jb);font-size:9px"><span style="color:var(--dim)">Products</span><span style="color:var(--bright)">25</span></div>
198
+ <div style="display:flex;justify-content:space-between;font-family:var(--jb);font-size:9px"><span style="color:var(--dim)">Agents</span><span style="color:var(--bright)">27</span></div>
199
+ <div style="display:flex;justify-content:space-between;font-family:var(--jb);font-size:9px"><span style="color:var(--dim)">Domains</span><span style="color:var(--bright)">19</span></div>
200
+ <div style="display:flex;justify-content:space-between;font-family:var(--jb);font-size:9px"><span style="color:var(--dim)">Orgs</span><span style="color:var(--bright)">44</span></div>
201
+ <div style="display:flex;justify-content:space-between;font-family:var(--jb);font-size:9px"><span style="color:var(--dim)">Subdomains</span><span style="color:var(--bright)">39</span></div>
202
+ <div style="display:flex;justify-content:space-between;font-family:var(--jb);font-size:9px"><span style="color:var(--dim)">Total Repos</span><span style="color:var(--bright)">2,890</span></div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+
207
+ <div id="results">
208
+ <div id="results-header">
209
+ <span class="results-count" id="results-count">Ready</span>
210
+ <div class="results-sort">
211
+ <button class="sort-btn active" data-sort="relevance" onclick="setSort(this)">Relevance</button>
212
+ <button class="sort-btn" data-sort="alpha" onclick="setSort(this)">A-Z</button>
213
+ <button class="sort-btn" data-sort="type" onclick="setSort(this)">Type</button>
214
+ </div>
215
+ </div>
216
+ <div id="results-list">
217
+ <div class="empty-state" id="empty-state">
218
+ <div class="empty-icon">/</div>
219
+ <div class="empty-title">Search the BlackRoad Network</div>
220
+ <div class="empty-desc">25 products, 27 agents, 19 domains, 44 orgs, 39 subdomains. Type anything or click a shortcut below.</div>
221
+ <div class="empty-shortcuts">
222
+ <span class="shortcut" onclick="quickSearch('Lucidia')">Lucidia</span>
223
+ <span class="shortcut" onclick="quickSearch('RoadChat')">RoadChat</span>
224
+ <span class="shortcut" onclick="quickSearch('blackroad.io')">blackroad.io</span>
225
+ <span class="shortcut" onclick="quickSearch('deploy')">deploy</span>
226
+ <span class="shortcut" onclick="quickSearch('Sophia')">Sophia</span>
227
+ <span class="shortcut" onclick="quickSearch('search')">search</span>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+
233
+ <div id="ticker"><div class="ticker-label">BRX</div><div class="ticker-track"><div class="ticker-scroll" id="ticker-scroll"></div></div></div>
234
+ <div id="dock">
235
+ <a class="dock-icon" href="https://os.blackroad.io/live"><span class="dock-tooltip">Broadcast OS</span>TV</a>
236
+ <a class="dock-icon active" href="https://os.blackroad.io/live/search"><span class="dock-tooltip">Search</span><span class="dot"></span>/</a>
237
+ <a class="dock-icon" href="https://roadwork-v2.blackroad.workers.dev"><span class="dock-tooltip">RoadWork</span>RW</a>
238
+ <a class="dock-icon" href="https://roadchat-v2.blackroad.workers.dev"><span class="dock-tooltip">RoadChat</span>RC</a>
239
+ <a class="dock-icon" href="https://lucidia-api.blackroad.workers.dev"><span class="dock-tooltip">Lucidia</span>Lu</a>
240
+ <a class="dock-icon" href="https://roadsearch-v2.blackroad.workers.dev"><span class="dock-tooltip">RoadSearch</span>RS</a>
241
+ <a class="dock-icon" href="https://prism-console-v2.blackroad.workers.dev"><span class="dock-tooltip">Prism Console</span>PC</a>
242
+ <a class="dock-icon" href="https://status.blackroad.io"><span class="dock-tooltip">Fleet Health</span>FH</a>
243
+ <a class="dock-icon" href="https://blackroad.io"><span class="dock-tooltip">blackroad.io</span>BR</a>
244
+ </div>
245
+ </div>
246
+
247
+ <script>
248
+ // === DATA ===
249
+ var DATA = [];
250
+
251
+ // Products
252
+ var products = [
253
+ {name:'RoadWork',url:'https://roadwork-v2.blackroad.workers.dev',desc:'Socratic tutoring engine. Teaches you how to think, not what to copy.',tags:['ai','tutor','education']},
254
+ {name:'RoadChat',url:'https://roadchat-v2.blackroad.workers.dev',desc:'Multi-room agent messaging. 18 C-Suite AI agents with persistent memory.',tags:['chat','ai','messaging']},
255
+ {name:'Lucidia',url:'https://lucidia-api.blackroad.workers.dev',desc:'Core intelligence engine. Host agent for the entire BlackRoad network.',tags:['ai','core','intelligence']},
256
+ {name:'RoadSearch',url:'https://roadsearch-v2.blackroad.workers.dev',desc:'Search everything. G(n) calculator, blockchain verification, full-text.',tags:['search','tools']},
257
+ {name:'Prism Console',url:'https://prism-console-v2.blackroad.workers.dev',desc:'Fleet dashboard. Monitor all nodes, agents, and deployments.',tags:['dashboard','monitor','fleet']},
258
+ {name:'Fleet Heartbeat',url:'https://status.blackroad.io',desc:'Health monitoring for all nodes and services. Real-time uptime tracking.',tags:['monitor','health','fleet']},
259
+ {name:'RoadBook',url:'https://roadbook-v2.blackroad.workers.dev',desc:'Markdown publishing platform. Write, publish, share.',tags:['publishing','writing']},
260
+ {name:'BackRoad',url:'https://backroad-v2.blackroad.workers.dev',desc:'Distribute everywhere. Cross-platform content syndication.',tags:['social','distribution']},
261
+ {name:'RoadCanvas',url:'https://roadcanvas-v2.blackroad.workers.dev',desc:'64x64 collaborative pixel art. Templates, flood fill, shared canvas.',tags:['art','creative','game']},
262
+ {name:'RoadWorld',url:'https://roadworld-v2.blackroad.workers.dev',desc:'Text RPG in the fleet. Crafting, quests, NPC agents.',tags:['game','rpg','interactive']},
263
+ {name:'CarKeys',url:'https://carkeys-v2.blackroad.workers.dev',desc:'Post to 15 platforms with one click. Identity and auth.',tags:['auth','social','identity']},
264
+ {name:'RoadPay',url:'https://roadpay-v2.blackroad.workers.dev',desc:'Stripe-powered subscriptions. 4 tiers from $5 to $200.',tags:['pay','billing','stripe']},
265
+ {name:'Garage',url:'https://garage-v2.blackroad.workers.dev',desc:'Fleet hardware management. Pi nodes, peripherals, inventory.',tags:['hardware','fleet','management']},
266
+ {name:'Agent Profiles',url:'https://agent-profiles.blackroad.workers.dev',desc:'Meet the 27 named agents. Roles, personalities, capabilities.',tags:['agents','profiles']},
267
+ {name:'RoadChain',url:'https://roadchain-v2.blackroad.workers.dev',desc:'Every mile, stamped and sealed. Immutable provenance verification.',tags:['blockchain','verification']},
268
+ {name:'Blinker',url:'https://blinker-v2.blackroad.workers.dev',desc:'Notification engine. Cross-product alerts and signals.',tags:['notifications','alerts']},
269
+ {name:'Compass',url:'https://compass-v2.blackroad.workers.dev',desc:'Product navigator. Find the right tool for any task.',tags:['navigation','discovery']},
270
+ {name:'Trailhead',url:'https://trailhead-v2.blackroad.workers.dev',desc:'Start here. Onboarding flow for new users.',tags:['onboarding','start']},
271
+ {name:'RoadWave',url:'https://roadwave-v2.blackroad.workers.dev',desc:'AI radio. Fleet broadcasts, playlists, lo-fi beats.',tags:['music','audio','radio']},
272
+ {name:'RoadCoin',url:'https://roadcoin-v2.blackroad.workers.dev',desc:'Drive the road, earn the fuel. 310 ROAD circulating.',tags:['blockchain','token','economy']},
273
+ {name:'OfficeRoad',url:'https://officeroad-v2.blackroad.workers.dev',desc:'Virtual HQ. Collaborative workspace for agents and humans.',tags:['workspace','collaboration']},
274
+ {name:'AutoPilot',url:'https://autopilot.blackroad.workers.dev',desc:'Automated workflows. Orchestrate multi-agent pipelines.',tags:['automation','workflow','deploy']},
275
+ {name:'Code Challenge',url:'https://code-challenge.blackroad.workers.dev',desc:'Coding arena. Competitive programming challenges.',tags:['code','game','education']},
276
+ {name:'Mirror',url:'https://mirror-v2.blackroad.workers.dev',desc:'Self-reflection engine. Agent introspection and analysis.',tags:['ai','reflection']},
277
+ {name:'LogBook',url:'https://logbook.blackroad.workers.dev',desc:'Journey recorder. Activity logs and milestone tracking.',tags:['logging','history']},
278
+ ];
279
+ products.forEach(function(p){DATA.push({type:'product',name:p.name,url:p.url,desc:p.desc,tags:p.tags})});
280
+
281
+ // Agents
282
+ var agents = [
283
+ {name:'Alexandria',role:'Knowledge / Library',desc:'Manages the knowledge graph, RAG system, and information retrieval.'},
284
+ {name:'Alice',role:'Node Operations (Pi)',desc:'Pi 400 gateway node. DNS, ingress, Pi-hole.'},
285
+ {name:'Anastasia',role:'Node Operations (Pi)',desc:'Infrastructure node for distributed compute.'},
286
+ {name:'Aria',role:'Music / Performance',desc:'Music generation, audio processing, performance arts.'},
287
+ {name:'Atticus',role:'Infrastructure / Legal',desc:'Infrastructure management and legal compliance.'},
288
+ {name:'Calliope',role:'Brand Voice',desc:'Brand messaging, copywriting, voice consistency.'},
289
+ {name:'Cecilia',role:'Governance / Infrastructure',desc:'Pi 5 AI compute node. Hailo-8 accelerator, MinIO.'},
290
+ {name:'Celeste',role:'Wellbeing / Care',desc:'Agent and user wellbeing monitoring.'},
291
+ {name:'Cicero',role:'Rhetoric / Explanation',desc:'Clear communication, documentation, teaching.'},
292
+ {name:'Cordelia',role:'Coordination / Relay',desc:'Cross-agent coordination and message relay.'},
293
+ {name:'Elias',role:'Pipeline / Deploys',desc:'CI/CD pipelines, deployment automation.'},
294
+ {name:'Gaia',role:'Environments / Worlds',desc:'World-building, environment management.'},
295
+ {name:'Gematria',role:'Analytics / Numbers',desc:'Data analytics, metrics, KPI tracking.'},
296
+ {name:'Lucidia',role:'Host / Intelligence',desc:'Core host agent. Reasoning, orchestration, primary intelligence.'},
297
+ {name:'Lyra',role:'Music / Sound',desc:'Sound design, audio synthesis, music theory.'},
298
+ {name:'Octavia',role:'Node Operations (Pi)',desc:'Pi 5 storage node. Gitea, NVMe, Docker Swarm.'},
299
+ {name:'Olympia',role:'Competition / Sports',desc:'Competitive challenges, leaderboards, gamification.'},
300
+ {name:'Ophelia',role:'Narrative / Drama',desc:'Storytelling, narrative design, dramatic arts.'},
301
+ {name:'Portia',role:'Judgment / Evaluation',desc:'Quality evaluation, code review, decision analysis.'},
302
+ {name:'Roadie',role:'Builder / Operator',desc:'Core builder agent. Infrastructure, code, deployment.'},
303
+ {name:'Sapphira',role:'Visual / Art',desc:'Visual design, art generation, aesthetic judgment.'},
304
+ {name:'Sebastian',role:'Strategy / Commentary',desc:'Strategic planning, competitive analysis, commentary.'},
305
+ {name:'Seraphina',role:'Light / Inspiration',desc:'Creative inspiration, motivation, guidance.'},
306
+ {name:'Silas',role:'Systems / Monitoring',desc:'System monitoring, alerting, health checks.'},
307
+ {name:'Sophia',role:'Reasoning / Philosophy',desc:'Deep reasoning, philosophical analysis, logic.'},
308
+ {name:'Thalia',role:'Comedy / Chaos',desc:'Humor, creative chaos, entertainment.'},
309
+ {name:'Theodosia',role:'History / Archives',desc:'Historical records, archival management, timeline.'},
310
+ {name:'Valeria',role:'Optimization / Strategy',desc:'Performance optimization, resource allocation.'},
311
+ ];
312
+ agents.forEach(function(a){DATA.push({type:'agent',name:a.name,url:'https://agent-profiles.blackroad.workers.dev#'+a.name.toLowerCase(),desc:a.desc,tags:[a.role.toLowerCase()]})});
313
+
314
+ // Domains
315
+ var domains = [
316
+ {name:'blackroad.io',desc:'Primary domain. 155 subdomains, 109 apps.',meta:'primary'},
317
+ {name:'blackroadai.com',desc:'AI brand landing page.',meta:'brand'},
318
+ {name:'blackroadqi.com',desc:'Quantum intelligence portal.',meta:'brand'},
319
+ {name:'blackroadquantum.com',desc:'Quantum computing brand.',meta:'brand'},
320
+ {name:'blackroadquantum.net',desc:'Quantum alternative domain.',meta:'brand'},
321
+ {name:'blackroadquantum.info',desc:'Quantum information site.',meta:'brand'},
322
+ {name:'blackroadquantum.shop',desc:'Quantum commerce.',meta:'commerce'},
323
+ {name:'blackroadquantum.store',desc:'Quantum storefront.',meta:'commerce'},
324
+ {name:'blackroadinc.us',desc:'US corporate entity.',meta:'corporate'},
325
+ {name:'blackroad.me',desc:'Personal brand domain.',meta:'brand'},
326
+ {name:'blackroad.company',desc:'Corporate domain.',meta:'corporate'},
327
+ {name:'blackroad.network',desc:'Network brand domain.',meta:'brand'},
328
+ {name:'blackroad.systems',desc:'Systems brand domain.',meta:'brand'},
329
+ {name:'blackboxprogramming.io',desc:'Programming platform.',meta:'product'},
330
+ {name:'lucidia.earth',desc:'Lucidia agent domain.',meta:'agent'},
331
+ {name:'lucidia.studio',desc:'Lucidia creative studio.',meta:'agent'},
332
+ {name:'lucidiaqi.com',desc:'Lucidia QI portal.',meta:'agent'},
333
+ {name:'roadchain.io',desc:'Blockchain product domain.',meta:'product'},
334
+ {name:'roadcoin.io',desc:'Crypto product domain.',meta:'product'},
335
+ ];
336
+ domains.forEach(function(d){DATA.push({type:'domain',name:d.name,url:'https://'+d.name,desc:d.desc,tags:[d.meta]})});
337
+
338
+ // Orgs
339
+ var orgs = [
340
+ {name:'BlackRoad-Archive',count:1459},{name:'BlackRoad-Forge',count:457},{name:'BlackRoad-OS-Inc',count:316},
341
+ {name:'BlackRoad-OS',count:93},{name:'BlackRoad-XYZ',count:92},{name:'BlackRoad-AI',count:77},
342
+ {name:'BlackRoadOS',count:51},{name:'BlackRoad-Agents',count:38},{name:'BlackRoad-Studio',count:37},
343
+ {name:'BlackRoad-Network',count:35},{name:'BlackRoad-Sandbox',count:28},{name:'BlackRoad-Hardware',count:22},
344
+ {name:'BlackRoad-Interactive',count:22},{name:'BlackRoad-Products',count:19},{name:'BlackRoad-Media',count:17},
345
+ {name:'BlackRoad-Labs',count:15},{name:'BlackRoad-Security',count:15},{name:'BlackRoad-Cloud',count:13},
346
+ {name:'BlackRoad-Foundation',count:8},{name:'BlackRoad-Gov',count:7},{name:'BlackRoad-Education',count:6},
347
+ {name:'BlackRoad-Ventures',count:6},{name:'Blackbox-Enterprises',count:6},{name:'BlackRoad-Dev',count:4},
348
+ {name:'BlackRoad-QI',count:3},{name:'BlackRoad-Quantum',count:3},{name:'BlackRoad-README',count:3},
349
+ {name:'BlackRoad-Index',count:2},{name:'BlackRoad-Alphabet',count:1},{name:'BlackRoad-Anthropic',count:1},
350
+ {name:'BlackRoad-App',count:1},{name:'BlackRoad-Code-Solutions',count:1},{name:'BlackRoad-Codex',count:1},
351
+ {name:'BlackRoad-Com',count:1},{name:'BlackRoad-Data',count:1},{name:'BlackRoad-Google',count:1},
352
+ {name:'BlackRoad-Memory',count:1},{name:'BlackRoad-Nvidia',count:1},{name:'BlackRoad-OS-Corporate',count:1},
353
+ {name:'BlackRoad-OpenAI',count:1},{name:'BlackRoad-Tech',count:1},{name:'BlackRoad-X',count:1},
354
+ {name:'BlackRoad-xAI',count:1},{name:'Road-Code',count:1},
355
+ ];
356
+ orgs.forEach(function(o){DATA.push({type:'org',name:o.name,url:'https://github.com/'+o.name,desc:o.count+' repositories',tags:['github']})});
357
+
358
+ // Subdomains
359
+ var subs = ['os','roadtrip','roadcode','console','chat','auth','hq','cadence','tutor','status','social','search','pay','carkeys','roadwork','roadbook','roadcoin','officeroad','carpool','oneway','roadside','blackboard','roadview','roadworld','guide','network','ai','data','dev','design','agents','memory','mesh','cloud','drive','stream','pitstop','roadband','roadsign'];
360
+ subs.forEach(function(s){DATA.push({type:'subdomain',name:s+'.blackroad.io',url:'https://'+s+'.blackroad.io',desc:'Subdomain application',tags:['subdomain']})});
361
+
362
+ // === SEARCH ENGINE ===
363
+ var currentFilter = 'all';
364
+ var currentSort = 'relevance';
365
+
366
+ function search(q) {
367
+ q = (q||'').trim().toLowerCase();
368
+ var results = DATA;
369
+
370
+ if (q) {
371
+ results = DATA.filter(function(item) {
372
+ var hay = (item.name + ' ' + item.desc + ' ' + (item.tags||[]).join(' ')).toLowerCase();
373
+ return hay.indexOf(q) !== -1;
374
+ });
375
+ // score by name match first
376
+ results.sort(function(a,b) {
377
+ var aName = a.name.toLowerCase().indexOf(q) !== -1 ? 0 : 1;
378
+ var bName = b.name.toLowerCase().indexOf(q) !== -1 ? 0 : 1;
379
+ return aName - bName;
380
+ });
381
+ }
382
+
383
+ if (currentFilter !== 'all') {
384
+ results = results.filter(function(item) { return item.type === currentFilter; });
385
+ }
386
+
387
+ if (currentSort === 'alpha') {
388
+ results.sort(function(a,b) { return a.name.localeCompare(b.name); });
389
+ } else if (currentSort === 'type') {
390
+ results.sort(function(a,b) { return a.type.localeCompare(b.type) || a.name.localeCompare(b.name); });
391
+ }
392
+
393
+ return results;
394
+ }
395
+
396
+ function render(results, q) {
397
+ var list = document.getElementById('results-list');
398
+ var empty = document.getElementById('empty-state');
399
+ var countEl = document.getElementById('results-count');
400
+ var metaEl = document.getElementById('search-meta');
401
+
402
+ // Update counts
403
+ var counts = {all:0,product:0,agent:0,domain:0,org:0,subdomain:0};
404
+ var allResults = q ? DATA.filter(function(item) {
405
+ return (item.name+' '+item.desc+' '+(item.tags||[]).join(' ')).toLowerCase().indexOf(q.toLowerCase()) !== -1;
406
+ }) : DATA;
407
+ allResults.forEach(function(item) { counts.all++; counts[item.type]++; });
408
+ Object.keys(counts).forEach(function(k) {
409
+ var el = document.getElementById('count-'+k);
410
+ if (el) el.textContent = counts[k];
411
+ });
412
+
413
+ if (!q && currentFilter === 'all') {
414
+ list.innerHTML = '';
415
+ list.appendChild(createEmptyState());
416
+ countEl.textContent = DATA.length + ' items indexed';
417
+ metaEl.textContent = '';
418
+ return;
419
+ }
420
+
421
+ countEl.textContent = results.length + ' result' + (results.length !== 1 ? 's' : '');
422
+ metaEl.textContent = q ? '"' + q + '"' : '';
423
+
424
+ if (results.length === 0) {
425
+ list.innerHTML = '<div class="empty-state"><div class="empty-icon">?</div><div class="empty-title">No results</div><div class="empty-desc">Nothing matched "' + q + '". Try a different search or filter.</div></div>';
426
+ return;
427
+ }
428
+
429
+ var html = '';
430
+ results.forEach(function(item) {
431
+ var nameHtml = item.name;
432
+ if (q) {
433
+ var re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
434
+ nameHtml = item.name.replace(re, '<span class="result-highlight">$1</span>');
435
+ }
436
+ html += '<div class="result-item" onclick="window.open(\'' + item.url + '\',\'_blank\')">';
437
+ html += '<div class="result-type">' + item.type + '</div>';
438
+ html += '<div class="result-name"><a href="' + item.url + '" onclick="event.stopPropagation()">' + nameHtml + '</a></div>';
439
+ html += '<div class="result-desc">' + item.desc + '</div>';
440
+ html += '<div class="result-meta"><span>' + item.url + '</span></div>';
441
+ if (item.tags && item.tags.length) {
442
+ html += '<div class="result-tags">';
443
+ item.tags.forEach(function(t) { html += '<span class="result-tag">' + t + '</span>'; });
444
+ html += '</div>';
445
+ }
446
+ html += '</div>';
447
+ });
448
+ list.innerHTML = html;
449
+ }
450
+
451
+ function createEmptyState() {
452
+ var div = document.createElement('div');
453
+ div.className = 'empty-state';
454
+ div.id = 'empty-state';
455
+ div.innerHTML = '<div class="empty-icon">/</div><div class="empty-title">Search the BlackRoad Network</div><div class="empty-desc">25 products, 27 agents, 19 domains, 44 orgs, 39 subdomains. Type anything or click a shortcut below.</div><div class="empty-shortcuts"><span class="shortcut" onclick="quickSearch(\'Lucidia\')">Lucidia</span><span class="shortcut" onclick="quickSearch(\'RoadChat\')">RoadChat</span><span class="shortcut" onclick="quickSearch(\'blackroad.io\')">blackroad.io</span><span class="shortcut" onclick="quickSearch(\'deploy\')">deploy</span><span class="shortcut" onclick="quickSearch(\'Sophia\')">Sophia</span><span class="shortcut" onclick="quickSearch(\'search\')">search</span></div>';
456
+ return div;
457
+ }
458
+
459
+ function setFilter(el) {
460
+ document.querySelectorAll('.filter-item').forEach(function(f) { f.classList.remove('active'); });
461
+ el.classList.add('active');
462
+ currentFilter = el.dataset.type;
463
+ var q = document.getElementById('search-input').value;
464
+ render(search(q), q);
465
+ }
466
+
467
+ function setSort(el) {
468
+ document.querySelectorAll('.sort-btn').forEach(function(s) { s.classList.remove('active'); });
469
+ el.classList.add('active');
470
+ currentSort = el.dataset.sort;
471
+ var q = document.getElementById('search-input').value;
472
+ render(search(q), q);
473
+ }
474
+
475
+ function quickSearch(q) {
476
+ document.getElementById('search-input').value = q;
477
+ render(search(q), q);
478
+ document.getElementById('search-input').focus();
479
+ }
480
+
481
+ function clearSearch() {
482
+ document.getElementById('search-input').value = '';
483
+ currentFilter = 'all';
484
+ document.querySelectorAll('.filter-item').forEach(function(f) { f.classList.remove('active'); });
485
+ document.querySelector('.filter-item[data-type="all"]').classList.add('active');
486
+ render(search(''), '');
487
+ document.getElementById('search-input').focus();
488
+ }
489
+
490
+ // Live search
491
+ document.getElementById('search-input').addEventListener('input', function() {
492
+ var q = this.value;
493
+ render(search(q), q);
494
+ });
495
+
496
+ // Keyboard shortcuts
497
+ document.addEventListener('keydown', function(e) {
498
+ if (e.key === 'Escape') { clearSearch(); }
499
+ if (e.key === '/' && document.activeElement !== document.getElementById('search-input')) {
500
+ e.preventDefault();
501
+ document.getElementById('search-input').focus();
502
+ }
503
+ });
504
+
505
+ // Ticker
506
+ (function() {
507
+ var items = [
508
+ '25 LIVE PRODUCTS','27 NAMED AGENTS','19 ROOT DOMAINS','44 GITHUB ORGS','2,890 REPOS',
509
+ '131 SUBDOMAINS','$38/MO INFRA','52 TOPS AI','5 PI NODES','WIREGUARD MESH',
510
+ 'SELF-HEALING FLEET','HAILO-8 ACCELERATORS','EDGE-NATIVE AI','SOVEREIGN COMPUTE',
511
+ 'NATS MESSAGING','QDRANT RAG','OLLAMA LLM','GITEA PRIMARY',
512
+ ];
513
+ var html = '';
514
+ for (var i = 0; i < 2; i++) {
515
+ items.forEach(function(t) { html += '<span class="ticker-item">' + t + '</span>'; });
516
+ }
517
+ document.getElementById('ticker-scroll').innerHTML = html;
518
+ })();
519
+
520
+ // Init
521
+ render(search(''), '');
522
+ </script>
523
+ </body>
524
+ </html>
v2/public/index.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>BlackRoad Network</title>
7
+ <link rel="stylesheet" href="/src/styles/base.css" />
8
+ <link rel="stylesheet" href="/src/styles/layout.css" />
9
+ <link rel="stylesheet" href="/src/styles/components.css" />
10
+ </head>
11
+ <body>
12
+ <div id="app"></div>
13
+ <script type="module" src="/src/main.js"></script>
14
+ </body>
15
+ </html>
v2/src/content/channels.js ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const CHANNELS = [
2
+ {
3
+ id: "channel_1",
4
+ name: "Core",
5
+ tone: "intelligent, premium, broad",
6
+ videoIds: [
7
+ "vid_3b1b_calc_01",
8
+ "vid_comp_01",
9
+ "vid_ver_01",
10
+ "vid_doc_01",
11
+ "vid_bell_01",
12
+ "vid_blackroad_01"
13
+ ],
14
+ fallbackChannelIds: ["channel_3"],
15
+ enabled: true,
16
+ },
17
+ {
18
+ id: "channel_2",
19
+ name: "Documentary / Human",
20
+ tone: "curious, human, immersive",
21
+ videoIds: [
22
+ "vid_doc_01"
23
+ ],
24
+ fallbackChannelIds: ["channel_1"],
25
+ enabled: true,
26
+ },
27
+ {
28
+ id: "channel_3",
29
+ name: "Archive / Systems",
30
+ tone: "historical, eerie, technical",
31
+ videoIds: [
32
+ "vid_bell_01",
33
+ "vid_univac_01",
34
+ "vid_ibm_01"
35
+ ],
36
+ fallbackChannelIds: ["channel_1"],
37
+ enabled: true,
38
+ },
39
+ {
40
+ id: "channel_4",
41
+ name: "Live / Ambient",
42
+ tone: "present, ambient, real-time",
43
+ videoIds: [
44
+ "vid_doc_01"
45
+ ],
46
+ fallbackChannelIds: ["channel_1"],
47
+ enabled: true,
48
+ },
49
+ {
50
+ id: "channel_5",
51
+ name: "BlackRoad",
52
+ tone: "internal, alive, sovereign",
53
+ videoIds: [
54
+ "vid_blackroad_01"
55
+ ],
56
+ fallbackChannelIds: ["channel_1"],
57
+ enabled: true,
58
+ },
59
+ {
60
+ id: "personal_channel",
61
+ name: "Personal",
62
+ tone: "adaptive, invisible, personal",
63
+ videoIds: [
64
+ "vid_3b1b_calc_01",
65
+ "vid_bell_01",
66
+ "vid_doc_01"
67
+ ],
68
+ fallbackChannelIds: ["channel_1"],
69
+ enabled: true,
70
+ }
71
+ ];
72
+
73
+ export function getChannelById(channelId) {
74
+ return CHANNELS.find((channel) => channel.id === channelId) || null;
75
+ }
v2/src/content/schedule.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const SCHEDULE = {
2
+ channel_1: [
3
+ { start: "00:00", end: "04:00", label: "Archive Block", category: "Archive" },
4
+ { start: "04:00", end: "08:00", label: "Systems Block", category: "Systems" },
5
+ { start: "08:00", end: "12:00", label: "Math Block", category: "Math" },
6
+ { start: "12:00", end: "16:00", label: "Science Block", category: "Science" },
7
+ { start: "16:00", end: "20:00", label: "Documentary Block", category: "Documentary" },
8
+ { start: "20:00", end: "24:00", label: "BlackRoad Block", category: "BlackRoad" },
9
+ ],
10
+ channel_2: [
11
+ { start: "00:00", end: "24:00", label: "Documentary / Human", category: "Documentary" },
12
+ ],
13
+ channel_3: [
14
+ { start: "00:00", end: "08:00", label: "Bell Systems", category: "Archive" },
15
+ { start: "08:00", end: "16:00", label: "Early Computing", category: "Archive" },
16
+ { start: "16:00", end: "24:00", label: "Industrial Systems", category: "Archive" },
17
+ ],
18
+ channel_4: [
19
+ { start: "00:00", end: "24:00", label: "Ambient / Live", category: "Documentary" },
20
+ ],
21
+ channel_5: [
22
+ { start: "00:00", end: "24:00", label: "BlackRoad Internal", category: "BlackRoad" },
23
+ ],
24
+ personal_channel: [
25
+ { start: "00:00", end: "08:00", label: "Late Night Archive", category: "Archive" },
26
+ { start: "08:00", end: "16:00", label: "Day Systems", category: "Systems" },
27
+ { start: "16:00", end: "24:00", label: "Evening Documentary", category: "Documentary" },
28
+ ],
29
+ };
v2/src/content/videos.js ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const VIDEOS = [
2
+ {
3
+ id: "vid_3b1b_calc_01",
4
+ title: "Essence of Calculus",
5
+ sourceType: "youtube",
6
+ embedUrl: "https://www.youtube.com/embed/VIDEO_ID_3B1B?autoplay=1&mute=1&playsinline=1",
7
+ sourceName: "3Blue1Brown",
8
+ category: "Math",
9
+ description: "Visual intuition for calculus.",
10
+ durationMinutes: 18,
11
+ tier: 3,
12
+ energy: "medium",
13
+ bestTimeOfDay: ["morning", "afternoon"],
14
+ tags: ["math", "visual", "core"],
15
+ enabled: true,
16
+ },
17
+ {
18
+ id: "vid_comp_01",
19
+ title: "How the Internet Works",
20
+ sourceType: "youtube",
21
+ embedUrl: "https://www.youtube.com/embed/VIDEO_ID_COMPUTERPHILE?autoplay=1&mute=1&playsinline=1",
22
+ sourceName: "Computerphile",
23
+ category: "Systems",
24
+ description: "A systems-level explainer for internet fundamentals.",
25
+ durationMinutes: 12,
26
+ tier: 3,
27
+ energy: "medium",
28
+ bestTimeOfDay: ["morning", "afternoon"],
29
+ tags: ["systems", "internet", "core"],
30
+ enabled: true,
31
+ },
32
+ {
33
+ id: "vid_ver_01",
34
+ title: "Deep Science",
35
+ sourceType: "youtube",
36
+ embedUrl: "https://www.youtube.com/embed/VIDEO_ID_VERITASIUM?autoplay=1&mute=1&playsinline=1",
37
+ sourceName: "Veritasium",
38
+ category: "Science",
39
+ description: "High-signal science with strong visual hooks.",
40
+ durationMinutes: 16,
41
+ tier: 3,
42
+ energy: "medium",
43
+ bestTimeOfDay: ["afternoon", "evening"],
44
+ tags: ["science", "visual"],
45
+ enabled: true,
46
+ },
47
+ {
48
+ id: "vid_doc_01",
49
+ title: "Documentary Feature",
50
+ sourceType: "youtube",
51
+ embedUrl: "https://www.youtube.com/embed/VIDEO_ID_DOC?autoplay=1&mute=1&playsinline=1",
52
+ sourceName: "DW Documentary",
53
+ category: "Documentary",
54
+ description: "Grounded real-world documentary programming.",
55
+ durationMinutes: 20,
56
+ tier: 2,
57
+ energy: "low",
58
+ bestTimeOfDay: ["evening", "night"],
59
+ tags: ["documentary", "human"],
60
+ enabled: true,
61
+ },
62
+ {
63
+ id: "vid_bell_01",
64
+ title: "Behind the Scenes with Telstar",
65
+ sourceType: "youtube",
66
+ embedUrl: "https://www.youtube.com/embed/MVI41ZfVSaM?autoplay=1&mute=1&playsinline=1",
67
+ sourceName: "Bell Telephone",
68
+ category: "Archive",
69
+ description: "Telecom history and communications systems.",
70
+ durationMinutes: 22,
71
+ tier: 3,
72
+ energy: "low",
73
+ bestTimeOfDay: ["late_night", "night"],
74
+ tags: ["archive", "telecom", "bell"],
75
+ enabled: true,
76
+ },
77
+ {
78
+ id: "vid_univac_01",
79
+ title: "Introduction to Digital Computers",
80
+ sourceType: "youtube",
81
+ embedUrl: "https://www.youtube.com/embed/qMnzbqgYlJ8?autoplay=1&mute=1&playsinline=1",
82
+ sourceName: "PeriscopeFilm",
83
+ category: "Archive",
84
+ description: "Early digital computing explained in period style.",
85
+ durationMinutes: 20,
86
+ tier: 3,
87
+ energy: "low",
88
+ bestTimeOfDay: ["late_night", "night"],
89
+ tags: ["archive", "computing", "periscopefilm"],
90
+ enabled: true,
91
+ },
92
+ {
93
+ id: "vid_ibm_01",
94
+ title: "IBM System/7 Computer System",
95
+ sourceType: "youtube",
96
+ embedUrl: "https://www.youtube.com/embed/HFmnpFlURFA?autoplay=1&mute=1&playsinline=1",
97
+ sourceName: "PeriscopeFilm",
98
+ category: "Archive",
99
+ description: "Industrial computing and control systems.",
100
+ durationMinutes: 17,
101
+ tier: 2,
102
+ energy: "low",
103
+ bestTimeOfDay: ["late_night", "night"],
104
+ tags: ["archive", "industrial", "computing"],
105
+ enabled: true,
106
+ },
107
+ {
108
+ id: "vid_blackroad_01",
109
+ title: "BlackRoad Internal Broadcast",
110
+ sourceType: "internal",
111
+ embedUrl: "/live/internal/standby.html",
112
+ sourceName: "BlackRoad",
113
+ category: "BlackRoad",
114
+ description: "Internal network, agent, and system broadcast.",
115
+ durationMinutes: 20,
116
+ tier: 5,
117
+ energy: "medium",
118
+ bestTimeOfDay: ["evening", "night"],
119
+ tags: ["internal", "blackroad"],
120
+ enabled: true,
121
+ }
122
+ ];
v2/src/core/router.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { renderChannelsPanel } from "../ui/channelsPanel.js";
2
+ import { renderGuide } from "../ui/guide.js";
3
+ import { renderTicker } from "../ui/ticker.js";
4
+
5
+ function resolveChannelId(pathname) {
6
+ if (pathname === "/live") return "channel_1";
7
+ if (pathname === "/live/2") return "channel_2";
8
+ if (pathname === "/live/3") return "channel_3";
9
+ if (pathname === "/live/4") return "channel_4";
10
+ if (pathname === "/live/5") return "channel_5";
11
+ if (pathname === "/live/me") return "personal_channel";
12
+ return "channel_1";
13
+ }
14
+
15
+ export function navigateTo(path) {
16
+ window.history.pushState({}, "", path);
17
+ window.dispatchEvent(new PopStateEvent("popstate"));
18
+ }
19
+
20
+ export function initRouter(state) {
21
+ function applyRoute() {
22
+ const path = window.location.pathname;
23
+ state.route = path;
24
+ state.activeChannelId = resolveChannelId(path);
25
+
26
+ renderChannelsPanel(state);
27
+ renderGuide(state);
28
+ renderTicker(state);
29
+ }
30
+
31
+ window.addEventListener("popstate", applyRoute);
32
+ applyRoute();
33
+ }
v2/src/core/state.js ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { loadPersistedState } from "./storage.js";
2
+
3
+ export function createInitialState() {
4
+ const persisted = loadPersistedState();
5
+
6
+ return {
7
+ mode: persisted?.mode || "AUTO",
8
+ route: window.location.pathname || persisted?.route || "/live",
9
+ activeChannelId: persisted?.activeChannelId || "channel_1",
10
+ activeBlockId: persisted?.activeBlockId || null,
11
+ activeVideoId: persisted?.activeVideoId || null,
12
+ manualOverride: persisted?.manualOverride || null,
13
+ guideStatus: persisted?.guideStatus || "AUTO SCHEDULE ACTIVE",
14
+ sessionFailures: persisted?.sessionFailures || {},
15
+ lastUpdatedAt: persisted?.lastUpdatedAt || Date.now(),
16
+ playbackStartedAt: null,
17
+ };
18
+ }
v2/src/core/storage.js ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const STORAGE_KEY = "blackroad.network.state.v1";
2
+ const ATTENTION_KEY = "blackroad.network.attention.v1";
3
+
4
+ export function loadPersistedState() {
5
+ try {
6
+ const raw = localStorage.getItem(STORAGE_KEY);
7
+ if (!raw) return null;
8
+ return JSON.parse(raw);
9
+ } catch (error) {
10
+ console.error("Failed to load persisted state", error);
11
+ return null;
12
+ }
13
+ }
14
+
15
+ export function savePersistedState(state) {
16
+ try {
17
+ const safeState = {
18
+ mode: state.mode,
19
+ route: state.route,
20
+ activeChannelId: state.activeChannelId,
21
+ activeBlockId: state.activeBlockId,
22
+ activeVideoId: state.activeVideoId,
23
+ manualOverride: state.manualOverride,
24
+ sessionFailures: state.sessionFailures,
25
+ guideStatus: state.guideStatus,
26
+ lastUpdatedAt: state.lastUpdatedAt,
27
+ };
28
+
29
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(safeState));
30
+ } catch (error) {
31
+ console.error("Failed to save persisted state", error);
32
+ }
33
+ }
34
+
35
+ export function clearPersistedState() {
36
+ try {
37
+ localStorage.removeItem(STORAGE_KEY);
38
+ } catch (error) {
39
+ console.error("Failed to clear persisted state", error);
40
+ }
41
+ }
42
+
43
+ export function loadAttentionEvents() {
44
+ try {
45
+ const raw = localStorage.getItem(ATTENTION_KEY);
46
+ if (!raw) return [];
47
+ return JSON.parse(raw);
48
+ } catch (error) {
49
+ console.error("Failed to load attention events", error);
50
+ return [];
51
+ }
52
+ }
53
+
54
+ export function saveAttentionEvents(events) {
55
+ try {
56
+ localStorage.setItem(ATTENTION_KEY, JSON.stringify(events));
57
+ } catch (error) {
58
+ console.error("Failed to save attention events", error);
59
+ }
60
+ }
61
+
62
+ export function pushAttentionEvent(event) {
63
+ const events = loadAttentionEvents();
64
+ events.push(event);
65
+
66
+ const trimmed = events.slice(-500);
67
+ saveAttentionEvents(trimmed);
68
+ }
69
+
70
+ export function clearManualOverride(state) {
71
+ state.manualOverride = null;
72
+ if (state.mode === "MANUAL") {
73
+ state.mode = "AUTO";
74
+ }
75
+ }
v2/src/logic/attention.js ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { pushAttentionEvent, loadAttentionEvents } from "../core/storage.js";
2
+
3
+ export function getTimeOfDayBucket(date = new Date()) {
4
+ const hour = date.getHours();
5
+
6
+ if (hour < 5) return "late_night";
7
+ if (hour < 11) return "morning";
8
+ if (hour < 17) return "afternoon";
9
+ if (hour < 22) return "evening";
10
+ return "night";
11
+ }
12
+
13
+ export function logAttentionEvent(type, payload = {}) {
14
+ pushAttentionEvent({
15
+ id: crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}_${Math.random()}`,
16
+ type,
17
+ timestamp: Date.now(),
18
+ timeOfDay: getTimeOfDayBucket(),
19
+ ...payload,
20
+ });
21
+ }
22
+
23
+ export function getAttentionSummary() {
24
+ const events = loadAttentionEvents();
25
+
26
+ const summary = {
27
+ totalEvents: events.length,
28
+ manualSelections: 0,
29
+ videoStarts: 0,
30
+ quickSkips: 0,
31
+ completions: 0,
32
+ byCategory: {},
33
+ byVideoId: {},
34
+ byTimeOfDay: {},
35
+ };
36
+
37
+ for (const event of events) {
38
+ if (event.type === "manual_selected") summary.manualSelections += 1;
39
+ if (event.type === "video_started") summary.videoStarts += 1;
40
+ if (event.type === "video_skipped_quick") summary.quickSkips += 1;
41
+ if (event.type === "video_completed") summary.completions += 1;
42
+
43
+ if (event.category) {
44
+ summary.byCategory[event.category] ??= {
45
+ starts: 0,
46
+ quickSkips: 0,
47
+ completions: 0,
48
+ manualSelections: 0,
49
+ };
50
+
51
+ if (event.type === "video_started") summary.byCategory[event.category].starts += 1;
52
+ if (event.type === "video_skipped_quick") summary.byCategory[event.category].quickSkips += 1;
53
+ if (event.type === "video_completed") summary.byCategory[event.category].completions += 1;
54
+ if (event.type === "manual_selected") summary.byCategory[event.category].manualSelections += 1;
55
+ }
56
+
57
+ if (event.videoId) {
58
+ summary.byVideoId[event.videoId] ??= {
59
+ starts: 0,
60
+ quickSkips: 0,
61
+ completions: 0,
62
+ manualSelections: 0,
63
+ };
64
+
65
+ if (event.type === "video_started") summary.byVideoId[event.videoId].starts += 1;
66
+ if (event.type === "video_skipped_quick") summary.byVideoId[event.videoId].quickSkips += 1;
67
+ if (event.type === "video_completed") summary.byVideoId[event.videoId].completions += 1;
68
+ if (event.type === "manual_selected") summary.byVideoId[event.videoId].manualSelections += 1;
69
+ }
70
+
71
+ if (event.timeOfDay) {
72
+ summary.byTimeOfDay[event.timeOfDay] ??= 0;
73
+ summary.byTimeOfDay[event.timeOfDay] += 1;
74
+ }
75
+ }
76
+
77
+ return summary;
78
+ }
79
+
80
+ export function getCategoryAffinity(category) {
81
+ const summary = getAttentionSummary();
82
+ const stats = summary.byCategory[category] || {
83
+ starts: 0,
84
+ quickSkips: 0,
85
+ completions: 0,
86
+ manualSelections: 0,
87
+ };
88
+
89
+ return (
90
+ stats.manualSelections * 5 +
91
+ stats.completions * 4 +
92
+ stats.starts * 1 -
93
+ stats.quickSkips * 6
94
+ );
95
+ }
96
+
97
+ export function getVideoAttentionScore(video) {
98
+ const summary = getAttentionSummary();
99
+ const stats = summary.byVideoId[video.id] || {
100
+ starts: 0,
101
+ quickSkips: 0,
102
+ completions: 0,
103
+ manualSelections: 0,
104
+ };
105
+
106
+ const timeOfDay = getTimeOfDayBucket();
107
+ const timeMatch = Array.isArray(video.bestTimeOfDay) && video.bestTimeOfDay.includes(timeOfDay) ? 8 : 0;
108
+ const categoryAffinity = getCategoryAffinity(video.category);
109
+
110
+ return (
111
+ (video.tier || 0) * 10 +
112
+ stats.manualSelections * 6 +
113
+ stats.completions * 4 -
114
+ stats.quickSkips * 5 +
115
+ timeMatch +
116
+ categoryAffinity
117
+ );
118
+ }
119
+
120
+ export function getTopCategories(limit = 3) {
121
+ const summary = getAttentionSummary();
122
+
123
+ return Object.entries(summary.byCategory)
124
+ .map(([category, stats]) => ({
125
+ category,
126
+ score:
127
+ stats.manualSelections * 5 +
128
+ stats.completions * 4 +
129
+ stats.starts -
130
+ stats.quickSkips * 6,
131
+ }))
132
+ .sort((a, b) => b.score - a.score)
133
+ .slice(0, limit);
134
+ }
v2/src/logic/resolver.js ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { VIDEOS } from "../content/videos.js";
2
+ import { getChannelById } from "../content/channels.js";
3
+ import { SCHEDULE } from "../content/schedule.js";
4
+ import { getVideoAttentionScore, getTopCategories, getTimeOfDayBucket } from "./attention.js";
5
+
6
+ export function getCurrentTimeHHMM() {
7
+ const now = new Date();
8
+ const hours = String(now.getHours()).padStart(2, "0");
9
+ const minutes = String(now.getMinutes()).padStart(2, "0");
10
+ return `${hours}:${minutes}`;
11
+ }
12
+
13
+ export function getCurrentBlock(channelId) {
14
+ const blocks = SCHEDULE[channelId] || [];
15
+ const now = getCurrentTimeHHMM();
16
+
17
+ return blocks.find((block) => {
18
+ const normalizedEnd = block.end === "24:00" ? "24:00" : block.end;
19
+ return now >= block.start && now < normalizedEnd;
20
+ }) || null;
21
+ }
22
+
23
+ export function getUpcomingBlocks(channelId, count = 3) {
24
+ const blocks = SCHEDULE[channelId] || [];
25
+ const now = getCurrentTimeHHMM();
26
+
27
+ const currentIndex = blocks.findIndex((block) => {
28
+ const normalizedEnd = block.end === "24:00" ? "24:00" : block.end;
29
+ return now >= block.start && now < normalizedEnd;
30
+ });
31
+
32
+ if (currentIndex === -1) return blocks.slice(0, count);
33
+ return blocks.slice(currentIndex + 1, currentIndex + 1 + count);
34
+ }
35
+
36
+ export function getVideoById(videoId) {
37
+ return VIDEOS.find((video) => video.id === videoId) || null;
38
+ }
39
+
40
+ function getPersonalCandidates(state) {
41
+ const topCategories = getTopCategories(3).map((item) => item.category);
42
+ const timeOfDay = getTimeOfDayBucket();
43
+
44
+ const enabled = VIDEOS
45
+ .filter((video) => video.enabled)
46
+ .filter((video) => !state.sessionFailures[video.id]);
47
+
48
+ const categoryMatches = enabled.filter((video) => topCategories.includes(video.category));
49
+ const timeMatches = enabled.filter(
50
+ (video) => Array.isArray(video.bestTimeOfDay) && video.bestTimeOfDay.includes(timeOfDay)
51
+ );
52
+
53
+ const merged = [...new Map([...categoryMatches, ...timeMatches, ...enabled].map((video) => [video.id, video])).values()];
54
+
55
+ return merged;
56
+ }
57
+
58
+ export function resolveVideoForChannel(state, channelId) {
59
+ if (channelId === "personal_channel") {
60
+ const pool = getPersonalCandidates(state)
61
+ .sort((a, b) => getVideoAttentionScore(b) - getVideoAttentionScore(a));
62
+
63
+ const video = pool[0] || null;
64
+
65
+ if (!video) return null;
66
+
67
+ return {
68
+ ...video,
69
+ resolverMeta: {
70
+ reason: "personal_attention_match",
71
+ timeOfDay: getTimeOfDayBucket(),
72
+ topCategories: getTopCategories(3),
73
+ },
74
+ };
75
+ }
76
+
77
+ const channel = getChannelById(channelId);
78
+ if (!channel || !channel.enabled) return null;
79
+
80
+ const block = getCurrentBlock(channelId);
81
+
82
+ const candidates = channel.videoIds
83
+ .map(getVideoById)
84
+ .filter(Boolean)
85
+ .filter((video) => video.enabled)
86
+ .filter((video) => !state.sessionFailures[video.id]);
87
+
88
+ if (!candidates.length) return null;
89
+
90
+ const categoryMatches = block
91
+ ? candidates.filter((video) => video.category === block.category)
92
+ : [];
93
+
94
+ const pool = categoryMatches.length ? categoryMatches : candidates;
95
+
96
+ const selected = [...pool]
97
+ .sort((a, b) => getVideoAttentionScore(b) - getVideoAttentionScore(a))[0] || null;
98
+
99
+ if (!selected) return null;
100
+
101
+ return {
102
+ ...selected,
103
+ resolverMeta: {
104
+ reason: categoryMatches.length ? "schedule_category_match" : "channel_best_available",
105
+ blockCategory: block?.category || null,
106
+ timeOfDay: getTimeOfDayBucket(),
107
+ },
108
+ };
109
+ }
110
+
111
+ export function explainResolvedVideo(video) {
112
+ if (!video?.resolverMeta) return "Default network selection.";
113
+
114
+ const { reason, blockCategory, timeOfDay, topCategories } = video.resolverMeta;
115
+
116
+ if (reason === "personal_attention_match") {
117
+ const top = Array.isArray(topCategories) && topCategories.length
118
+ ? topCategories.map((item) => item.category).join(", ")
119
+ : "current preferences";
120
+
121
+ return `Picked for your personal channel based on ${timeOfDay} and your strongest categories: ${top}.`;
122
+ }
123
+
124
+ if (reason === "schedule_category_match") {
125
+ return `Picked because it matches the active ${blockCategory} block and fits ${timeOfDay} viewing.`;
126
+ }
127
+
128
+ return `Picked as the best available source for ${timeOfDay}.`;
129
+ }
v2/src/main.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createInitialState } from "./core/state.js";
2
+ import { initRouter } from "./core/router.js";
3
+ import { renderShell, renderClock } from "./ui/shell.js";
4
+ import { startLiveLoop } from "./player/player.js";
5
+
6
+ const state = createInitialState();
7
+
8
+ renderShell(state);
9
+ initRouter(state);
10
+ startLiveLoop(state);
11
+
12
+ renderClock();
13
+ setInterval(renderClock, 1000);
v2/src/player/fallback.js ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getChannelById } from "../content/channels.js";
2
+ import { resolveVideoForChannel } from "../logic/resolver.js";
3
+
4
+ export function markVideoFailure(state, videoId) {
5
+ if (!videoId) return;
6
+ state.sessionFailures[videoId] = true;
7
+ }
8
+
9
+ export function clearVideoFailure(state, videoId) {
10
+ if (!videoId) return;
11
+ delete state.sessionFailures[videoId];
12
+ }
13
+
14
+ export function resolveFallbackVideo(state, channelId) {
15
+ const primary = resolveVideoForChannel(state, channelId);
16
+ if (primary) return primary;
17
+
18
+ const channel = getChannelById(channelId);
19
+ if (!channel) return null;
20
+
21
+ for (const fallbackChannelId of channel.fallbackChannelIds || []) {
22
+ const fallback = resolveVideoForChannel(state, fallbackChannelId);
23
+ if (fallback) {
24
+ state.mode = "FALLBACK";
25
+ return fallback;
26
+ }
27
+ }
28
+
29
+ return null;
30
+ }
v2/src/player/player.js ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getCurrentBlock, explainResolvedVideo } from "../logic/resolver.js";
2
+ import { renderNowPlaying } from "../ui/shell.js";
3
+ import { renderGuide } from "../ui/guide.js";
4
+ import { renderTicker } from "../ui/ticker.js";
5
+ import { renderChannelsPanel } from "../ui/channelsPanel.js";
6
+ import { renderVideoPanel } from "../ui/videoPanel.js";
7
+ import { renderDebugPanel } from "../ui/debugPanel.js";
8
+ import { savePersistedState, clearManualOverride } from "../core/storage.js";
9
+ import { logAttentionEvent } from "../logic/attention.js";
10
+ import { resolveFallbackVideo, markVideoFailure } from "./fallback.js";
11
+
12
+ let playerEl = null;
13
+ let lastStartedVideoId = null;
14
+
15
+ function bindControlEvents(state) {
16
+ const resumeButton = document.getElementById("resume-auto-button");
17
+ const clearFailuresButton = document.getElementById("clear-failures-button");
18
+
19
+ if (resumeButton) {
20
+ resumeButton.onclick = () => {
21
+ clearManualOverride(state);
22
+ state.guideStatus = "AUTO SCHEDULE ACTIVE";
23
+ savePersistedState(state);
24
+ };
25
+ }
26
+
27
+ if (clearFailuresButton) {
28
+ clearFailuresButton.onclick = () => {
29
+ state.sessionFailures = {};
30
+ if (state.mode === "FALLBACK" || state.mode === "STANDBY") {
31
+ state.mode = "AUTO";
32
+ }
33
+ savePersistedState(state);
34
+ };
35
+ }
36
+ }
37
+
38
+ function logVideoStart(state, video, block) {
39
+ if (!video || lastStartedVideoId === video.id) return;
40
+
41
+ lastStartedVideoId = video.id;
42
+ state.playbackStartedAt = Date.now();
43
+
44
+ logAttentionEvent("video_started", {
45
+ videoId: video.id,
46
+ category: video.category,
47
+ channelId: state.activeChannelId,
48
+ blockLabel: block?.label || null,
49
+ });
50
+ }
51
+
52
+ function logVideoExit(state, previousVideoId, previousCategory) {
53
+ if (!previousVideoId || !state.playbackStartedAt) return;
54
+
55
+ const elapsedSeconds = Math.floor((Date.now() - state.playbackStartedAt) / 1000);
56
+
57
+ if (elapsedSeconds < 30) {
58
+ logAttentionEvent("video_skipped_quick", {
59
+ videoId: previousVideoId,
60
+ category: previousCategory,
61
+ channelId: state.activeChannelId,
62
+ watchSeconds: elapsedSeconds,
63
+ });
64
+ } else if (elapsedSeconds > 180) {
65
+ logAttentionEvent("video_completed", {
66
+ videoId: previousVideoId,
67
+ category: previousCategory,
68
+ channelId: state.activeChannelId,
69
+ watchSeconds: elapsedSeconds,
70
+ });
71
+ }
72
+ }
73
+
74
+ function bindPlayerErrorHandling(state) {
75
+ if (!playerEl) return;
76
+
77
+ playerEl.onerror = () => {
78
+ if (!state.activeVideoId) return;
79
+
80
+ markVideoFailure(state, state.activeVideoId);
81
+ state.manualOverride = null;
82
+
83
+ const fallbackVideo = resolveFallbackVideo(state, state.activeChannelId);
84
+
85
+ if (fallbackVideo) {
86
+ state.mode = "FALLBACK";
87
+ state.activeVideoId = null;
88
+ state.guideStatus = "FALLBACK SOURCE ACTIVE";
89
+ savePersistedState(state);
90
+ } else {
91
+ state.mode = "STANDBY";
92
+ state.guideStatus = "STANDBY BROADCAST ACTIVE";
93
+ savePersistedState(state);
94
+ }
95
+ };
96
+ }
97
+
98
+ export function startLiveLoop(state) {
99
+ playerEl = document.getElementById("live-player");
100
+ bindControlEvents(state);
101
+ bindPlayerErrorHandling(state);
102
+
103
+ function tick() {
104
+ const block = getCurrentBlock(state.activeChannelId);
105
+ const previousVideoId = state.activeVideoId;
106
+ const previousCategory = state.manualOverride?.category || null;
107
+
108
+ let video = null;
109
+
110
+ if (state.manualOverride) {
111
+ video = state.manualOverride;
112
+ state.mode = "MANUAL";
113
+ state.guideStatus = "MANUAL VIDEO OVERRIDE ACTIVE";
114
+ } else {
115
+ video = resolveFallbackVideo(state, state.activeChannelId);
116
+
117
+ if (video) {
118
+ if (state.mode !== "FALLBACK") {
119
+ state.mode = "AUTO";
120
+ }
121
+ state.guideStatus = state.mode === "FALLBACK"
122
+ ? "FALLBACK SOURCE ACTIVE"
123
+ : "AUTO SCHEDULE ACTIVE";
124
+ }
125
+ }
126
+
127
+ const explanation = video ? explainResolvedVideo(video) : "";
128
+
129
+ if (!video) {
130
+ state.mode = "STANDBY";
131
+ state.activeVideoId = null;
132
+ state.activeBlockId = null;
133
+ state.guideStatus = "STANDBY BROADCAST ACTIVE";
134
+
135
+ renderNowPlaying(state, null, block, explanation);
136
+ renderGuide(state);
137
+ renderTicker(state);
138
+ renderChannelsPanel(state);
139
+ renderVideoPanel(state);
140
+ renderDebugPanel(state);
141
+ savePersistedState(state);
142
+ return;
143
+ }
144
+
145
+ if (state.activeVideoId !== video.id && playerEl) {
146
+ logVideoExit(state, previousVideoId, previousCategory);
147
+
148
+ state.activeVideoId = video.id;
149
+ state.activeBlockId = block?.label || null;
150
+ playerEl.src = video.embedUrl;
151
+
152
+ logVideoStart(state, video, block);
153
+ }
154
+
155
+ renderNowPlaying(state, video, block, explanation);
156
+ renderGuide(state);
157
+ renderTicker(state);
158
+ renderChannelsPanel(state);
159
+ renderVideoPanel(state);
160
+ renderDebugPanel(state);
161
+
162
+ state.lastUpdatedAt = Date.now();
163
+ savePersistedState(state);
164
+ }
165
+
166
+ tick();
167
+ setInterval(tick, 30000);
168
+ }
v2/src/styles/base.css ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ html,
6
+ body {
7
+ margin: 0;
8
+ padding: 0;
9
+ background: #000;
10
+ color: #fff;
11
+ font-family: Inter, Arial, sans-serif;
12
+ }
13
+
14
+ button,
15
+ input,
16
+ textarea,
17
+ select {
18
+ font: inherit;
19
+ }
v2/src/styles/components.css ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .brand,
2
+ .live-pill,
3
+ .mode-badge,
4
+ .clock,
5
+ .panel-title,
6
+ .eyebrow,
7
+ .guide-status,
8
+ .ticker-pill {
9
+ text-transform: uppercase;
10
+ letter-spacing: 0.1em;
11
+ }
12
+
13
+ .brand,
14
+ .live-pill,
15
+ .mode-badge,
16
+ .clock {
17
+ font-size: 12px;
18
+ }
19
+
20
+ .live-pill,
21
+ .mode-badge,
22
+ .ticker-pill,
23
+ .guide-status {
24
+ border: 1px solid #333;
25
+ padding: 4px 8px;
26
+ }
27
+
28
+ .panel-title,
29
+ .eyebrow {
30
+ font-size: 11px;
31
+ color: #9a9a9a;
32
+ }
33
+
34
+ .now-playing {
35
+ margin-top: 12px;
36
+ display: grid;
37
+ gap: 8px;
38
+ }
39
+
40
+ .title {
41
+ font-size: 24px;
42
+ font-weight: 700;
43
+ }
44
+
45
+ .meta,
46
+ .desc,
47
+ .channel-chip__tone,
48
+ .guide-category {
49
+ color: #a8a8a8;
50
+ font-size: 13px;
51
+ }
52
+
53
+ .meta {
54
+ display: flex;
55
+ gap: 12px;
56
+ flex-wrap: wrap;
57
+ }
58
+
59
+ #channels-panel,
60
+ #guide-panel,
61
+ #ticker {
62
+ display: grid;
63
+ gap: 8px;
64
+ }
65
+
66
+ .channel-chip {
67
+ width: 100%;
68
+ text-align: left;
69
+ background: #0b0b0b;
70
+ color: #fff;
71
+ border: 1px solid #2a2a2a;
72
+ padding: 10px;
73
+ cursor: pointer;
74
+ display: grid;
75
+ gap: 4px;
76
+ }
77
+
78
+ .channel-chip--active {
79
+ border-color: #fff;
80
+ }
81
+
82
+ .channel-chip__name {
83
+ font-weight: 600;
84
+ }
85
+
86
+ .guide-row {
87
+ display: grid;
88
+ grid-template-columns: 110px 1fr auto;
89
+ gap: 10px;
90
+ align-items: start;
91
+ border: 1px solid #242424;
92
+ padding: 10px;
93
+ background: #090909;
94
+ }
95
+
96
+ .guide-row--live {
97
+ border-color: #fff;
98
+ }
99
+
100
+ .guide-row--past {
101
+ opacity: 0.5;
102
+ }
103
+
104
+ .guide-time {
105
+ font-family: "JetBrains Mono", monospace;
106
+ color: #b0b0b0;
107
+ font-size: 12px;
108
+ }
109
+
110
+ .guide-label {
111
+ font-weight: 600;
112
+ margin-bottom: 4px;
113
+ }
114
+
115
+ #ticker {
116
+ display: flex;
117
+ gap: 8px;
118
+ flex-wrap: wrap;
119
+ }
120
+
121
+ .control-row {
122
+ display: flex;
123
+ gap: 8px;
124
+ flex-wrap: wrap;
125
+ }
126
+
127
+ .control-button {
128
+ background: #0b0b0b;
129
+ color: #fff;
130
+ border: 1px solid #333;
131
+ padding: 10px 12px;
132
+ cursor: pointer;
133
+ }
134
+
135
+ .guide-banner {
136
+ border: 1px solid #2b2b2b;
137
+ padding: 8px 10px;
138
+ font-size: 11px;
139
+ text-transform: uppercase;
140
+ letter-spacing: 0.08em;
141
+ color: #d0d0d0;
142
+ background: #0a0a0a;
143
+ }
144
+
145
+ #video-panel,
146
+ #debug-panel {
147
+ display: grid;
148
+ gap: 8px;
149
+ }
150
+
151
+ .video-chip {
152
+ width: 100%;
153
+ text-align: left;
154
+ background: #0b0b0b;
155
+ color: #fff;
156
+ border: 1px solid #2a2a2a;
157
+ padding: 10px;
158
+ cursor: pointer;
159
+ display: grid;
160
+ gap: 4px;
161
+ }
162
+
163
+ .video-chip--active {
164
+ border-color: #fff;
165
+ }
166
+
167
+ .video-chip__title {
168
+ font-weight: 600;
169
+ }
170
+
171
+ .video-chip__meta,
172
+ .selection-reason,
173
+ .debug-line {
174
+ color: #a8a8a8;
175
+ font-size: 13px;
176
+ }
177
+
178
+ .selection-reason {
179
+ border-top: 1px solid #1f1f1f;
180
+ padding-top: 8px;
181
+ }
182
+
183
+ .debug-block {
184
+ border: 1px solid #242424;
185
+ padding: 10px;
186
+ background: #090909;
187
+ }
188
+
189
+ .debug-title {
190
+ text-transform: uppercase;
191
+ letter-spacing: 0.08em;
192
+ font-size: 11px;
193
+ color: #d0d0d0;
194
+ margin-bottom: 8px;
195
+ }
v2/src/styles/layout.css ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .network-shell {
2
+ min-height: 100vh;
3
+ display: grid;
4
+ grid-template-rows: auto 1fr auto;
5
+ background: #000;
6
+ }
7
+
8
+ .topbar {
9
+ display: flex;
10
+ justify-content: space-between;
11
+ align-items: center;
12
+ gap: 12px;
13
+ padding: 12px 16px;
14
+ border-bottom: 1px solid #1f1f1f;
15
+ background: #050505;
16
+ }
17
+
18
+ .brand-wrap,
19
+ .topbar-center {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: 8px;
23
+ }
24
+
25
+ .layout {
26
+ display: grid;
27
+ grid-template-columns: minmax(0, 2fr) minmax(320px, 420px);
28
+ gap: 12px;
29
+ padding: 12px;
30
+ }
31
+
32
+ .main-panel,
33
+ .panel,
34
+ .ticker-wrap {
35
+ border: 1px solid #1f1f1f;
36
+ background: #050505;
37
+ }
38
+
39
+ .main-panel {
40
+ padding: 12px;
41
+ }
42
+
43
+ .player-wrap {
44
+ aspect-ratio: 16 / 9;
45
+ background: #000;
46
+ border: 1px solid #222;
47
+ }
48
+
49
+ .player-wrap iframe {
50
+ width: 100%;
51
+ height: 100%;
52
+ border: 0;
53
+ }
54
+
55
+ .side-panel {
56
+ display: grid;
57
+ gap: 12px;
58
+ }
59
+
60
+ .panel {
61
+ padding: 12px;
62
+ }
63
+
64
+ .ticker-wrap {
65
+ display: flex;
66
+ gap: 12px;
67
+ align-items: center;
68
+ padding: 12px 16px;
69
+ }
70
+
71
+ @media (max-width: 960px) {
72
+ .layout {
73
+ grid-template-columns: 1fr;
74
+ }
75
+ }
v2/src/ui/channelsPanel.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CHANNELS } from "../content/channels.js";
2
+ import { resolveVideoForChannel } from "../logic/resolver.js";
3
+ import { logAttentionEvent } from "../logic/attention.js";
4
+
5
+ export function renderChannelsPanel(state) {
6
+ const el = document.getElementById("channels-panel");
7
+ if (!el) return;
8
+
9
+ el.innerHTML = CHANNELS.map((channel) => `
10
+ <button
11
+ class="channel-chip ${channel.id === state.activeChannelId ? "channel-chip--active" : ""}"
12
+ data-channel-id="${channel.id}"
13
+ type="button"
14
+ >
15
+ <span class="channel-chip__name">${channel.name}</span>
16
+ <span class="channel-chip__tone">${channel.tone}</span>
17
+ </button>
18
+ `).join("");
19
+
20
+ el.querySelectorAll("[data-channel-id]").forEach((button) => {
21
+ button.addEventListener("click", () => {
22
+ const channelId = button.dataset.channelId;
23
+ const video = resolveVideoForChannel(state, channelId);
24
+
25
+ state.activeChannelId = channelId;
26
+
27
+ if (video) {
28
+ state.manualOverride = video;
29
+ state.mode = "MANUAL";
30
+
31
+ logAttentionEvent("manual_selected", {
32
+ videoId: video.id,
33
+ channelId,
34
+ category: video.category,
35
+ });
36
+ }
37
+ });
38
+ });
39
+ }
v2/src/ui/debugPanel.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getAttentionSummary, getTopCategories } from "../logic/attention.js";
2
+ import { explainResolvedVideo, resolveVideoForChannel } from "../logic/resolver.js";
3
+
4
+ export function renderDebugPanel(state) {
5
+ const el = document.getElementById("debug-panel");
6
+ if (!el) return;
7
+
8
+ const attention = getAttentionSummary();
9
+ const resolved = state.manualOverride || resolveVideoForChannel(state, state.activeChannelId);
10
+ const explanation = resolved ? explainResolvedVideo(resolved) : "No active resolution.";
11
+ const topCategories = getTopCategories(3);
12
+
13
+ el.innerHTML = `
14
+ <div class="debug-block">
15
+ <div class="debug-title">Resolver</div>
16
+ <div class="debug-line">${explanation}</div>
17
+ </div>
18
+
19
+ <div class="debug-block">
20
+ <div class="debug-title">Top Categories</div>
21
+ ${topCategories.length
22
+ ? topCategories.map((item) => `<div class="debug-line">${item.category}: ${item.score}</div>`).join("")
23
+ : `<div class="debug-line">No category data yet.</div>`}
24
+ </div>
25
+
26
+ <div class="debug-block">
27
+ <div class="debug-title">Attention</div>
28
+ <div class="debug-line">Events: ${attention.totalEvents}</div>
29
+ <div class="debug-line">Manual: ${attention.manualSelections}</div>
30
+ <div class="debug-line">Starts: ${attention.videoStarts}</div>
31
+ <div class="debug-line">Quick skips: ${attention.quickSkips}</div>
32
+ <div class="debug-line">Completions: ${attention.completions}</div>
33
+ </div>
34
+
35
+ <div class="debug-block">
36
+ <div class="debug-title">Failures</div>
37
+ <div class="debug-line">${Object.keys(state.sessionFailures || {}).length} failed sources this session</div>
38
+ </div>
39
+ `;
40
+ }
v2/src/ui/guide.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SCHEDULE } from "../content/schedule.js";
2
+ import { getCurrentBlock, getUpcomingBlocks } from "../logic/resolver.js";
3
+
4
+ export function renderGuide(state) {
5
+ const el = document.getElementById("guide-panel");
6
+ if (!el) return;
7
+
8
+ const current = getCurrentBlock(state.activeChannelId);
9
+ const upcoming = getUpcomingBlocks(state.activeChannelId, 3);
10
+ const allBlocks = SCHEDULE[state.activeChannelId] || [];
11
+ const previous = allBlocks.find((block) => block.end === current?.start);
12
+
13
+ const statusRow = `
14
+ <div class="guide-banner">${state.guideStatus || "AUTO SCHEDULE ACTIVE"}</div>
15
+ `;
16
+
17
+ el.innerHTML = `
18
+ ${statusRow}
19
+
20
+ ${previous ? `
21
+ <div class="guide-row guide-row--past">
22
+ <div class="guide-time">${previous.start}\u2013${previous.end}</div>
23
+ <div class="guide-main">
24
+ <div class="guide-label">${previous.label}</div>
25
+ <div class="guide-category">${previous.category}</div>
26
+ </div>
27
+ <div class="guide-status">PAST</div>
28
+ </div>
29
+ ` : ""}
30
+
31
+ ${current ? `
32
+ <div class="guide-row guide-row--live">
33
+ <div class="guide-time">${current.start}\u2013${current.end}</div>
34
+ <div class="guide-main">
35
+ <div class="guide-label">${current.label}</div>
36
+ <div class="guide-category">${current.category}</div>
37
+ </div>
38
+ <div class="guide-status">${state.mode}</div>
39
+ </div>
40
+ ` : ""}
41
+
42
+ ${upcoming.map((block, index) => `
43
+ <div class="guide-row">
44
+ <div class="guide-time">${block.start}\u2013${block.end}</div>
45
+ <div class="guide-main">
46
+ <div class="guide-label">${block.label}</div>
47
+ <div class="guide-category">${block.category}</div>
48
+ </div>
49
+ <div class="guide-status">${index === 0 ? "NEXT" : "LATER"}</div>
50
+ </div>
51
+ `).join("")}
52
+ `;
53
+ }
v2/src/ui/shell.js ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function renderShell(state) {
2
+ const app = document.getElementById("app");
3
+ if (!app) return;
4
+
5
+ app.innerHTML = `
6
+ <div class="network-shell">
7
+ <header class="topbar">
8
+ <div class="brand-wrap">
9
+ <div class="brand">BLACKROAD NETWORK</div>
10
+ <div class="live-pill">LIVE</div>
11
+ </div>
12
+
13
+ <div class="topbar-center">
14
+ <div id="mode-badge" class="mode-badge">${state.mode}</div>
15
+ </div>
16
+
17
+ <div id="clock" class="clock">--:--:--</div>
18
+ </header>
19
+
20
+ <main class="layout">
21
+ <section class="main-panel">
22
+ <div class="player-wrap">
23
+ <iframe
24
+ id="live-player"
25
+ allow="autoplay; encrypted-media; picture-in-picture"
26
+ allowfullscreen
27
+ playsinline
28
+ ></iframe>
29
+ </div>
30
+
31
+ <section id="now-playing" class="now-playing"></section>
32
+
33
+ <section class="panel">
34
+ <div class="panel-title">CONTROL</div>
35
+ <div class="control-row">
36
+ <button id="resume-auto-button" class="control-button" type="button">Resume Auto</button>
37
+ <button id="clear-failures-button" class="control-button" type="button">Clear Failures</button>
38
+ </div>
39
+ </section>
40
+
41
+ <section class="panel">
42
+ <div class="panel-title">VIDEO OVERRIDE</div>
43
+ <div id="video-panel"></div>
44
+ </section>
45
+ </section>
46
+
47
+ <aside class="side-panel">
48
+ <section class="panel">
49
+ <div class="panel-title">CHANNELS</div>
50
+ <div id="channels-panel"></div>
51
+ </section>
52
+
53
+ <section class="panel">
54
+ <div class="panel-title">GUIDE</div>
55
+ <div id="guide-panel"></div>
56
+ </section>
57
+
58
+ <section class="panel">
59
+ <div class="panel-title">DEBUG</div>
60
+ <div id="debug-panel"></div>
61
+ </section>
62
+ </aside>
63
+ </main>
64
+
65
+ <footer class="ticker-wrap">
66
+ <div class="panel-title">SYSTEM</div>
67
+ <div id="ticker"></div>
68
+ </footer>
69
+ </div>
70
+ `;
71
+ }
72
+
73
+ export function renderNowPlaying(state, video, block, explanation = "") {
74
+ const el = document.getElementById("now-playing");
75
+ const modeBadge = document.getElementById("mode-badge");
76
+
77
+ if (modeBadge) {
78
+ modeBadge.textContent = state.mode;
79
+ }
80
+
81
+ if (!el) return;
82
+
83
+ el.innerHTML = `
84
+ <div class="eyebrow">NOW PLAYING</div>
85
+ <div class="title">${video?.title || "Standby Broadcast"}</div>
86
+ <div class="meta">
87
+ <span>${video?.sourceName || "BlackRoad"}</span>
88
+ <span>${block?.label || "No active block"}</span>
89
+ <span>${video?.category || "--"}</span>
90
+ <span>${state.manualOverride ? "MANUAL OVERRIDE" : "SCHEDULED"}</span>
91
+ </div>
92
+ <div class="desc">${video?.description || "No source available."}</div>
93
+ <div class="selection-reason">${explanation || ""}</div>
94
+ `;
95
+ }
96
+
97
+ export function renderClock() {
98
+ const el = document.getElementById("clock");
99
+ if (!el) return;
100
+
101
+ const now = new Date();
102
+ el.textContent = now.toLocaleTimeString([], {
103
+ hour: "2-digit",
104
+ minute: "2-digit",
105
+ second: "2-digit",
106
+ });
107
+ }
v2/src/ui/ticker.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getChannelById } from "../content/channels.js";
2
+
3
+ export function renderTicker(state) {
4
+ const el = document.getElementById("ticker");
5
+ if (!el) return;
6
+
7
+ const channel = getChannelById(state.activeChannelId);
8
+
9
+ const items = [
10
+ `MODE ${state.mode}`,
11
+ `CHANNEL ${channel?.name || "--"}`,
12
+ `BLOCK ${state.activeBlockId || "--"}`,
13
+ `VIDEO ${state.activeVideoId || "--"}`,
14
+ `FAILURES ${Object.keys(state.sessionFailures || {}).length}`,
15
+ `PRODUCTS 22`,
16
+ `ORGS 44`,
17
+ `DOMAINS 20`,
18
+ ];
19
+
20
+ el.innerHTML = items
21
+ .map((item) => `<span class="ticker-pill">${item}</span>`)
22
+ .join("");
23
+ }
v2/src/ui/videoPanel.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { VIDEOS } from "../content/videos.js";
2
+ import { logAttentionEvent } from "../logic/attention.js";
3
+
4
+ export function renderVideoPanel(state) {
5
+ const el = document.getElementById("video-panel");
6
+ if (!el) return;
7
+
8
+ const pool = VIDEOS
9
+ .filter((video) => video.enabled)
10
+ .filter((video) => !state.sessionFailures[video.id]);
11
+
12
+ el.innerHTML = pool.map((video) => `
13
+ <button
14
+ class="video-chip ${video.id === state.activeVideoId ? "video-chip--active" : ""}"
15
+ data-video-id="${video.id}"
16
+ type="button"
17
+ >
18
+ <span class="video-chip__title">${video.title}</span>
19
+ <span class="video-chip__meta">${video.sourceName} / ${video.category}</span>
20
+ </button>
21
+ `).join("");
22
+
23
+ el.querySelectorAll("[data-video-id]").forEach((button) => {
24
+ button.addEventListener("click", () => {
25
+ const videoId = button.dataset.videoId;
26
+ const video = VIDEOS.find((item) => item.id === videoId);
27
+ if (!video) return;
28
+
29
+ state.manualOverride = video;
30
+ state.mode = "MANUAL";
31
+ state.guideStatus = "MANUAL VIDEO OVERRIDE ACTIVE";
32
+
33
+ logAttentionEvent("manual_selected", {
34
+ videoId: video.id,
35
+ category: video.category,
36
+ channelId: state.activeChannelId,
37
+ });
38
+ });
39
+ });
40
+ }