Spaces:
Running
BlackRoad Network v2: full broadcast system + modular codebase
Browse filesv1 (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 +243 -0
- index.html +0 -0
- live.js +570 -0
- schedule.js +120 -0
- search.html +524 -0
- v2/public/index.html +15 -0
- v2/src/content/channels.js +75 -0
- v2/src/content/schedule.js +29 -0
- v2/src/content/videos.js +122 -0
- v2/src/core/router.js +33 -0
- v2/src/core/state.js +18 -0
- v2/src/core/storage.js +75 -0
- v2/src/logic/attention.js +134 -0
- v2/src/logic/resolver.js +129 -0
- v2/src/main.js +13 -0
- v2/src/player/fallback.js +30 -0
- v2/src/player/player.js +168 -0
- v2/src/styles/base.css +19 -0
- v2/src/styles/components.css +195 -0
- v2/src/styles/layout.css +75 -0
- v2/src/ui/channelsPanel.js +39 -0
- v2/src/ui/debugPanel.js +40 -0
- v2/src/ui/guide.js +53 -0
- v2/src/ui/shell.js +107 -0
- v2/src/ui/ticker.js +23 -0
- v2/src/ui/videoPanel.js +40 -0
|
@@ -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 |
+
}
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -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);
|
|
@@ -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 |
+
}
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
};
|
|
@@ -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 |
+
];
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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);
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|