prismcast 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/app.js +18 -1
  2. package/dist/app.js.map +1 -1
  3. package/dist/browser/cdp.d.ts +2 -1
  4. package/dist/browser/cdp.js +44 -23
  5. package/dist/browser/cdp.js.map +1 -1
  6. package/dist/browser/channelSelection.d.ts +45 -1
  7. package/dist/browser/channelSelection.js +164 -27
  8. package/dist/browser/channelSelection.js.map +1 -1
  9. package/dist/browser/display.d.ts +0 -4
  10. package/dist/browser/display.js +0 -7
  11. package/dist/browser/display.js.map +1 -1
  12. package/dist/browser/index.d.ts +5 -0
  13. package/dist/browser/index.js +14 -0
  14. package/dist/browser/index.js.map +1 -1
  15. package/dist/browser/precaching.d.ts +5 -0
  16. package/dist/browser/precaching.js +112 -0
  17. package/dist/browser/precaching.js.map +1 -0
  18. package/dist/browser/tuning/directv.d.ts +2 -0
  19. package/dist/browser/tuning/directv.js +723 -0
  20. package/dist/browser/tuning/directv.js.map +1 -0
  21. package/dist/browser/tuning/fox.d.ts +2 -2
  22. package/dist/browser/tuning/fox.js +141 -22
  23. package/dist/browser/tuning/fox.js.map +1 -1
  24. package/dist/browser/tuning/hbo.d.ts +2 -2
  25. package/dist/browser/tuning/hbo.js +110 -41
  26. package/dist/browser/tuning/hbo.js.map +1 -1
  27. package/dist/browser/tuning/hulu.d.ts +2 -2
  28. package/dist/browser/tuning/hulu.js +442 -168
  29. package/dist/browser/tuning/hulu.js.map +1 -1
  30. package/dist/browser/tuning/sling.d.ts +2 -2
  31. package/dist/browser/tuning/sling.js +200 -56
  32. package/dist/browser/tuning/sling.js.map +1 -1
  33. package/dist/browser/tuning/youtubeTv.d.ts +2 -2
  34. package/dist/browser/tuning/youtubeTv.js +243 -57
  35. package/dist/browser/tuning/youtubeTv.js.map +1 -1
  36. package/dist/browser/video.d.ts +1 -1
  37. package/dist/browser/video.js +4 -2
  38. package/dist/browser/video.js.map +1 -1
  39. package/dist/channels/index.js +280 -34
  40. package/dist/channels/index.js.map +1 -1
  41. package/dist/config/health.d.ts +78 -0
  42. package/dist/config/health.js +202 -0
  43. package/dist/config/health.js.map +1 -0
  44. package/dist/config/paths.d.ts +10 -0
  45. package/dist/config/paths.js +14 -0
  46. package/dist/config/paths.js.map +1 -1
  47. package/dist/config/profiles.d.ts +6 -4
  48. package/dist/config/profiles.js +65 -14
  49. package/dist/config/profiles.js.map +1 -1
  50. package/dist/config/providerPacks.d.ts +46 -0
  51. package/dist/config/providerPacks.js +200 -0
  52. package/dist/config/providerPacks.js.map +1 -0
  53. package/dist/config/providers.d.ts +36 -3
  54. package/dist/config/providers.js +168 -8
  55. package/dist/config/providers.js.map +1 -1
  56. package/dist/config/sites.d.ts +1 -12
  57. package/dist/config/sites.js +38 -5
  58. package/dist/config/sites.js.map +1 -1
  59. package/dist/config/userChannels.d.ts +12 -0
  60. package/dist/config/userChannels.js +81 -14
  61. package/dist/config/userChannels.js.map +1 -1
  62. package/dist/config/userConfig.d.ts +5 -1
  63. package/dist/config/userConfig.js +58 -6
  64. package/dist/config/userConfig.js.map +1 -1
  65. package/dist/config/userProfiles.d.ts +81 -0
  66. package/dist/config/userProfiles.js +424 -0
  67. package/dist/config/userProfiles.js.map +1 -0
  68. package/dist/routes/config/channels/index.d.ts +3 -0
  69. package/dist/routes/config/channels/index.js +3 -0
  70. package/dist/routes/config/channels/index.js.map +1 -0
  71. package/dist/routes/config/channels/routes.d.ts +6 -0
  72. package/dist/routes/config/channels/routes.js +797 -0
  73. package/dist/routes/config/channels/routes.js.map +1 -0
  74. package/dist/routes/config/channels/table.d.ts +46 -0
  75. package/dist/routes/config/channels/table.js +811 -0
  76. package/dist/routes/config/channels/table.js.map +1 -0
  77. package/dist/routes/config/index.d.ts +37 -0
  78. package/dist/routes/config/index.js +81 -0
  79. package/dist/routes/config/index.js.map +1 -0
  80. package/dist/routes/config/providers.d.ts +18 -0
  81. package/dist/routes/config/providers.js +573 -0
  82. package/dist/routes/config/providers.js.map +1 -0
  83. package/dist/routes/config/settings.d.ts +39 -0
  84. package/dist/routes/config/settings.js +820 -0
  85. package/dist/routes/config/settings.js.map +1 -0
  86. package/dist/routes/index.d.ts +3 -2
  87. package/dist/routes/index.js +7 -4
  88. package/dist/routes/index.js.map +1 -1
  89. package/dist/routes/playlist.d.ts +5 -1
  90. package/dist/routes/playlist.js +32 -7
  91. package/dist/routes/playlist.js.map +1 -1
  92. package/dist/routes/providers.d.ts +6 -0
  93. package/dist/routes/providers.js +166 -0
  94. package/dist/routes/providers.js.map +1 -0
  95. package/dist/routes/root/content.d.ts +32 -0
  96. package/dist/routes/root/content.js +857 -0
  97. package/dist/routes/root/content.js.map +1 -0
  98. package/dist/routes/root/index.js +197 -0
  99. package/dist/routes/root/index.js.map +1 -0
  100. package/dist/routes/root/scripts/channels.d.ts +1 -0
  101. package/dist/routes/root/scripts/channels.js +745 -0
  102. package/dist/routes/root/scripts/channels.js.map +1 -0
  103. package/dist/routes/root/scripts/config.d.ts +1 -0
  104. package/dist/routes/root/scripts/config.js +1662 -0
  105. package/dist/routes/root/scripts/config.js.map +1 -0
  106. package/dist/routes/root/scripts/index.d.ts +3 -0
  107. package/dist/routes/root/scripts/index.js +8 -0
  108. package/dist/routes/root/scripts/index.js.map +1 -0
  109. package/dist/routes/root/scripts/status.d.ts +1 -0
  110. package/dist/routes/root/scripts/status.js +436 -0
  111. package/dist/routes/root/scripts/status.js.map +1 -0
  112. package/dist/routes/root/styles.d.ts +5 -0
  113. package/dist/routes/root/styles.js +447 -0
  114. package/dist/routes/root/styles.js.map +1 -0
  115. package/dist/routes/ui.js +8 -3
  116. package/dist/routes/ui.js.map +1 -1
  117. package/dist/service/commands.d.ts +5 -3
  118. package/dist/service/commands.js +117 -33
  119. package/dist/service/commands.js.map +1 -1
  120. package/dist/service/generators.d.ts +27 -0
  121. package/dist/service/generators.js +97 -6
  122. package/dist/service/generators.js.map +1 -1
  123. package/dist/streaming/hls.js +18 -1
  124. package/dist/streaming/hls.js.map +1 -1
  125. package/dist/streaming/lifecycle.js +1 -1
  126. package/dist/streaming/lifecycle.js.map +1 -1
  127. package/dist/streaming/monitor.d.ts +1 -38
  128. package/dist/streaming/monitor.js +2 -289
  129. package/dist/streaming/monitor.js.map +1 -1
  130. package/dist/streaming/recovery.d.ts +143 -0
  131. package/dist/streaming/recovery.js +286 -0
  132. package/dist/streaming/recovery.js.map +1 -0
  133. package/dist/streaming/registry.d.ts +1 -1
  134. package/dist/streaming/setup.d.ts +1 -1
  135. package/dist/streaming/setup.js +3 -1
  136. package/dist/streaming/setup.js.map +1 -1
  137. package/dist/streaming/statusEmitter.d.ts +4 -2
  138. package/dist/streaming/statusEmitter.js +5 -0
  139. package/dist/streaming/statusEmitter.js.map +1 -1
  140. package/dist/types/channels.d.ts +68 -0
  141. package/dist/types/channels.js +2 -0
  142. package/dist/types/channels.js.map +1 -0
  143. package/dist/types/config.d.ts +127 -0
  144. package/dist/types/config.js +2 -0
  145. package/dist/types/config.js.map +1 -0
  146. package/dist/types/index.d.ts +7 -429
  147. package/dist/types/index.js +1 -5
  148. package/dist/types/index.js.map +1 -1
  149. package/dist/types/profiles.d.ts +141 -0
  150. package/dist/types/profiles.js +2 -0
  151. package/dist/types/profiles.js.map +1 -0
  152. package/dist/types/selection.d.ts +101 -0
  153. package/dist/types/selection.js +6 -0
  154. package/dist/types/selection.js.map +1 -0
  155. package/dist/types/shared.d.ts +7 -0
  156. package/dist/types/shared.js +6 -0
  157. package/dist/types/shared.js.map +1 -0
  158. package/dist/types/streaming.d.ts +96 -0
  159. package/dist/types/streaming.js +2 -0
  160. package/dist/types/streaming.js.map +1 -0
  161. package/dist/utils/debugFilter.js +3 -1
  162. package/dist/utils/debugFilter.js.map +1 -1
  163. package/dist/utils/format.d.ts +7 -0
  164. package/dist/utils/format.js +22 -0
  165. package/dist/utils/format.js.map +1 -1
  166. package/dist/utils/index.d.ts +1 -0
  167. package/dist/utils/index.js +1 -0
  168. package/dist/utils/index.js.map +1 -1
  169. package/dist/utils/sanitize.d.ts +14 -0
  170. package/dist/utils/sanitize.js +37 -0
  171. package/dist/utils/sanitize.js.map +1 -0
  172. package/package.json +5 -3
  173. package/dist/routes/config.d.ts +0 -76
  174. package/dist/routes/config.js +0 -2187
  175. package/dist/routes/config.js.map +0 -1
  176. package/dist/routes/root.js +0 -3158
  177. package/dist/routes/root.js.map +0 -1
  178. /package/dist/routes/{root.d.ts → root/index.d.ts} +0 -0
@@ -0,0 +1,857 @@
1
+ /* Copyright(C) 2024-2026, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * content.ts: Tab content HTML generators for the PrismCast landing page.
4
+ */
5
+ import { escapeHtml, isRunningAsService } from "../../utils/index.js";
6
+ import { generateAdvancedTabContent, generateChannelsPanel, generateProvidersPanel, generateSettingsFormFooter, generateSettingsTabContent, generateWizardModal, hasEnvOverrides } from "../config/index.js";
7
+ import { getUITabs } from "../../config/userConfig.js";
8
+ /**
9
+ * Generates the active streams table for the Overview tab.
10
+ * @returns HTML content for the active streams section.
11
+ */
12
+ function generateActiveStreamsSection() {
13
+ return [
14
+ "<div id=\"streams-container\">",
15
+ "<table id=\"streams-table\" class=\"streams-table\">",
16
+ "<tbody id=\"streams-tbody\">",
17
+ "<tr class=\"empty-row\"><td colspan=\"4\">No active streams</td></tr>",
18
+ "</tbody>",
19
+ "</table>",
20
+ "</div>"
21
+ ].join("\n");
22
+ }
23
+ /**
24
+ * Generates the Backup subtab content with download and import functionality for both settings and channels.
25
+ * @returns HTML content for the Backup subtab panel.
26
+ */
27
+ function generateBackupPanel() {
28
+ // Description text varies based on whether running as a managed service.
29
+ const restartDescription = isRunningAsService() ?
30
+ "The server will restart automatically to apply the imported settings." :
31
+ "After importing, you will need to restart PrismCast for changes to take effect.";
32
+ return [
33
+ // Panel description.
34
+ "<p class=\"settings-panel-description\">Export and import configuration and channel data.</p>",
35
+ // Settings backup section.
36
+ "<div class=\"backup-group\">",
37
+ "<div class=\"backup-group-title\">Settings Backup</div>",
38
+ "<div class=\"backup-section\">",
39
+ "<h3>Download Settings</h3>",
40
+ "<p>Download your current server configuration as a JSON file. This includes all settings (server, browser, streaming, playback, etc.) ",
41
+ "but does not include channel definitions.</p>",
42
+ "<button type=\"button\" class=\"btn btn-export\" onclick=\"exportConfig()\">Download Settings</button>",
43
+ "</div>",
44
+ "<div class=\"backup-section\">",
45
+ "<h3>Import Settings</h3>",
46
+ "<p>Import a previously saved settings file. " + restartDescription + "</p>",
47
+ "<button type=\"button\" class=\"btn btn-import\" onclick=\"document.getElementById('import-settings-file').click()\">Import Settings</button>",
48
+ "<input type=\"file\" id=\"import-settings-file\" accept=\".json\" onchange=\"importConfig(this)\">",
49
+ "</div>",
50
+ "</div>",
51
+ // Channels backup section.
52
+ "<div class=\"backup-group\">",
53
+ "<div class=\"backup-group-title\">Channels Backup</div>",
54
+ "<div class=\"backup-section\">",
55
+ "<h3>Download Channels</h3>",
56
+ "<p>Download your custom channel definitions as a JSON file. This includes only user-defined channels, not the predefined channels ",
57
+ "built into PrismCast.</p>",
58
+ "<button type=\"button\" class=\"btn btn-export\" onclick=\"exportChannels()\">Download Channels</button>",
59
+ "</div>",
60
+ "<div class=\"backup-section\">",
61
+ "<h3>Import Channels</h3>",
62
+ "<p>Import channel definitions from a previously saved file. This will <strong>replace all existing user channels</strong>.</p>",
63
+ "<button type=\"button\" class=\"btn btn-import\" onclick=\"document.getElementById('import-channels-file').click()\">Import Channels</button>",
64
+ "<input type=\"file\" id=\"import-channels-file\" accept=\".json\" onchange=\"importChannels(this)\">",
65
+ "</div>",
66
+ "</div>"
67
+ ].join("\n");
68
+ }
69
+ /**
70
+ * Generates the Overview tab content with a comprehensive user guide covering what PrismCast is, video quality expectations, quick start instructions, tuning speed,
71
+ * channel authentication, working with channels, and system requirements.
72
+ * @param baseUrl - The base URL for the server.
73
+ * @returns HTML content for the Overview tab.
74
+ */
75
+ export function generateOverviewContent(baseUrl) {
76
+ return [
77
+ // Active streams table at the top.
78
+ generateActiveStreamsSection(),
79
+ // What Is PrismCast?
80
+ "<div class=\"section\">",
81
+ "<h3>What Is PrismCast?</h3>",
82
+ "<p>PrismCast captures live video from web-based TV players by driving a real Chrome browser. It navigates to streaming sites, captures the ",
83
+ "screen and audio output, and serves the result as HLS streams over HTTP. Think of it as a <strong>virtual TV tuner for web-based content</strong> &mdash; ",
84
+ "it lets Channels DVR (and other applications) record and watch content from streaming sites that do not offer direct video URLs.</p>",
85
+ "<p>PrismCast is built around three priorities, in order:</p>",
86
+ "<ol>",
87
+ "<li><strong>Reliability</strong> &mdash; tuning a channel always delivers that channel. When the primary approach fails, fallback strategies ",
88
+ "ensure the tune still succeeds.</li>",
89
+ "<li><strong>Health monitoring</strong> &mdash; once a channel is playing, PrismCast continuously monitors the stream and takes corrective ",
90
+ "action automatically if issues arise.</li>",
91
+ "<li><strong>Speed</strong> &mdash; tuning and recovery should be as fast as possible, but never at the expense of reliability.</li>",
92
+ "</ol>",
93
+ "<p>The ordering is intentional. PrismCast will always choose the reliable path over the fast one.</p>",
94
+ "</div>",
95
+ // Video Quality.
96
+ "<div class=\"section\">",
97
+ "<h3>Video Quality</h3>",
98
+ "<p><strong>PrismCast delivers H.264 video with AAC stereo audio</strong> at configurable quality presets ranging from 480p to 1080p. ",
99
+ "Quality presets can be changed in the <a href=\"#config/settings\">Configuration</a> tab.</p>",
100
+ "<p>This is <em>not</em> a replacement for native 4K, HDR, Dolby Vision, or surround sound &mdash; it is screen capture, not a direct feed. ",
101
+ "PrismCast captures directly from Chrome's media pipeline with <strong>no video transcoding</strong>, which is why tuning is fast and CPU usage ",
102
+ "stays low. The result is good quality video that works well for everyday viewing and DVR recording. PrismCast is designed for content you ",
103
+ "<strong>cannot get any other way</strong> in Channels DVR: network streaming sites, free ad-supported TV, and live channels that only exist on the web.</p>",
104
+ "</div>",
105
+ // Quick Start (Channels DVR).
106
+ "<div class=\"section\">",
107
+ "<h3>Quick Start</h3>",
108
+ "<p>To add PrismCast channels to Channels DVR:</p>",
109
+ "<ol>",
110
+ "<li>Go to <strong>Settings &rarr; Custom Channels</strong> in your Channels DVR server.</li>",
111
+ "<li>Click <strong>Add Source</strong> and select <strong>M3U Playlist</strong>.</li>",
112
+ "<li>Enter the playlist URL: <code id=\"overview-playlist-url\">" + baseUrl + "/playlist</code> ",
113
+ "<button class=\"btn-copy-inline\" onclick=\"copyOverviewPlaylistUrl()\" title=\"Copy URL\">Copy</button>",
114
+ "<span id=\"overview-copy-feedback\" class=\"copy-feedback-inline\">Copied!</span></li>",
115
+ "<li>Set <strong>Stream Format</strong> to <strong>HLS</strong>.</li>",
116
+ "<li>Optionally, go to the <a href=\"#channels\">Channels tab</a> and set the <strong>provider filter</strong> to only include streaming services you ",
117
+ "subscribe to. This controls which channels Channels DVR sees in the playlist.</li>",
118
+ "<li>Your configured channels will be imported automatically.</li>",
119
+ "</ol>",
120
+ "<p>Individual channels can also be streamed directly using HLS URLs like <code>" + baseUrl + "/hls/nbc/stream.m3u8</code>.</p>",
121
+ "</div>",
122
+ // Plex Integration.
123
+ "<div class=\"section\">",
124
+ "<h3>Plex Integration</h3>",
125
+ "<p>PrismCast includes built-in HDHomeRun emulation, allowing Plex to use it as a network tuner for live TV and DVR recording.</p>",
126
+ "<ol>",
127
+ "<li>In Plex, go to <strong>Settings &rarr; Live TV &amp; DVR &rarr; Set Up Plex DVR</strong>.</li>",
128
+ "<li>Enter your PrismCast server address with port 5004 (e.g., <code>192.168.1.100:5004</code>).</li>",
129
+ "<li>Plex will detect PrismCast as an HDHomeRun tuner and import available channels.</li>",
130
+ "</ol>",
131
+ "<p>HDHomeRun emulation is enabled by default and can be configured in the ",
132
+ "<a href=\"#config/settings\">HDHomeRun / Plex</a> configuration tab.</p>",
133
+ "</div>",
134
+ // Tuning Speed.
135
+ "<div class=\"section\">",
136
+ "<h3>Tuning Speed</h3>",
137
+ "<p>When a client requests a channel, PrismCast navigates Chrome to the streaming site, locates the video player, starts capture, and serves the ",
138
+ "first HLS segment. How long this takes depends on the channel type:</p>",
139
+ "<h4>Direct URL Channels (~3&ndash;5 seconds)</h4>",
140
+ "<p>Sites where PrismCast navigates directly to a player page and video starts automatically. ",
141
+ "Examples: NBC, ABC, Paramount+, USA Network.</p>",
142
+ "<h4>Guide-Based Providers &mdash; First Tune (~5&ndash;10 seconds)</h4>",
143
+ "<p>Sites where PrismCast navigates a live TV guide to find and select the channel. The first tune for a given channel is slower because the ",
144
+ "guide grid must be searched. Examples: DirecTV Stream, HBO Max, Hulu, Sling TV, YouTube TV, Fox.</p>",
145
+ "<h4>Guide-Based Providers &mdash; Subsequent Tunes (~3&ndash;5 seconds)</h4>",
146
+ "<p>After the first tune, PrismCast caches channel data for <strong>DirecTV Stream, HBO Max, Hulu, Sling TV, and YouTube TV</strong>. ",
147
+ "Subsequent tunes skip guide navigation entirely and are comparable to direct URL channels. If cached data ",
148
+ "becomes stale, PrismCast falls back to guide navigation transparently.</p>",
149
+ "<h4>Idle Window</h4>",
150
+ "<p>Streams stay alive for <strong>30 seconds</strong> after the last client disconnects (configurable in the ",
151
+ "<a href=\"#config/settings\">Configuration</a> tab). This means channel surfing in Channels DVR is instant for recently-viewed channels &mdash; ",
152
+ "no re-tuning is needed. Combined with channel caching, the system gets faster the more you use it.</p>",
153
+ "</div>",
154
+ // Channel Authentication.
155
+ "<div class=\"section\">",
156
+ "<h3>Channel Authentication</h3>",
157
+ "<p>Many streaming channels require TV provider authentication before content can be accessed. To authenticate:</p>",
158
+ "<ol>",
159
+ "<li>Go to the <a href=\"#channels\">Channels tab</a>.</li>",
160
+ "<li>Click the <strong>Login</strong> button next to the channel you want to authenticate.</li>",
161
+ "<li>A browser window will open with the channel's streaming page.</li>",
162
+ "<li>Complete the TV provider sign-in process in the browser.</li>",
163
+ "<li>Click <strong>Done</strong> when authentication is complete.</li>",
164
+ "</ol>",
165
+ "<p>Your login credentials are saved in the browser profile and persist across restarts. You only need to authenticate once per TV provider. ",
166
+ "The Login button is stateless and always displays &ldquo;Login&rdquo; regardless of authentication status &mdash; successful authentication is ",
167
+ "confirmed when the channel streams correctly. Some TV providers periodically expire sessions on their end, requiring re-authentication. This is ",
168
+ "a provider limitation, not a PrismCast issue &mdash; simply click Login again to re-authenticate.</p>",
169
+ "<p class=\"description-hint\">If PrismCast is running headless or on a remote server, use a VNC client to access the browser for authentication.</p>",
170
+ "</div>",
171
+ // Working with Channels.
172
+ "<div class=\"section\">",
173
+ "<h3>Working with Channels</h3>",
174
+ "<h4>Predefined Channels</h4>",
175
+ "<p>PrismCast ships with channels across multiple streaming providers, maintained and updated with each release. You can disable any channels ",
176
+ "you do not need from the <a href=\"#channels\">Channels tab</a>. The predefined set covers common networks and is a good starting point &mdash; ",
177
+ "enable what you watch and disable the rest. You can also override any predefined channel with your own custom definition ",
178
+ "(see <em>Overriding Predefined Channels</em> below).</p>",
179
+ "<h4>Provider Variants</h4>",
180
+ "<p>Some channels (Comedy Central, Fox, NBC, etc.) are available from multiple streaming providers. The <strong>provider dropdown</strong> on each ",
181
+ "channel lets you choose which service to use for that channel. Different providers may offer different tuning performance.</p>",
182
+ "<h4>Provider Filter</h4>",
183
+ "<p>If you only subscribe to certain streaming services, use the <strong>provider filter</strong> on the ",
184
+ "<a href=\"#channels\">Channels tab</a> toolbar to show only relevant channels. This filter also controls which channels appear in the playlist ",
185
+ "that Channels DVR imports &mdash; set it before adding the playlist source in the <a href=\"#overview\">Quick Start</a>. You can also filter ",
186
+ "programmatically using the <code>?provider=</code> query parameter on the playlist URL.</p>",
187
+ "<h4>Bulk Operations</h4>",
188
+ "<p>The <strong>Set all channels to</strong> dropdown on the <a href=\"#channels\">Channels tab</a> toolbar switches every multi-provider channel ",
189
+ "to a single provider at once. This is useful when you want all channels routed through one streaming service. The operation can be undone by ",
190
+ "switching individual channels back or selecting a different provider from the same dropdown.</p>",
191
+ "<h4>User-Defined Channels</h4>",
192
+ "<p>You can add custom channels for any streaming site. Provide a URL, select a site profile, and PrismCast will capture it. For sites with ",
193
+ "multiple live channels (like a live TV provider), the <strong>Channel Selector</strong> field tells PrismCast which channel to tune to &mdash; ",
194
+ "the expected value depends on the provider. When adding or editing a channel, select a profile to see the <strong>Profile Reference</strong> ",
195
+ "section with site-specific guidance, including expected channel selector formats for known providers.</p>",
196
+ "<h4>Overriding Predefined Channels</h4>",
197
+ "<p>To override a predefined channel, create a user-defined channel with the same channel key. Both versions will appear in the provider ",
198
+ "dropdown &mdash; yours labeled <em>Custom</em> and the original with its provider name. You can switch between them at any time.</p>",
199
+ "<p class=\"description-hint\">For automation and integration with other workflows, see the <a href=\"#api\">API Reference</a> tab for the full HTTP API.</p>",
200
+ "</div>",
201
+ // Requirements.
202
+ "<div class=\"section\">",
203
+ "<h3>Requirements</h3>",
204
+ "<ul>",
205
+ "<li>Google Chrome browser installed.</li>",
206
+ "<li>Sufficient memory for browser automation (2GB+ recommended).</li>",
207
+ "<li>Network access to streaming sites.</li>",
208
+ "</ul>",
209
+ "<p class=\"description-hint\">See the <a href=\"#help\">Help</a> tab for platform-specific requirements and troubleshooting.</p>",
210
+ "</div>"
211
+ ].join("\n");
212
+ }
213
+ /**
214
+ * Generates the Help tab content with updating instructions, platform notes, troubleshooting, and known limitations.
215
+ * @returns HTML content for the Help tab.
216
+ */
217
+ export function generateHelpContent() {
218
+ return [
219
+ // Updating PrismCast.
220
+ "<div class=\"section\">",
221
+ "<h3>Updating PrismCast</h3>",
222
+ "<p>Settings and channel configurations are preserved across updates.</p>",
223
+ "<h4>Homebrew (macOS)</h4>",
224
+ "<pre>brew upgrade prismcast\nprismcast service restart</pre>",
225
+ "<h4>npm</h4>",
226
+ "<pre>npm install -g prismcast\nprismcast service restart</pre>",
227
+ "<h4>Docker</h4>",
228
+ "<p>Pull the latest image and recreate the container. If using Watchtower, updates are applied automatically.</p>",
229
+ "<pre>docker pull ghcr.io/hjdhjd/prismcast:latest\ndocker compose up -d</pre>",
230
+ "</div>",
231
+ // Display and Resolution.
232
+ "<div class=\"section\">",
233
+ "<h3>Display and Resolution</h3>",
234
+ "<p>PrismCast captures video from Chrome's display output. The <strong>capture resolution must be smaller than the physical display resolution</strong> ",
235
+ "because browser toolbars and window chrome consume approximately 100&ndash;150 vertical pixels. For example, to capture at 1080p (1920&times;1080), the ",
236
+ "display must be larger than 1080p.</p>",
237
+ "<p>When the selected quality preset exceeds what the display can provide, PrismCast logs a warning and automatically degrades to the best available preset. ",
238
+ "This is not an error &mdash; PrismCast is adapting to your display.</p>",
239
+ "<h4>Headless Servers</h4>",
240
+ "<p>macOS works without a physical monitor. Windows and Linux servers without a display need an <strong>HDMI dummy plug</strong> or a ",
241
+ "<strong>virtual display adapter</strong> to provide a display resolution for Chrome to render into.</p>",
242
+ "<h4>Remote Access</h4>",
243
+ "<p>macOS Screen Sharing and VNC work correctly. <strong>Windows Remote Desktop (RDP) does not work</strong> &mdash; RDP creates a virtual display ",
244
+ "with different properties that interfere with Chrome's rendering. Use VNC or connect a physical display on Windows.</p>",
245
+ "</div>",
246
+ // Platform Notes.
247
+ "<div class=\"section\">",
248
+ "<h3>Platform Notes</h3>",
249
+ "<h4>macOS</h4>",
250
+ "<p>Chrome on macOS uses GPU hardware acceleration for video encoding, providing the best capture performance. After installing Node.js, go to ",
251
+ "<strong>System Settings &rarr; Privacy &amp; Security &rarr; App Management</strong> and allow Node.js. Use Screen Sharing or VNC for remote access ",
252
+ "to the PrismCast machine.</p>",
253
+ "<h4>Windows</h4>",
254
+ "<p>Install PrismCast as a service with <code>prismcast service install</code>. See Remote Access above for display capture requirements.</p>",
255
+ "<h4>Linux / Docker</h4>",
256
+ "<p>Chrome cannot use GPU hardware acceleration with virtual displays on Linux (a Chrome limitation), so Docker containers rely on software ",
257
+ "rendering. Access the browser via VNC for authentication &mdash; Docker containers expose noVNC at port 6080.</p>",
258
+ "</div>",
259
+ // Troubleshooting.
260
+ "<div class=\"section\">",
261
+ "<h3>Troubleshooting</h3>",
262
+ "<table>",
263
+ "<tr><th>Problem</th><th>Cause</th><th>Solution</th></tr>",
264
+ "<tr>",
265
+ "<td>\"Browser Offline\" or \"Browser is not connected\"</td>",
266
+ "<td>An existing Chrome process is running.</td>",
267
+ "<td>Quit all Chrome instances, then restart PrismCast.</td>",
268
+ "</tr>",
269
+ "<tr>",
270
+ "<td>\"All tuners in use\" despite no active streams</td>",
271
+ "<td>Stale stream state.</td>",
272
+ "<td>Restart PrismCast service.</td>",
273
+ "</tr>",
274
+ "<tr>",
275
+ "<td>Chrome won't open for login</td>",
276
+ "<td>Running headless or as a service.</td>",
277
+ "<td>Access the PrismCast machine via VNC or Screen Sharing to complete authentication.</td>",
278
+ "</tr>",
279
+ "<tr>",
280
+ "<td>macOS blocks Node.js after install</td>",
281
+ "<td>App Management security gate.</td>",
282
+ "<td>System Settings &rarr; Privacy &amp; Security &rarr; App Management &rarr; Allow Node.js.</td>",
283
+ "</tr>",
284
+ "<tr>",
285
+ "<td>Port conflict (address in use)</td>",
286
+ "<td>Another service using port 5589.</td>",
287
+ "<td>Stop the conflicting service, or change the port in <a href=\"#config/settings\">Configuration</a>.</td>",
288
+ "</tr>",
289
+ "</table>",
290
+ "</div>",
291
+ // Known Limitations.
292
+ "<div class=\"section\">",
293
+ "<h3>Known Limitations</h3>",
294
+ "<ul>",
295
+ "<li><strong>Bitrate is approximate.</strong> Chrome's media encoder treats the configured bitrate as a target, not a hard limit. ",
296
+ "Actual bitrate may vary based on content complexity.</li>",
297
+ "<li><strong>Frame rate follows the source.</strong> If the streaming site delivers 30fps, capture will be 30fps regardless of the configured ",
298
+ "frame rate setting.</li>",
299
+ "<li><strong>No closed captions.</strong> Chrome's capture API does not include caption data. Subtitles are not available in PrismCast streams.</li>",
300
+ "<li><strong>No 4K, HDR, or surround sound.</strong> PrismCast captures H.264 video with AAC stereo audio. It is not a replacement for native ",
301
+ "4K, HDR, Dolby Vision, or Dolby Atmos content.</li>",
302
+ "<li><strong>Capture resolution is limited by display size.</strong> See the Display and Resolution section above for details.</li>",
303
+ "<li><strong>Chrome may drop frames after extended use.</strong> The Chrome encoder can degrade after many hours of continuous operation. PrismCast ",
304
+ "automatically restarts Chrome during idle periods to mitigate this.</li>",
305
+ "</ul>",
306
+ "</div>"
307
+ ].join("\n");
308
+ }
309
+ /**
310
+ * Generates the API Reference tab content with endpoint documentation.
311
+ * @returns HTML content for the API Reference tab.
312
+ */
313
+ export function generateApiReferenceContent() {
314
+ return [
315
+ "<div class=\"section\">",
316
+ "<p>PrismCast provides a RESTful HTTP API for streaming, management, and diagnostics.</p>",
317
+ "<div class=\"api-index\">",
318
+ "<div class=\"api-index-group\">",
319
+ "<a href=\"#api-streaming\" class=\"api-index-heading\">Streaming</a>",
320
+ "<span class=\"api-index-desc\">HLS and MPEG-TS video streams.</span>",
321
+ "<a href=\"#api-streaming\"><code>GET /hls/:name/stream.m3u8</code></a>",
322
+ "<a href=\"#api-streaming\"><code>GET /stream/:name</code></a>",
323
+ "<a href=\"#api-streaming\"><code>GET /play</code></a>",
324
+ "</div>",
325
+ "<div class=\"api-index-group\">",
326
+ "<a href=\"#api-playlist\" class=\"api-index-heading\">Playlist</a>",
327
+ "<span class=\"api-index-desc\">M3U playlist for Channels DVR.</span>",
328
+ "<a href=\"#api-playlist\"><code>GET /playlist</code></a>",
329
+ "</div>",
330
+ "<div class=\"api-index-group\">",
331
+ "<a href=\"#api-channels\" class=\"api-index-heading\">Channels</a>",
332
+ "<span class=\"api-index-desc\">Add, edit, import, and toggle channel definitions.</span>",
333
+ "<a href=\"#api-channels\"><code>POST /config/channels</code></a>",
334
+ "<a href=\"#api-channels\"><code>GET /config/channels/export</code></a>",
335
+ "<a href=\"#api-channels\"><code>POST /config/channels/import</code></a>",
336
+ "</div>",
337
+ "<div class=\"api-index-group\">",
338
+ "<a href=\"#api-providers\" class=\"api-index-heading\">Providers</a>",
339
+ "<span class=\"api-index-desc\">Channel discovery, provider selection, and playlist filtering.</span>",
340
+ "<a href=\"#api-providers\"><code>GET /providers/:slug/channels</code></a>",
341
+ "<a href=\"#api-providers\"><code>POST /config/provider</code></a>",
342
+ "<a href=\"#api-providers\"><code>POST /config/provider-filter</code></a>",
343
+ "</div>",
344
+ "<div class=\"api-index-group\">",
345
+ "<a href=\"#api-auth\" class=\"api-index-heading\">Authentication</a>",
346
+ "<span class=\"api-index-desc\">TV provider login sessions.</span>",
347
+ "<a href=\"#api-auth\"><code>POST /auth/login</code></a>",
348
+ "<a href=\"#api-auth\"><code>POST /auth/done</code></a>",
349
+ "</div>",
350
+ "<div class=\"api-index-group\">",
351
+ "<a href=\"#api-management\" class=\"api-index-heading\">Management</a>",
352
+ "<span class=\"api-index-desc\">List channels, view and control active streams.</span>",
353
+ "<a href=\"#api-management\"><code>GET /channels</code></a>",
354
+ "<a href=\"#api-management\"><code>GET /streams</code></a>",
355
+ "<a href=\"#api-management\"><code>DELETE /streams/:id</code></a>",
356
+ "</div>",
357
+ "<div class=\"api-index-group\">",
358
+ "<a href=\"#api-settings\" class=\"api-index-heading\">Settings</a>",
359
+ "<span class=\"api-index-desc\">Save, export, and import server configuration.</span>",
360
+ "<a href=\"#api-settings\"><code>POST /config</code></a>",
361
+ "<a href=\"#api-settings\"><code>GET /config/export</code></a>",
362
+ "<a href=\"#api-settings\"><code>POST /config/import</code></a>",
363
+ "</div>",
364
+ "<div class=\"api-index-group\">",
365
+ "<a href=\"#api-diagnostics\" class=\"api-index-heading\">Diagnostics</a>",
366
+ "<span class=\"api-index-desc\">Health checks, logs, and real-time monitoring.</span>",
367
+ "<a href=\"#api-diagnostics\"><code>GET /health</code></a>",
368
+ "<a href=\"#api-diagnostics\"><code>GET /logs</code></a>",
369
+ "<a href=\"#api-diagnostics\"><code>GET /logs/stream</code></a>",
370
+ "</div>",
371
+ "</div>",
372
+ "</div>",
373
+ // Streaming endpoints.
374
+ "<div class=\"section\">",
375
+ "<h3 id=\"api-streaming\">Streaming</h3>",
376
+ "<table>",
377
+ "<tr><th style=\"width: 35%;\">Endpoint</th><th>Description</th></tr>",
378
+ "<tr>",
379
+ "<td class=\"endpoint\"><code>GET /hls/:name/stream.m3u8</code></td>",
380
+ "<td>HLS playlist for a named channel. Example: <code>/hls/nbc/stream.m3u8</code></td>",
381
+ "</tr>",
382
+ "<tr>",
383
+ "<td class=\"endpoint\"><code>GET /hls/:name/init.mp4</code></td>",
384
+ "<td>fMP4 initialization segment containing codec configuration.</td>",
385
+ "</tr>",
386
+ "<tr>",
387
+ "<td class=\"endpoint\"><code>GET /hls/:name/:segment.m4s</code></td>",
388
+ "<td>fMP4 media segment containing audio/video data.</td>",
389
+ "</tr>",
390
+ "<tr>",
391
+ "<td class=\"endpoint\"><code>GET /play</code></td>",
392
+ "<td>Stream any URL without creating a channel definition. Pass the URL as <code>?url=&lt;url&gt;</code>. " +
393
+ "Advanced: <code>&amp;profile=</code> overrides auto-detection, <code>&amp;selector=</code> picks a channel on multi-channel sites, " +
394
+ "<code>&amp;clickToPlay=true</code> clicks the video to start playback, <code>&amp;clickSelector=</code> specifies a play button element to click " +
395
+ "(implies clickToPlay).</td>",
396
+ "</tr>",
397
+ "<tr>",
398
+ "<td class=\"endpoint\"><code>GET /stream/:name</code></td>",
399
+ "<td>MPEG-TS stream for HDHomeRun-compatible clients (e.g., Plex). Remuxes fMP4 to MPEG-TS with codec copy.</td>",
400
+ "</tr>",
401
+ "</table>",
402
+ "</div>",
403
+ // Playlist endpoints.
404
+ "<div class=\"section\">",
405
+ "<h3 id=\"api-playlist\">Playlist</h3>",
406
+ "<table>",
407
+ "<tr><th style=\"width: 35%;\">Endpoint</th><th>Description</th></tr>",
408
+ "<tr>",
409
+ "<td class=\"endpoint\"><a href=\"/playlist\"><code>GET /playlist</code></a></td>",
410
+ "<td>M3U playlist of all channels in Channels DVR format. Use this URL when adding PrismCast as a custom channel source. " +
411
+ "Optional query parameters: " +
412
+ "<code>?provider=</code> filters by streaming provider (<code>?provider=yttv</code>, <code>?provider=yttv,sling</code>, " +
413
+ "<code>?provider=-hulu</code>). " +
414
+ "<code>?sort=</code> overrides sort field (<code>name</code>, <code>key</code>, <code>channelNumber</code>, <code>provider</code>, " +
415
+ "<code>profile</code>, <code>stationId</code>, <code>channelSelector</code>). " +
416
+ "<code>?direction=</code> overrides sort direction (<code>asc</code> or <code>desc</code>). " +
417
+ "All parameters are optional and can be combined. " +
418
+ "<strong>Provider filter only controls which channels appear in the playlist, not which provider is used for tuning.</strong></td>",
419
+ "</tr>",
420
+ "</table>",
421
+ "</div>",
422
+ // Channel endpoints.
423
+ "<div class=\"section\">",
424
+ "<h3 id=\"api-channels\">Channels</h3>",
425
+ "<p>Channel definitions, import/export, and predefined channel management.</p>",
426
+ "<table>",
427
+ "<tr><th style=\"width: 35%;\">Endpoint</th><th>Description</th></tr>",
428
+ "<tr>",
429
+ "<td class=\"endpoint\"><code>POST /config/channels</code></td>",
430
+ "<td>Add, edit, delete, or revert user channels. Body includes <code>action</code> (add/edit/delete/revert) and channel data. " +
431
+ "Revert removes a predefined channel override, restoring defaults.</td>",
432
+ "</tr>",
433
+ "<tr>",
434
+ "<td class=\"endpoint\"><a href=\"/config/channels/export\"><code>GET /config/channels/export</code></a></td>",
435
+ "<td>Export user-defined channels as a JSON file download.</td>",
436
+ "</tr>",
437
+ "<tr>",
438
+ "<td class=\"endpoint\"><code>POST /config/channels/import</code></td>",
439
+ "<td>Import channels from JSON, replacing all existing user channels.</td>",
440
+ "</tr>",
441
+ "<tr>",
442
+ "<td class=\"endpoint\"><code>POST /config/channels/import-m3u</code></td>",
443
+ "<td>Import channels from M3U playlist. Body: <code>{ \"content\": \"...\", \"conflictMode\": \"skip\" | \"replace\" }</code></td>",
444
+ "</tr>",
445
+ "<tr>",
446
+ "<td class=\"endpoint\"><code>POST /config/channels/toggle-predefined</code></td>",
447
+ "<td>Enable or disable a single predefined channel. Body: <code>{ \"key\": \"nbc\", \"enabled\": true }</code></td>",
448
+ "</tr>",
449
+ "<tr>",
450
+ "<td class=\"endpoint\"><code>POST /config/channels/bulk-toggle-predefined</code></td>",
451
+ "<td>Enable or disable predefined channels by scope. Body: <code>{ \"enabled\": true, \"scope\": \"all\" | \"pacific\" | \"east\" }</code></td>",
452
+ "</tr>",
453
+ "<tr>",
454
+ "<td class=\"endpoint\"><code>POST /config/channels/display-prefs</code></td>",
455
+ "<td>Update channel table display preferences: sort field, sort direction, and visible optional columns. " +
456
+ "Body: <code>{ \"sortField\": \"name\", \"sortDirection\": \"asc\", \"visibleColumns\": [\"channelNumber\", \"stationId\"] }</code>. " +
457
+ "All fields are optional. Persists to config file.</td>",
458
+ "</tr>",
459
+ "</table>",
460
+ "</div>",
461
+ // Provider endpoints.
462
+ "<div class=\"section\">",
463
+ "<h3 id=\"api-providers\">Providers</h3>",
464
+ "<p>Channel discovery, provider selection, and filtering for multi-provider channels.</p>",
465
+ "<table>",
466
+ "<tr><th style=\"width: 35%;\">Endpoint</th><th>Description</th></tr>",
467
+ "<tr>",
468
+ "<td class=\"endpoint\"><code>GET /providers/:slug/channels</code></td>",
469
+ "<td>Discover all available channels for a provider. Returns a JSON array of channel objects with <code>name</code>, <code>channelSelector</code>, " +
470
+ "and optional <code>affiliate</code> and <code>tier</code> fields. Provider slugs: <code>directv</code>, <code>foxcom</code>, <code>hbomax</code>, <code>hulu</code>, " +
471
+ "<code>sling</code>, <code>yttv</code>. Returns cached results instantly when a prior tune or discovery call has already enumerated the lineup. " +
472
+ "Add <code>?refresh=true</code> to clear caches and force a fresh discovery walk.</td>",
473
+ "</tr>",
474
+ "<tr>",
475
+ "<td class=\"endpoint\"><code>POST /config/provider</code></td>",
476
+ "<td>Update provider selection for a multi-provider channel. Body: <code>{ \"channel\": \"nbc\", \"provider\": \"nbc-hulu\" }</code></td>",
477
+ "</tr>",
478
+ "<tr>",
479
+ "<td class=\"endpoint\"><code>POST /config/provider-filter</code></td>",
480
+ "<td>Set enabled provider tags. Body: <code>{ \"enabledProviders\": [\"hulu\", \"yttv\"] }</code>. Empty array disables filter.</td>",
481
+ "</tr>",
482
+ "<tr>",
483
+ "<td class=\"endpoint\"><code>POST /config/provider-bulk-assign</code></td>",
484
+ "<td>Assign a provider to all multi-provider channels. Body: <code>{ \"provider\": \"hulu\" }</code>. " +
485
+ "Returns <code>{ affected, previousSelections, selections }</code></td>",
486
+ "</tr>",
487
+ "<tr>",
488
+ "<td class=\"endpoint\"><code>POST /config/provider-bulk-restore</code></td>",
489
+ "<td>Restore previous provider selections (undo bulk assign). Body: <code>{ \"selections\": { \"nbc\": \"nbc-hulu\", \"fox\": null } }</code>. " +
490
+ "A <code>null</code> value restores the channel to its default provider.</td>",
491
+ "</tr>",
492
+ "</table>",
493
+ "</div>",
494
+ // Authentication endpoints.
495
+ "<div class=\"section\">",
496
+ "<h3 id=\"api-auth\">Authentication</h3>",
497
+ "<table>",
498
+ "<tr><th style=\"width: 35%;\">Endpoint</th><th>Description</th></tr>",
499
+ "<tr>",
500
+ "<td class=\"endpoint\"><code>POST /auth/login</code></td>",
501
+ "<td>Start login mode for a channel. Body: <code>{ \"channel\": \"name\" }</code> or <code>{ \"url\": \"...\" }</code></td>",
502
+ "</tr>",
503
+ "<tr>",
504
+ "<td class=\"endpoint\"><code>POST /auth/done</code></td>",
505
+ "<td>End login mode and close the login browser tab.</td>",
506
+ "</tr>",
507
+ "<tr>",
508
+ "<td class=\"endpoint\"><a href=\"/auth/status\"><code>GET /auth/status</code></a></td>",
509
+ "<td>Get current login status including whether login mode is active and which channel.</td>",
510
+ "</tr>",
511
+ "</table>",
512
+ "</div>",
513
+ // Management endpoints.
514
+ "<div class=\"section\">",
515
+ "<h3 id=\"api-management\">Management</h3>",
516
+ "<table>",
517
+ "<tr><th style=\"width: 35%;\">Endpoint</th><th>Description</th></tr>",
518
+ "<tr>",
519
+ "<td class=\"endpoint\"><a href=\"/channels\"><code>GET /channels</code></a></td>",
520
+ "<td>List all channels (predefined + user) as JSON with source, enabled status, and channel metadata.</td>",
521
+ "</tr>",
522
+ "<tr>",
523
+ "<td class=\"endpoint\"><a href=\"/streams\"><code>GET /streams</code></a></td>",
524
+ "<td>List all currently active streams with their ID, channel, URL, duration, and status.</td>",
525
+ "</tr>",
526
+ "<tr>",
527
+ "<td class=\"endpoint\"><code>GET /streams/status</code></td>",
528
+ "<td>Server-Sent Events stream for real-time stream and system status updates.</td>",
529
+ "</tr>",
530
+ "<tr>",
531
+ "<td class=\"endpoint\"><code>DELETE /streams/:id</code></td>",
532
+ "<td>Terminate a specific stream by its numeric ID. Returns 200 on success, 404 if not found.</td>",
533
+ "</tr>",
534
+ "</table>",
535
+ "</div>",
536
+ // Settings endpoints.
537
+ "<div class=\"section\">",
538
+ "<h3 id=\"api-settings\">Settings</h3>",
539
+ "<p>Server configuration and backup.</p>",
540
+ "<table>",
541
+ "<tr><th style=\"width: 35%;\">Endpoint</th><th>Description</th></tr>",
542
+ "<tr>",
543
+ "<td class=\"endpoint\"><code>POST /config</code></td>",
544
+ "<td>Save configuration settings. Returns <code>{ success, message, willRestart, deferred, activeStreams }</code></td>",
545
+ "</tr>",
546
+ "<tr>",
547
+ "<td class=\"endpoint\"><a href=\"/config/export\"><code>GET /config/export</code></a></td>",
548
+ "<td>Export current configuration as a JSON file download.</td>",
549
+ "</tr>",
550
+ "<tr>",
551
+ "<td class=\"endpoint\"><code>POST /config/import</code></td>",
552
+ "<td>Import configuration from JSON. Server restarts to apply changes (if running as service).</td>",
553
+ "</tr>",
554
+ "<tr>",
555
+ "<td class=\"endpoint\"><code>POST /config/restart-now</code></td>",
556
+ "<td>Force immediate server restart regardless of active streams. Only works when running as a service.</td>",
557
+ "</tr>",
558
+ "</table>",
559
+ "</div>",
560
+ // Diagnostics endpoints.
561
+ "<div class=\"section\">",
562
+ "<h3 id=\"api-diagnostics\">Diagnostics</h3>",
563
+ "<table>",
564
+ "<tr><th style=\"width: 35%;\">Endpoint</th><th>Description</th></tr>",
565
+ "<tr>",
566
+ "<td class=\"endpoint\"><a href=\"/health\"><code>GET /health</code></a></td>",
567
+ "<td>Health check returning JSON with browser status, memory usage, stream counts, and configuration.</td>",
568
+ "</tr>",
569
+ "<tr>",
570
+ "<td class=\"endpoint\"><a href=\"/logs\"><code>GET /logs</code></a></td>",
571
+ "<td>Recent log entries as JSON. Query params: <code>?lines=N</code> (default 100, max 1000), <code>?level=error|warn|info</code></td>",
572
+ "</tr>",
573
+ "<tr>",
574
+ "<td class=\"endpoint\"><code>GET /logs/stream</code></td>",
575
+ "<td>Server-Sent Events stream for real-time log entries. Query param: <code>?level=error|warn|info</code></td>",
576
+ "</tr>",
577
+ "</table>",
578
+ "</div>",
579
+ // Example responses.
580
+ "<div class=\"section\">",
581
+ "<h3>Example: Health Check Response</h3>",
582
+ "<pre>{",
583
+ " \"browser\": { \"connected\": true, \"pageCount\": 2 },",
584
+ " \"captureMode\": \"ffmpeg\",",
585
+ " \"chrome\": \"Chrome/144.0.7559.110\",",
586
+ " \"clients\": { \"byType\": [{ \"count\": 1, \"type\": \"hls\" }], \"total\": 1 },",
587
+ " \"ffmpegAvailable\": true,",
588
+ " \"memory\": { \"heapTotal\": 120000000, \"heapUsed\": 85000000, \"rss\": 150000000, \"segmentBuffers\": 25000000 },",
589
+ " \"status\": \"healthy\",",
590
+ " \"streams\": { \"active\": 1, \"limit\": 10 },",
591
+ " \"timestamp\": \"2026-01-26T12:00:00.000Z\",",
592
+ " \"uptime\": 3600.5,",
593
+ " \"version\": \"1.0.12\"",
594
+ "}</pre>",
595
+ "</div>"
596
+ ].join("\n");
597
+ }
598
+ /**
599
+ * Generates the Channels tab content. This wraps the channels panel from config.ts and includes the login modal for channel authentication.
600
+ * @returns HTML content for the Channels tab.
601
+ */
602
+ export function generateChannelsTabContent() {
603
+ return [
604
+ // Channels subtab bar. Uses scoped CSS classes to avoid collision with Configuration tab subtabs.
605
+ "<div class=\"channels-subtab-bar\" role=\"tablist\">",
606
+ "<button type=\"button\" class=\"channels-subtab-btn active\" data-channels-subtab=\"channels\" role=\"tab\" aria-selected=\"true\">Channels</button>",
607
+ "<button type=\"button\" class=\"channels-subtab-btn\" data-channels-subtab=\"providers\" role=\"tab\" aria-selected=\"false\">Providers</button>",
608
+ "</div>",
609
+ // Channels subtab panel (default active).
610
+ "<div id=\"channels-subtab-channels\" class=\"channels-subtab-panel active\">",
611
+ "<div class=\"section\">",
612
+ generateChannelsPanel(),
613
+ "</div>",
614
+ "</div>",
615
+ // Providers subtab panel.
616
+ "<div id=\"channels-subtab-providers\" class=\"channels-subtab-panel\">",
617
+ "<div class=\"section\">",
618
+ generateProvidersPanel(),
619
+ "</div>",
620
+ "</div>",
621
+ // Profile builder wizard modal.
622
+ generateWizardModal(),
623
+ // Test flow dialog. Shown when the user starts a profile test to check selectors against a live page.
624
+ "<div id=\"test-modal\" class=\"login-modal\" style=\"display: none;\">",
625
+ "<div class=\"login-modal-content\">",
626
+ "<h3>Profile Test</h3>",
627
+ "<p id=\"test-modal-status\">A Chrome window has been opened on the PrismCast server. Navigate to the site and verify that your profile works.</p>",
628
+ "<div id=\"test-selector-results\" style=\"display: none;\"></div>",
629
+ "<div class=\"login-modal-buttons\" style=\"gap: 8px;\">",
630
+ "<button type=\"button\" class=\"btn btn-secondary\" id=\"test-check-btn\" onclick=\"checkSelectors()\">Check Selectors</button>",
631
+ "<button type=\"button\" class=\"btn btn-primary\" onclick=\"endProfileTest()\">Done</button>",
632
+ "</div>",
633
+ "</div>",
634
+ "</div>",
635
+ // Login modal for channel authentication. Hidden by default, shown when user clicks "Login" on a channel.
636
+ "<div id=\"login-modal\" class=\"login-modal\" style=\"display: none;\">",
637
+ "<div class=\"login-modal-content\">",
638
+ "<h3>Channel Authentication</h3>",
639
+ "<p id=\"login-modal-message\">Complete authentication in the Chrome window on the PrismCast server, then click Done.</p>",
640
+ "<p class=\"login-modal-hint\">A Chrome window has been opened on the machine running PrismCast. ",
641
+ "If PrismCast is running on a remote server or headless system, you'll need screen sharing ",
642
+ "(VNC, Screen Sharing, etc.) to access it. Sign in with your TV provider credentials in that window. ",
643
+ "This login session will automatically close after 15 minutes.</p>",
644
+ "<div class=\"login-modal-buttons\">",
645
+ "<button type=\"button\" class=\"btn btn-primary\" onclick=\"endLogin()\">Done</button>",
646
+ "</div>",
647
+ "</div>",
648
+ "</div>"
649
+ ].join("\n");
650
+ }
651
+ /**
652
+ * Generates the Logs tab content with the log viewer controls and display area. Uses Server-Sent Events for real-time log streaming instead of polling.
653
+ * @returns HTML content for the Logs tab.
654
+ */
655
+ export function generateLogsContent() {
656
+ return [
657
+ "<div class=\"section\">",
658
+ "<div class=\"log-controls\" style=\"display: flex; gap: 15px; align-items: center; margin-bottom: 15px; flex-wrap: wrap;\">",
659
+ "<div>",
660
+ "<label for=\"log-level\" style=\"margin-right: 5px;\">Level:</label>",
661
+ "<select id=\"log-level\" onchange=\"onLevelChange()\">",
662
+ "<option value=\"\">All</option>",
663
+ "<option value=\"error\">Errors</option>",
664
+ "<option value=\"warn\">Warnings</option>",
665
+ "<option value=\"info\">Info</option>",
666
+ "</select>",
667
+ "</div>",
668
+ "<button class=\"btn btn-primary btn-sm\" onclick=\"loadLogs()\">Reload History</button>",
669
+ "<span id=\"sse-status\" style=\"font-size: 13px; margin-left: auto;\"></span>",
670
+ "</div>",
671
+ "</div>",
672
+ "<div id=\"log-container\" class=\"log-viewer\">",
673
+ "<div class=\"log-connecting\">Connecting...</div>",
674
+ "</div>",
675
+ // Log viewer JavaScript with SSE support.
676
+ "<script>",
677
+ "var logContainer = document.getElementById('log-container');",
678
+ "var sseStatus = document.getElementById('sse-status');",
679
+ "var eventSource = null;",
680
+ "var isConsoleMode = false;",
681
+ "var currentLevel = '';",
682
+ // Load historical logs from the /logs endpoint.
683
+ "function loadLogs() {",
684
+ " var level = document.getElementById('log-level').value;",
685
+ " var url = '/logs?lines=500';",
686
+ " if(level) { url += '&level=' + level; }",
687
+ " fetch(url)",
688
+ " .then(function(res) { return res.json(); })",
689
+ " .then(function(data) {",
690
+ " if(data.mode === 'console') {",
691
+ " isConsoleMode = true;",
692
+ " logContainer.innerHTML = '<div class=\"log-warn\">File logging is disabled. Logs are being written to the console.</div>';",
693
+ " return;",
694
+ " }",
695
+ " isConsoleMode = false;",
696
+ " if(data.entries.length === 0) {",
697
+ " logContainer.innerHTML = '<div class=\"log-muted\">No log entries found.</div>';",
698
+ " } else {",
699
+ " renderHistoricalLogs(data.entries);",
700
+ " }",
701
+ " })",
702
+ " .catch(function(err) {",
703
+ " logContainer.innerHTML = '<div class=\"log-error\">Error loading logs: ' + err.message + '</div>';",
704
+ " });",
705
+ "}",
706
+ // Render historical log entries (replaces container content).
707
+ "function renderHistoricalLogs(entries) {",
708
+ " var html = '';",
709
+ " for (var i = 0; i < entries.length; i++) {",
710
+ " html += formatLogEntry(entries[i]);",
711
+ " }",
712
+ " logContainer.innerHTML = html;",
713
+ " logContainer.scrollTop = logContainer.scrollHeight;",
714
+ "}",
715
+ // Format a single log entry as HTML using CSS classes for theme-aware colors.
716
+ "function formatLogEntry(entry) {",
717
+ " var cls = 'log-entry';",
718
+ " if(entry.level === 'error') { cls += ' log-error'; }",
719
+ " else if(entry.level === 'warn') { cls += ' log-warn'; }",
720
+ " else if(entry.level === 'debug') { cls += ' log-debug'; }",
721
+ " var levelBadge = '';",
722
+ " if(entry.level !== 'info') {",
723
+ " var tag = entry.categoryTag ? entry.level.toUpperCase() + ':' + entry.categoryTag : entry.level.toUpperCase();",
724
+ " levelBadge = '[' + escapeHtml(tag) + '] ';",
725
+ " }",
726
+ " return '<div class=\"' + cls + '\">[' + escapeHtml(entry.timestamp) + '] ' + levelBadge + escapeHtml(entry.message) + '</div>';",
727
+ "}",
728
+ // Append a single log entry (for SSE streaming).
729
+ "function appendLogEntry(entry) {",
730
+ " if (isConsoleMode) { return; }",
731
+ " var level = document.getElementById('log-level').value;",
732
+ " if (level && (entry.level !== level)) { return; }",
733
+ " var wasAtBottom = (logContainer.scrollHeight - logContainer.scrollTop - logContainer.clientHeight) < 50;",
734
+ " var entryHtml = formatLogEntry(entry);",
735
+ " logContainer.insertAdjacentHTML('beforeend', entryHtml);",
736
+ " if (wasAtBottom) { logContainer.scrollTop = logContainer.scrollHeight; }",
737
+ "}",
738
+ "function escapeHtml(text) {",
739
+ " var div = document.createElement('div');",
740
+ " div.textContent = text;",
741
+ " return div.innerHTML;",
742
+ "}",
743
+ // Track the last time any SSE event was received from the logs stream. Used by the staleness checker below.
744
+ "var lastLogsEventTime = 0;",
745
+ "var logsStalenessInterval = null;",
746
+ // Connect to the SSE stream.
747
+ "function connectSSE() {",
748
+ " if(eventSource) { eventSource.close(); }",
749
+ " if(logsStalenessInterval) { clearInterval(logsStalenessInterval); }",
750
+ " eventSource = new EventSource('/logs/stream');",
751
+ " lastLogsEventTime = Date.now();",
752
+ " sseStatus.innerHTML = '<span class=\"status-dot\" style=\"color: var(--stream-buffering);\">&#9679;</span> Connecting...';",
753
+ // Same on() wrapper pattern as the status stream. Updates the staleness timestamp on every data event so the 45-second checker stays
754
+ // satisfied as long as any data (heartbeats or log entries) is flowing. Lifecycle handlers (onopen, onerror) stay outside the wrapper.
755
+ " function on(event, handler) {",
756
+ " eventSource.addEventListener(event, function(e) {",
757
+ " lastLogsEventTime = Date.now();",
758
+ " if(handler) { handler(e); }",
759
+ " });",
760
+ " }",
761
+ " on('heartbeat');",
762
+ " on('message', function(e) {",
763
+ " try {",
764
+ " var entry = JSON.parse(e.data);",
765
+ " appendLogEntry(entry);",
766
+ " } catch(err) { /* Ignore parse errors. */ }",
767
+ " });",
768
+ " eventSource.onopen = function() {",
769
+ " lastLogsEventTime = Date.now();",
770
+ " sseStatus.innerHTML = '<span class=\"status-dot\" style=\"color: var(--stream-healthy);\">&#9679;</span> Live';",
771
+ " loadLogs();",
772
+ " };",
773
+ " eventSource.onerror = function() {",
774
+ " sseStatus.innerHTML = '<span class=\"status-dot\" style=\"color: var(--stream-error);\">&#9679;</span> Disconnected';",
775
+ " };",
776
+ " logsStalenessInterval = setInterval(function() {",
777
+ " if((Date.now() - lastLogsEventTime) > 45000) { connectSSE(); }",
778
+ " }, 45000);",
779
+ "}",
780
+ // Disconnect from the SSE stream.
781
+ "function disconnectSSE() {",
782
+ " if(logsStalenessInterval) { clearInterval(logsStalenessInterval); logsStalenessInterval = null; }",
783
+ " if (eventSource) {",
784
+ " eventSource.close();",
785
+ " eventSource = null;",
786
+ " }",
787
+ " sseStatus.innerHTML = '';",
788
+ "}",
789
+ // Handle level filter change (reload history with new filter, SSE filters client-side).
790
+ "function onLevelChange() {",
791
+ " loadLogs();",
792
+ "}",
793
+ // Handle tab activation events for logs SSE connection. The onopen handler calls loadLogs() to ensure history is loaded on both initial
794
+ // connection and reconnection after a disconnect.
795
+ "document.addEventListener('tabactivated', function(e) {",
796
+ " if (e.detail.category === 'logs') {",
797
+ " connectSSE();",
798
+ " } else {",
799
+ " disconnectSSE();",
800
+ " }",
801
+ "});",
802
+ "</script>"
803
+ ].join("\n");
804
+ }
805
+ /**
806
+ * Generates the Configuration tab content with subtabs for channels, settings, advanced, and backup.
807
+ * @returns HTML content for the Configuration tab.
808
+ */
809
+ export function generateConfigContent() {
810
+ const tabs = getUITabs();
811
+ const lines = [];
812
+ // Environment variable warning if applicable.
813
+ if (hasEnvOverrides()) {
814
+ lines.push("<div class=\"warning\">");
815
+ lines.push("<div class=\"warning-title\">Environment Variable Overrides</div>");
816
+ lines.push("Some settings are overridden by environment variables and cannot be changed through this interface. ");
817
+ lines.push("To modify these settings, update your environment variables and restart the server.");
818
+ lines.push("</div>");
819
+ }
820
+ // Subtab bar: Settings tabs plus Backup.
821
+ lines.push("<div class=\"subtab-bar\" role=\"tablist\">");
822
+ let isFirst = true;
823
+ for (const tab of tabs) {
824
+ const activeClass = isFirst ? " active" : "";
825
+ const ariaSelected = isFirst ? "true" : "false";
826
+ lines.push("<button type=\"button\" class=\"subtab-btn" + activeClass + "\" data-subtab=\"" + escapeHtml(tab.id) + "\" role=\"tab\" aria-selected=\"" +
827
+ ariaSelected + "\">" + escapeHtml(tab.displayName) + "</button>");
828
+ isFirst = false;
829
+ }
830
+ lines.push("<button type=\"button\" class=\"subtab-btn\" data-subtab=\"backup\" role=\"tab\" aria-selected=\"false\">Backup</button>");
831
+ lines.push("</div>");
832
+ // Start the settings form (wraps settings and advanced subtabs, not channels or backup).
833
+ lines.push("<form id=\"settings-form\" onsubmit=\"return submitSettingsForm(event)\">");
834
+ // Settings subtab panel with non-collapsible section headers (default active subtab).
835
+ lines.push("<div id=\"subtab-settings\" class=\"subtab-panel active\" role=\"tabpanel\">");
836
+ lines.push(generateSettingsTabContent());
837
+ lines.push("</div>");
838
+ // Advanced subtab panel with collapsible sections.
839
+ lines.push("<div id=\"subtab-advanced\" class=\"subtab-panel\" role=\"tabpanel\">");
840
+ lines.push(generateAdvancedTabContent());
841
+ lines.push("</div>");
842
+ // Settings buttons (hidden on Backup subtab). Button text varies based on whether running as a managed service.
843
+ const saveButtonText = isRunningAsService() ? "Save &amp; Restart" : "Save Settings";
844
+ lines.push("<div id=\"settings-buttons\" class=\"button-row\" style=\"display: flex;\">");
845
+ lines.push("<button type=\"submit\" class=\"btn btn-primary\" id=\"save-btn\">" + saveButtonText + "</button>");
846
+ lines.push("<button type=\"button\" class=\"btn btn-danger\" onclick=\"resetAllToDefaults()\">Reset All to Defaults</button>");
847
+ lines.push("</div>");
848
+ lines.push("</form>");
849
+ // Backup subtab panel (outside form since it doesn't contain settings inputs).
850
+ lines.push("<div id=\"subtab-backup\" class=\"subtab-panel\" role=\"tabpanel\">");
851
+ lines.push(generateBackupPanel());
852
+ lines.push("</div>");
853
+ // Config path display.
854
+ lines.push(generateSettingsFormFooter());
855
+ return lines.join("\n");
856
+ }
857
+ //# sourceMappingURL=content.js.map