prismcast 1.13.0 → 1.14.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.
- package/dist/app.d.ts +88 -6
- package/dist/app.js +287 -154
- package/dist/app.js.map +1 -1
- package/dist/browser/cdp.d.ts +6 -17
- package/dist/browser/cdp.js +6 -57
- package/dist/browser/cdp.js.map +1 -1
- package/dist/browser/index.d.ts +106 -23
- package/dist/browser/index.js +292 -225
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/precaching.d.ts +25 -11
- package/dist/browser/precaching.js +245 -97
- package/dist/browser/precaching.js.map +1 -1
- package/dist/browser/tabCapture.js +3 -10
- package/dist/browser/tabCapture.js.map +1 -1
- package/dist/browser/tuning/comcastPolymer.js +13 -14
- package/dist/browser/tuning/comcastPolymer.js.map +1 -1
- package/dist/browser/tuning/hbo.js +34 -17
- package/dist/browser/tuning/hbo.js.map +1 -1
- package/dist/browser/video.d.ts +1 -1
- package/dist/browser/video.js +3 -3
- package/dist/channels/index.js +5 -5
- package/dist/channels/index.js.map +1 -1
- package/dist/cliHelp.d.ts +11 -0
- package/dist/cliHelp.js +148 -0
- package/dist/cliHelp.js.map +1 -0
- package/dist/config/channelForm.js +5 -4
- package/dist/config/channelForm.js.map +1 -1
- package/dist/config/consistencyProbe.d.ts +2 -4
- package/dist/config/consistencyProbe.js +8 -51
- package/dist/config/consistencyProbe.js.map +1 -1
- package/dist/config/index.d.ts +107 -55
- package/dist/config/index.js +499 -267
- package/dist/config/index.js.map +1 -1
- package/dist/config/paths.d.ts +13 -8
- package/dist/config/paths.js +21 -25
- package/dist/config/paths.js.map +1 -1
- package/dist/config/persistence.d.ts +57 -11
- package/dist/config/persistence.js +127 -44
- package/dist/config/persistence.js.map +1 -1
- package/dist/config/reactivity.d.ts +60 -36
- package/dist/config/reactivity.js +89 -116
- package/dist/config/reactivity.js.map +1 -1
- package/dist/config/services.d.ts +61 -26
- package/dist/config/services.js +137 -68
- package/dist/config/services.js.map +1 -1
- package/dist/config/streamSettings.d.ts +24 -0
- package/dist/config/streamSettings.js +17 -0
- package/dist/config/streamSettings.js.map +1 -0
- package/dist/config/userChannels.d.ts +60 -52
- package/dist/config/userChannels.js +138 -188
- package/dist/config/userChannels.js.map +1 -1
- package/dist/config/userConfig.d.ts +224 -61
- package/dist/config/userConfig.js +351 -134
- package/dist/config/userConfig.js.map +1 -1
- package/dist/hdhr/discover.d.ts +7 -1
- package/dist/hdhr/discover.js +12 -6
- package/dist/hdhr/discover.js.map +1 -1
- package/dist/hdhr/index.d.ts +51 -10
- package/dist/hdhr/index.js +108 -112
- package/dist/hdhr/index.js.map +1 -1
- package/dist/hdhr/udp.d.ts +11 -4
- package/dist/hdhr/udp.js +25 -20
- package/dist/hdhr/udp.js.map +1 -1
- package/dist/index.js +16 -143
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +1 -1
- package/dist/native/index.js +4 -3
- package/dist/native/index.js.map +1 -1
- package/dist/native/proxy.d.ts +1 -1
- package/dist/native/proxy.js +36 -28
- package/dist/native/proxy.js.map +1 -1
- package/dist/routes/cdp.d.ts +7 -6
- package/dist/routes/cdp.js +9 -8
- package/dist/routes/cdp.js.map +1 -1
- package/dist/routes/components.d.ts +23 -5
- package/dist/routes/components.js +21 -4
- package/dist/routes/components.js.map +1 -1
- package/dist/routes/config/channels/endpoints/browse.js +39 -29
- package/dist/routes/config/channels/endpoints/browse.js.map +1 -1
- package/dist/routes/config/channels/endpoints/bulk.js +2 -6
- package/dist/routes/config/channels/endpoints/bulk.js.map +1 -1
- package/dist/routes/config/channels/endpoints/predefined.js +5 -15
- package/dist/routes/config/channels/endpoints/predefined.js.map +1 -1
- package/dist/routes/config/channels/endpoints/prefs.js +7 -6
- package/dist/routes/config/channels/endpoints/prefs.js.map +1 -1
- package/dist/routes/config/channels/endpoints/service.js +6 -5
- package/dist/routes/config/channels/endpoints/service.js.map +1 -1
- package/dist/routes/config/channels/endpoints/tags.js +4 -16
- package/dist/routes/config/channels/endpoints/tags.js.map +1 -1
- package/dist/routes/config/channels/table.d.ts +2 -1
- package/dist/routes/config/channels/table.js +11 -13
- package/dist/routes/config/channels/table.js.map +1 -1
- package/dist/routes/config/http/envelope.d.ts +6 -3
- package/dist/routes/config/http/envelope.js +18 -5
- package/dist/routes/config/http/envelope.js.map +1 -1
- package/dist/routes/config/index.d.ts +21 -13
- package/dist/routes/config/index.js +47 -34
- package/dist/routes/config/index.js.map +1 -1
- package/dist/routes/config/settings.d.ts +43 -6
- package/dist/routes/config/settings.js +225 -125
- package/dist/routes/config/settings.js.map +1 -1
- package/dist/routes/config/vocabulary.d.ts +24 -0
- package/dist/routes/config/vocabulary.js +33 -0
- package/dist/routes/config/vocabulary.js.map +1 -0
- package/dist/routes/debug.js +28 -13
- package/dist/routes/debug.js.map +1 -1
- package/dist/routes/root/content.js +33 -26
- package/dist/routes/root/content.js.map +1 -1
- package/dist/routes/root/scripts/channels.js +23 -11
- package/dist/routes/root/scripts/channels.js.map +1 -1
- package/dist/routes/root/scripts/config.d.ts +1 -1
- package/dist/routes/root/scripts/config.js +127 -79
- package/dist/routes/root/scripts/config.js.map +1 -1
- package/dist/routes/root/scripts/shared.js +34 -23
- package/dist/routes/root/scripts/shared.js.map +1 -1
- package/dist/routes/root/scripts/status.handlers.d.ts +8 -13
- package/dist/routes/root/scripts/status.handlers.js +35 -21
- package/dist/routes/root/scripts/status.handlers.js.map +1 -1
- package/dist/routes/root/styles.js +13 -16
- package/dist/routes/root/styles.js.map +1 -1
- package/dist/routes/services.js +12 -16
- package/dist/routes/services.js.map +1 -1
- package/dist/routes/theme.js +12 -12
- package/dist/routes/theme.js.map +1 -1
- package/dist/routes/ui.d.ts +4 -3
- package/dist/routes/ui.js +17 -13
- package/dist/routes/ui.js.map +1 -1
- package/dist/service/generators.js +12 -9
- package/dist/service/generators.js.map +1 -1
- package/dist/streaming/captureSession.d.ts +4 -4
- package/dist/streaming/captureSession.js +6 -7
- package/dist/streaming/captureSession.js.map +1 -1
- package/dist/streaming/codec.d.ts +12 -2
- package/dist/streaming/codec.js +40 -28
- package/dist/streaming/codec.js.map +1 -1
- package/dist/streaming/fmp4Segmenter.d.ts +14 -4
- package/dist/streaming/fmp4Segmenter.js +99 -79
- package/dist/streaming/fmp4Segmenter.js.map +1 -1
- package/dist/streaming/hls.d.ts +21 -14
- package/dist/streaming/hls.js +148 -122
- package/dist/streaming/hls.js.map +1 -1
- package/dist/streaming/hlsResume.d.ts +36 -13
- package/dist/streaming/hlsResume.js +68 -37
- package/dist/streaming/hlsResume.js.map +1 -1
- package/dist/streaming/monitor.d.ts +5 -3
- package/dist/streaming/monitor.js +47 -37
- package/dist/streaming/monitor.js.map +1 -1
- package/dist/streaming/mp4Parser.d.ts +29 -4
- package/dist/streaming/mp4Parser.js +49 -7
- package/dist/streaming/mp4Parser.js.map +1 -1
- package/dist/streaming/playlistBuilder.d.ts +1 -2
- package/dist/streaming/playlistBuilder.js +2 -3
- package/dist/streaming/playlistBuilder.js.map +1 -1
- package/dist/streaming/preroll.d.ts +43 -16
- package/dist/streaming/preroll.js +112 -62
- package/dist/streaming/preroll.js.map +1 -1
- package/dist/streaming/pretune.d.ts +2 -2
- package/dist/streaming/pretune.js +11 -7
- package/dist/streaming/pretune.js.map +1 -1
- package/dist/streaming/recovery.d.ts +29 -2
- package/dist/streaming/recovery.js +43 -4
- package/dist/streaming/recovery.js.map +1 -1
- package/dist/streaming/registry.d.ts +13 -10
- package/dist/streaming/registry.js +6 -5
- package/dist/streaming/registry.js.map +1 -1
- package/dist/streaming/setup.d.ts +29 -20
- package/dist/streaming/setup.js +181 -170
- package/dist/streaming/setup.js.map +1 -1
- package/dist/streaming/showInfo.d.ts +24 -12
- package/dist/streaming/showInfo.js +120 -113
- package/dist/streaming/showInfo.js.map +1 -1
- package/dist/streaming/statusEmitter.d.ts +15 -10
- package/dist/streaming/statusEmitter.js +24 -20
- package/dist/streaming/statusEmitter.js.map +1 -1
- package/dist/types/config.d.ts +24 -13
- package/dist/types/config.js +5 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.js +2 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/profiles.d.ts +2 -2
- package/dist/types/selection.d.ts +12 -10
- package/dist/types/streaming.d.ts +12 -5
- package/dist/types/streaming.js +12 -5
- package/dist/types/streaming.js.map +1 -1
- package/dist/utils/debugFilter.d.ts +3 -2
- package/dist/utils/debugFilter.js +7 -5
- package/dist/utils/debugFilter.js.map +1 -1
- package/dist/{hdhr → utils}/deviceId.d.ts +1 -1
- package/dist/{hdhr → utils}/deviceId.js +11 -29
- package/dist/utils/deviceId.js.map +1 -0
- package/dist/utils/errors.d.ts +7 -0
- package/dist/utils/errors.js +12 -3
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/fileLogger.d.ts +23 -12
- package/dist/utils/fileLogger.js +73 -41
- package/dist/utils/fileLogger.js.map +1 -1
- package/dist/utils/httpErrors.d.ts +13 -0
- package/dist/utils/httpErrors.js +22 -0
- package/dist/utils/httpErrors.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +9 -8
- package/dist/utils/logger.js +64 -20
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/markup.js +8 -3
- package/dist/utils/markup.js.map +1 -1
- package/dist/utils/plainObject.d.ts +6 -0
- package/dist/utils/plainObject.js +18 -0
- package/dist/utils/plainObject.js.map +1 -0
- package/dist/utils/runtimeIdentity.context.js +1 -29
- package/dist/utils/runtimeIdentity.context.js.map +1 -1
- package/dist/utils/runtimeIdentity.d.ts +17 -6
- package/dist/utils/runtimeIdentity.js +50 -17
- package/dist/utils/runtimeIdentity.js.map +1 -1
- package/package.json +5 -5
- package/dist/hdhr/deviceId.js.map +0 -1
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
*
|
|
3
3
|
* config.ts: Client-side JavaScript generator for the PrismCast configuration subtab.
|
|
4
4
|
*/
|
|
5
|
+
import { PENDING_PATH_ATTRIBUTE, RESTART_SETTINGS_SENTENCE, SAVE_SETTINGS_LABEL } from "../../config/vocabulary.js";
|
|
5
6
|
import { ACTIONS } from "../../clientActions.js";
|
|
7
|
+
import { ICON_FILTER } from "../../icons.js";
|
|
6
8
|
import { VIDEO_QUALITY_PRESETS } from "../../../config/presets.js";
|
|
7
|
-
import { isRunningAsService } from "../../../utils/index.js";
|
|
8
9
|
/**
|
|
9
10
|
* Generates the configuration subtab script block containing the form handling, preset auto-fill, and import/export client-side logic. Functions are exposed on
|
|
10
|
-
* window so
|
|
11
|
+
* window so the action dispatcher's registerAction handlers and the other tab scripts (status.ts, channels.ts) can call them.
|
|
11
12
|
* @returns HTML script block with configuration subtab functions.
|
|
12
13
|
*/
|
|
13
14
|
export function generateConfigSubtabScript() {
|
|
@@ -26,18 +27,18 @@ export function generateConfigSubtabScript() {
|
|
|
26
27
|
].join("\n");
|
|
27
28
|
presetBlocks.push(block);
|
|
28
29
|
}
|
|
29
|
-
// Pass service status to JavaScript for conditional messaging.
|
|
30
|
-
const isService = isRunningAsService();
|
|
31
30
|
return [
|
|
32
31
|
"<script>",
|
|
33
32
|
"(function() {",
|
|
34
|
-
//
|
|
35
|
-
" const
|
|
33
|
+
// The words this script shares with the page, each emitted once through JSON.stringify so no constant carries a quoting rule of its own.
|
|
34
|
+
" const PENDING_PATH_ATTRIBUTE = " + JSON.stringify(PENDING_PATH_ATTRIBUTE) + ";",
|
|
35
|
+
" const RESTART_SETTINGS_SENTENCE = " + JSON.stringify(RESTART_SETTINGS_SENTENCE) + ";",
|
|
36
|
+
" const SAVE_SETTINGS_LABEL = " + JSON.stringify(SAVE_SETTINGS_LABEL) + ";",
|
|
36
37
|
// Preset values for auto-filling bitrate and frame rate.
|
|
37
38
|
" const presetValues = {",
|
|
38
39
|
presetBlocks.join(",\n"),
|
|
39
40
|
" };",
|
|
40
|
-
// When quality preset changes, auto-fill bitrate and frame rate with preset values.
|
|
41
|
+
// When quality preset changes, auto-fill bitrate and frame rate with preset values. This is attached as a change listener on the preset select.
|
|
41
42
|
" function onPresetChange(presetId) {",
|
|
42
43
|
" const values = presetValues[presetId];",
|
|
43
44
|
" if(!values) return;",
|
|
@@ -72,11 +73,14 @@ export function generateConfigSubtabScript() {
|
|
|
72
73
|
" }",
|
|
73
74
|
// Interval handle for restart polling.
|
|
74
75
|
" let restartPollInterval = null;",
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"
|
|
76
|
+
/* The restart dialog's one piece of state: null while no dialog is open, otherwise the settle step of the save or import that opened it. The dialog's
|
|
77
|
+
* cancel, its forced restart and the automatic restart at zero streams each read the step and clear the state before acting, so every way the dialog ends
|
|
78
|
+
* settles as its opener would: a save's in place, an import's by a reload.
|
|
79
|
+
*/
|
|
80
|
+
" let pendingRestart = null;",
|
|
81
|
+
// Show the pending restart dialog when streams are active, holding the opener's settle step for whichever way the dialog ends.
|
|
82
|
+
" function showPendingRestartDialog(streamCount, settle) {",
|
|
83
|
+
" pendingRestart = { settle: settle };",
|
|
80
84
|
" document.getElementById('restart-stream-count').textContent = streamCount;",
|
|
81
85
|
" document.getElementById('restart-dialog').style.display = 'flex';",
|
|
82
86
|
" updateRestartDialogStatus();",
|
|
@@ -93,47 +97,56 @@ export function generateConfigSubtabScript() {
|
|
|
93
97
|
" const count = activeStreamCount;",
|
|
94
98
|
" document.getElementById('restart-stream-count').textContent = count;",
|
|
95
99
|
" if((count === 0) && pendingRestart) {",
|
|
96
|
-
"
|
|
100
|
+
" const settle = pendingRestart.settle;",
|
|
101
|
+
" pendingRestart = null;",
|
|
97
102
|
" document.getElementById('restart-dialog').style.display = 'none';",
|
|
98
|
-
" triggerRestart();",
|
|
103
|
+
" triggerRestart(settle);",
|
|
99
104
|
" }",
|
|
100
105
|
" }",
|
|
101
106
|
" window.updateRestartDialogStatus = updateRestartDialogStatus;",
|
|
102
|
-
// Cancel the pending restart.
|
|
107
|
+
// Cancel the pending restart. The settings marked Restart wait for the next restart, and the opener's settle step carries the sentence that says so.
|
|
103
108
|
" window.cancelPendingRestart = () => {",
|
|
104
|
-
" pendingRestart
|
|
109
|
+
" if(!pendingRestart) return;",
|
|
110
|
+
" const settle = pendingRestart.settle;",
|
|
111
|
+
" pendingRestart = null;",
|
|
105
112
|
" document.getElementById('restart-dialog').style.display = 'none';",
|
|
106
|
-
"
|
|
113
|
+
" settle('Restart cancelled. ' + RESTART_SETTINGS_SENTENCE, 'info');",
|
|
107
114
|
" };",
|
|
108
115
|
// Force immediate restart despite active streams.
|
|
109
116
|
" window.forceRestart = async () => {",
|
|
110
|
-
" pendingRestart
|
|
117
|
+
" if(!pendingRestart) return;",
|
|
118
|
+
" const settle = pendingRestart.settle;",
|
|
119
|
+
" pendingRestart = null;",
|
|
111
120
|
" document.getElementById('restart-dialog').style.display = 'none';",
|
|
112
121
|
" try {",
|
|
113
122
|
" const res = await fetch('/config/restart-now', { method: 'POST' });",
|
|
114
123
|
" if(res.ok) {",
|
|
115
|
-
" waitForServerRestart();",
|
|
124
|
+
" waitForServerRestart(settle);",
|
|
116
125
|
" } else {",
|
|
117
126
|
" const data = await res.json();",
|
|
118
127
|
" throw new Error(data.message || 'Restart failed');",
|
|
119
128
|
" }",
|
|
120
129
|
" } catch(err) {",
|
|
121
|
-
"
|
|
130
|
+
" console.error('Forced restart request failed.', err);",
|
|
131
|
+
" settle('Failed to restart: ' + err.message, 'error');",
|
|
122
132
|
" }",
|
|
123
133
|
" };",
|
|
124
|
-
// Trigger restart (called when streams reach 0).
|
|
125
|
-
" async function triggerRestart() {",
|
|
134
|
+
// Trigger restart (called when streams reach 0). A refused request settles as a failed one does, so the user learns the restart did not happen.
|
|
135
|
+
" async function triggerRestart(settle) {",
|
|
126
136
|
" try {",
|
|
127
137
|
" const res = await fetch('/config/restart-now', { method: 'POST' });",
|
|
128
|
-
" if(res.ok) {",
|
|
129
|
-
"
|
|
138
|
+
" if(!res.ok) {",
|
|
139
|
+
" throw new Error('The restart request was refused.');",
|
|
130
140
|
" }",
|
|
141
|
+
" waitForServerRestart(settle);",
|
|
131
142
|
" } catch(err) {",
|
|
132
|
-
"
|
|
143
|
+
" console.error('Restart request failed.', err);",
|
|
144
|
+
" settle('Failed to trigger restart. Please restart manually.', 'error');",
|
|
133
145
|
" }",
|
|
134
146
|
" }",
|
|
135
|
-
// Wait for server restart by polling /health, then reload.
|
|
136
|
-
|
|
147
|
+
// Wait for server restart by polling /health, then reload. A wait that times out settles through its caller's step, and a wait started while another polls
|
|
148
|
+
// replaces the earlier interval and its step.
|
|
149
|
+
" function waitForServerRestart(settle) {",
|
|
137
150
|
" let attempts = 0;",
|
|
138
151
|
" const maxAttempts = 30;",
|
|
139
152
|
" showToast('Restarting server...', 'info', 0);",
|
|
@@ -151,11 +164,37 @@ export function generateConfigSubtabScript() {
|
|
|
151
164
|
" if(attempts >= maxAttempts) {",
|
|
152
165
|
" clearInterval(restartPollInterval);",
|
|
153
166
|
" restartPollInterval = null;",
|
|
154
|
-
"
|
|
167
|
+
" settle('Server did not restart within 30 seconds. Please check the server manually.', 'error');",
|
|
155
168
|
" }",
|
|
156
169
|
" }",
|
|
157
170
|
" }, 1000);",
|
|
158
171
|
" }",
|
|
172
|
+
/* The server composes every pending-marker sentence and the client only places it. A response carries the whole pending view after the save, so every slot
|
|
173
|
+
* is rewritten from it: a marker the save cleared is emptied and hidden, and nothing the page showed before survives. A save settles in place, because the
|
|
174
|
+
* form already holds what it posted; an import settles by a reload carrying its toast, because it can change every field, the grey-outs and the channel
|
|
175
|
+
* table at once, and a form left showing the pre-import values would post them back on the next save. Green is kept for a save that refused nothing and
|
|
176
|
+
* left nothing waiting, so a blue toast tells the user to look at the markers.
|
|
177
|
+
*/
|
|
178
|
+
" function renderPendingSettings(pending) {",
|
|
179
|
+
" const texts = new Map(pending.map((entry) => [ entry.path, entry.text ]));",
|
|
180
|
+
" for(const slot of document.querySelectorAll('[' + PENDING_PATH_ATTRIBUTE + ']')) {",
|
|
181
|
+
" const text = texts.get(slot.getAttribute(PENDING_PATH_ATTRIBUTE)) ?? '';",
|
|
182
|
+
" slot.textContent = text;",
|
|
183
|
+
" slot.hidden = (text === '');",
|
|
184
|
+
" }",
|
|
185
|
+
" }",
|
|
186
|
+
" function applyConfigurationOutcome(data, options) {",
|
|
187
|
+
" if(data.willRestart) {",
|
|
188
|
+
" if(data.deferred) {",
|
|
189
|
+
" showPendingRestartDialog(data.activeStreams, options.settle);",
|
|
190
|
+
" } else {",
|
|
191
|
+
" waitForServerRestart(options.settle);",
|
|
192
|
+
" }",
|
|
193
|
+
" return;",
|
|
194
|
+
" }",
|
|
195
|
+
" const type = ((data.changes.rejected.length === 0) && (data.pending.length === 0)) ? 'success' : 'info';",
|
|
196
|
+
" options.settle(data.message || options.fallbackMessage, type);",
|
|
197
|
+
" }",
|
|
159
198
|
// Open the changelog modal and fetch content dynamically. Also checks whether an upgrade button should be shown. Changelog items are escaped through the shared
|
|
160
199
|
// window.escapeHtml SSOT installed by the shared utilities script, which loads before this one.
|
|
161
200
|
" window.openChangelogModal = async () => {",
|
|
@@ -189,6 +228,7 @@ export function generateConfigSubtabScript() {
|
|
|
189
228
|
" }",
|
|
190
229
|
" if(data.updateAvailable && upgradeBtn) { upgradeBtn.style.display = ''; }",
|
|
191
230
|
" } catch(err) {",
|
|
231
|
+
" console.error('Changelog request failed.', err);",
|
|
192
232
|
" loading.style.display = 'none';",
|
|
193
233
|
" error.style.display = 'block';",
|
|
194
234
|
" }",
|
|
@@ -225,7 +265,7 @@ export function generateConfigSubtabScript() {
|
|
|
225
265
|
" const upgradeRes = await fetch('/upgrade', { method: 'POST' });",
|
|
226
266
|
" const result = await upgradeRes.json();",
|
|
227
267
|
" if(result.success && result.willRestart) {",
|
|
228
|
-
" waitForServerRestart();",
|
|
268
|
+
" waitForServerRestart(showToast);",
|
|
229
269
|
// A success that did not ask us to poll can still mean the upgrade is unfinished, and the server's message is what tells those outcomes apart. An upgrade
|
|
230
270
|
// handed to a detached helper is still running when the response lands, and it names the helper's log file so the user can follow it; an upgrade that ran
|
|
231
271
|
// here is done. We render whatever the server said, keep a fallback sentence for a response that carries no message, and show the still-running case as an
|
|
@@ -238,6 +278,7 @@ export function generateConfigSubtabScript() {
|
|
|
238
278
|
" showToast('Upgrade failed: ' + result.error, 'error');",
|
|
239
279
|
" }",
|
|
240
280
|
" } catch(err) {",
|
|
281
|
+
" console.error('Upgrade request failed.', err);",
|
|
241
282
|
" showToast('Upgrade failed: ' + err.message, 'error');",
|
|
242
283
|
" }",
|
|
243
284
|
" };",
|
|
@@ -261,6 +302,7 @@ export function generateConfigSubtabScript() {
|
|
|
261
302
|
" setTimeout(() => { btn.classList.remove('up-to-date'); }, 2000);",
|
|
262
303
|
" }",
|
|
263
304
|
" } catch(err) {",
|
|
305
|
+
" console.error('Update check failed.', err);",
|
|
264
306
|
" btn.classList.remove('checking');",
|
|
265
307
|
" btn.classList.add('check-error');",
|
|
266
308
|
" setTimeout(() => { btn.classList.remove('check-error'); }, 2000);",
|
|
@@ -331,13 +373,21 @@ export function generateConfigSubtabScript() {
|
|
|
331
373
|
" }",
|
|
332
374
|
" }",
|
|
333
375
|
" }",
|
|
334
|
-
//
|
|
376
|
+
// A checkbox list's hidden input lists its members in grid order once a checkbox changes and in the stored order before, so order carries no meaning: lists
|
|
377
|
+
// are equal when their sorted members are, the reading the server gives a list setting.
|
|
378
|
+
" function listMembersEqual(a, b) {",
|
|
379
|
+
" const left = JSON.parse(a || '[]').toSorted();",
|
|
380
|
+
" const right = JSON.parse(b || '[]').toSorted();",
|
|
381
|
+
" return (left.length === right.length) && left.every((member, index) => member === right[index]);",
|
|
382
|
+
" }",
|
|
383
|
+
// Update modified indicators for a single input. A checkbox list compares its members with its default's in any order, and every other input compares its
|
|
384
|
+
// value with its default as a string.
|
|
335
385
|
" function updateModifiedIndicator(input) {",
|
|
336
386
|
" const defaultVal = input.getAttribute('data-default');",
|
|
337
387
|
" const currentVal = getInputValue(input);",
|
|
338
388
|
" const formGroup = input.closest('.form-group');",
|
|
339
389
|
" if(!formGroup) return;",
|
|
340
|
-
" const isModified = currentVal !== defaultVal;",
|
|
390
|
+
" const isModified = input.hasAttribute('data-checkbox-list') ? !listMembersEqual(currentVal, defaultVal) : (currentVal !== defaultVal);",
|
|
341
391
|
" const dot = formGroup.querySelector('.modified-dot');",
|
|
342
392
|
" const resetBtn = formGroup.querySelector('.btn-reset');",
|
|
343
393
|
" if(isModified) {",
|
|
@@ -404,7 +454,7 @@ export function generateConfigSubtabScript() {
|
|
|
404
454
|
" updateDependentFields(cbInput.id);",
|
|
405
455
|
" }",
|
|
406
456
|
" }",
|
|
407
|
-
" showToast('Settings reset to defaults. Click ' +
|
|
457
|
+
" showToast('Settings reset to defaults. Click ' + SAVE_SETTINGS_LABEL + ' to apply changes.', 'info');",
|
|
408
458
|
" };",
|
|
409
459
|
// Read the advanced section expansion state from localStorage. Returns the parsed object or {} if missing or corrupted. Single source of truth for
|
|
410
460
|
// reading this key - used by both toggleSection (read-modify-write) and initSections (read-only).
|
|
@@ -449,7 +499,7 @@ export function generateConfigSubtabScript() {
|
|
|
449
499
|
" initSections();",
|
|
450
500
|
// Reset all settings to defaults (client-side only).
|
|
451
501
|
" window.resetAllToDefaults = () => {",
|
|
452
|
-
" if(!confirm('Reset ALL settings to defaults? Click ' +
|
|
502
|
+
" if(!confirm('Reset ALL settings to defaults? Click ' + SAVE_SETTINGS_LABEL + ' after to apply.')) return;",
|
|
453
503
|
" const form = document.getElementById('settings-form');",
|
|
454
504
|
" if(!form) return;",
|
|
455
505
|
" const inputs = form.querySelectorAll('input[data-default], select[data-default]');",
|
|
@@ -465,7 +515,7 @@ export function generateConfigSubtabScript() {
|
|
|
465
515
|
" updateDependentFields(cbInput.id);",
|
|
466
516
|
" }",
|
|
467
517
|
" }",
|
|
468
|
-
" showToast('All settings reset to defaults. Click ' +
|
|
518
|
+
" showToast('All settings reset to defaults. Click ' + SAVE_SETTINGS_LABEL + ' to apply changes.', 'info');",
|
|
469
519
|
" };",
|
|
470
520
|
// Submit settings form via AJAX.
|
|
471
521
|
" window.submitSettingsForm = async (event) => {",
|
|
@@ -497,15 +547,8 @@ export function generateConfigSubtabScript() {
|
|
|
497
547
|
" const data = await res.json();",
|
|
498
548
|
" if(saveBtn) saveBtn.classList.remove('loading');",
|
|
499
549
|
" if(res.ok && data.success) {",
|
|
500
|
-
"
|
|
501
|
-
"
|
|
502
|
-
" showPendingRestartDialog(data.activeStreams);",
|
|
503
|
-
" } else {",
|
|
504
|
-
" waitForServerRestart();",
|
|
505
|
-
" }",
|
|
506
|
-
" } else {",
|
|
507
|
-
" showToast(data.message || 'Configuration saved.', 'info');",
|
|
508
|
-
" }",
|
|
550
|
+
" renderPendingSettings(data.pending);",
|
|
551
|
+
" applyConfigurationOutcome(data, { fallbackMessage: 'Configuration saved.', settle: showToast });",
|
|
509
552
|
" } else if(data.errors) {",
|
|
510
553
|
" displayFieldErrors(data.errors);",
|
|
511
554
|
" showToast('Please correct the errors below.', 'error');",
|
|
@@ -513,6 +556,7 @@ export function generateConfigSubtabScript() {
|
|
|
513
556
|
" showToast(extractErrorMessage(data, 'Failed to save configuration.'), 'error');",
|
|
514
557
|
" }",
|
|
515
558
|
" } catch(err) {",
|
|
559
|
+
" console.error('Settings save request failed.', err);",
|
|
516
560
|
" if(saveBtn) saveBtn.classList.remove('loading');",
|
|
517
561
|
" showToast('Failed to save configuration: ' + err.message, 'error');",
|
|
518
562
|
" }",
|
|
@@ -532,6 +576,7 @@ export function generateConfigSubtabScript() {
|
|
|
532
576
|
" document.body.removeChild(a);",
|
|
533
577
|
" window.URL.revokeObjectURL(url);",
|
|
534
578
|
" } catch(err) {",
|
|
579
|
+
" console.error('Configuration export failed.', err);",
|
|
535
580
|
" showToast('Failed to export configuration: ' + err.message, 'error');",
|
|
536
581
|
" }",
|
|
537
582
|
" };",
|
|
@@ -543,7 +588,7 @@ export function generateConfigSubtabScript() {
|
|
|
543
588
|
" reader.onload = async (e) => {",
|
|
544
589
|
" try {",
|
|
545
590
|
" const config = JSON.parse(e.target.result);",
|
|
546
|
-
" if(confirm('Import this configuration?
|
|
591
|
+
" if(confirm('Import this configuration? ' + RESTART_SETTINGS_SENTENCE)) {",
|
|
547
592
|
" const res = await fetch('/config/import', {",
|
|
548
593
|
" body: JSON.stringify(config),",
|
|
549
594
|
" headers: { 'Content-Type': 'application/json' },",
|
|
@@ -551,21 +596,13 @@ export function generateConfigSubtabScript() {
|
|
|
551
596
|
" });",
|
|
552
597
|
" const data = await res.json();",
|
|
553
598
|
" if(res.ok && data.success) {",
|
|
554
|
-
|
|
555
|
-
// from green success to an informational notice in that case - reserving green for a clean import keeps the positive signal accurate.
|
|
556
|
-
" showToast(data.message || 'Configuration imported.', (data.rejectedCount > 0) ? 'info' : 'success');",
|
|
557
|
-
" if(data.willRestart) {",
|
|
558
|
-
" if(data.deferred) {",
|
|
559
|
-
" showPendingRestartDialog(data.activeStreams);",
|
|
560
|
-
" } else {",
|
|
561
|
-
" waitForServerRestart();",
|
|
562
|
-
" }",
|
|
563
|
-
" }",
|
|
599
|
+
" applyConfigurationOutcome(data, { fallbackMessage: 'Configuration imported.', settle: showToastAfterReload });",
|
|
564
600
|
" } else {",
|
|
565
601
|
" throw new Error(extractErrorMessage(data, 'Import failed'));",
|
|
566
602
|
" }",
|
|
567
603
|
" }",
|
|
568
604
|
" } catch(err) {",
|
|
605
|
+
" console.error('Configuration import failed.', err);",
|
|
569
606
|
" showToast('Failed to import configuration: ' + err.message, 'error');",
|
|
570
607
|
" }",
|
|
571
608
|
" fileInput.value = '';",
|
|
@@ -586,6 +623,7 @@ export function generateConfigSubtabScript() {
|
|
|
586
623
|
" document.body.removeChild(a);",
|
|
587
624
|
" window.URL.revokeObjectURL(url);",
|
|
588
625
|
" } catch(err) {",
|
|
626
|
+
" console.error('Channel export failed.', err);",
|
|
589
627
|
" showToast('Failed to export channels: ' + err.message, 'error');",
|
|
590
628
|
" }",
|
|
591
629
|
" };",
|
|
@@ -612,6 +650,7 @@ export function generateConfigSubtabScript() {
|
|
|
612
650
|
" }",
|
|
613
651
|
" }",
|
|
614
652
|
" } catch(err) {",
|
|
653
|
+
" console.error('Channel import failed.', err);",
|
|
615
654
|
" showToast('Failed to import channels: ' + err.message, 'error');",
|
|
616
655
|
" }",
|
|
617
656
|
" fileInput.value = '';",
|
|
@@ -792,9 +831,10 @@ export function generateConfigSubtabScript() {
|
|
|
792
831
|
" ref.style.display = ref.style.display === 'none' ? 'block' : 'none';",
|
|
793
832
|
" }",
|
|
794
833
|
" };",
|
|
795
|
-
// Inline cell editing for channel number and station ID. Clicking an editable cell replaces its content with
|
|
796
|
-
// record at /config/channels/:key with a typed partial update. Escape cancels and
|
|
797
|
-
// name, channel key, and current value so
|
|
834
|
+
// Inline cell editing for channel number and station ID. Clicking an editable cell replaces its content with an input, a number input for the channel number
|
|
835
|
+
// and a text input for the station ID. Enter or blur PATCHes the channel record at /config/channels/:key with a typed partial update. Escape cancels and
|
|
836
|
+
// restores the original value. The cell's data attributes carry the field name, channel key, and current value so one handler serves the channel number and
|
|
837
|
+
// station ID cells alike.
|
|
798
838
|
" window.startInlineEdit = (td) => {",
|
|
799
839
|
" if(td.querySelector('input.inline-edit')) return;",
|
|
800
840
|
" const field = td.getAttribute('data-field');",
|
|
@@ -815,7 +855,7 @@ export function generateConfigSubtabScript() {
|
|
|
815
855
|
" saving = true;",
|
|
816
856
|
" const newValue = input.value.trim();",
|
|
817
857
|
" if(newValue === value) { cancel(); return; }",
|
|
818
|
-
// Build a typed partial-update body. channelNumber is parsed as an integer
|
|
858
|
+
// Build a typed partial-update body. channelNumber is parsed as an integer, and an empty stationId or channelNumber is sent as null to clear the field.
|
|
819
859
|
" const body = {};",
|
|
820
860
|
" if(field === 'channelNumber') body.channelNumber = newValue ? parseInt(newValue, 10) : null;",
|
|
821
861
|
" else if(field === 'stationId') body.stationId = newValue || null;",
|
|
@@ -863,6 +903,7 @@ export function generateConfigSubtabScript() {
|
|
|
863
903
|
" showToast(extractErrorMessage(d, 'Failed to update HDHR setting.'), 'error');",
|
|
864
904
|
" }",
|
|
865
905
|
" } catch(err) {",
|
|
906
|
+
" console.error('HDHR setting update failed.', err);",
|
|
866
907
|
" checkbox.checked = !checkbox.checked;",
|
|
867
908
|
" showToast('Failed to update HDHR setting: ' + err.message, 'error');",
|
|
868
909
|
" }",
|
|
@@ -934,6 +975,7 @@ export function generateConfigSubtabScript() {
|
|
|
934
975
|
" if(result.success) {",
|
|
935
976
|
" showToast('Channel ' + key + ' ' + (enable ? 'enabled' : 'disabled') + '.' + PLAYLIST_HINT, 'success');",
|
|
936
977
|
" if(result.patch) { channelTable.applyPatch(result.patch); }",
|
|
978
|
+
" if(result.persistenceWarning) { showToast(result.persistenceWarning, 'warning'); }",
|
|
937
979
|
" } else {",
|
|
938
980
|
" showToast(extractErrorMessage(result, 'Failed to toggle channel.'), 'error');",
|
|
939
981
|
" }",
|
|
@@ -977,6 +1019,7 @@ export function generateConfigSubtabScript() {
|
|
|
977
1019
|
" const labels = { all: 'All predefined', pacific: 'Pacific predefined', east: 'East predefined' };",
|
|
978
1020
|
" showToast((labels[scope] || 'Predefined') + ' channels ' + (enable ? 'enabled' : 'disabled') + '.' + PLAYLIST_HINT, 'success');",
|
|
979
1021
|
" if(result.patch) { channelTable.applyPatch(result.patch); }",
|
|
1022
|
+
" if(result.persistenceWarning) { showToast(result.persistenceWarning, 'warning'); }",
|
|
980
1023
|
" } else {",
|
|
981
1024
|
" showToast(extractErrorMessage(result, 'Failed to toggle channels.'), 'error');",
|
|
982
1025
|
" }",
|
|
@@ -986,7 +1029,9 @@ export function generateConfigSubtabScript() {
|
|
|
986
1029
|
" }",
|
|
987
1030
|
" };",
|
|
988
1031
|
// Auto-number visible channels sequentially based on the current sort order. Reads the starting number from the input in the Quick Actions dropdown. When
|
|
989
|
-
// the input is empty, clears all channel numbers from visible channels instead of assigning them.
|
|
1032
|
+
// the input is empty, clears all channel numbers from visible channels instead of assigning them. "Visible" here and in the HDHR and tag bulk actions below
|
|
1033
|
+
// means the server's set - channels that are enabled and available under the service filter - because these requests send no row list. The client-side tag
|
|
1034
|
+
// column filter does not narrow that set, so rows it hides are still numbered, toggled or tagged.
|
|
990
1035
|
" window.autoNumberChannels = async () => {",
|
|
991
1036
|
" const startInput = document.getElementById('auto-number-start');",
|
|
992
1037
|
" const raw = startInput ? startInput.value.trim() : '1';",
|
|
@@ -1073,6 +1118,8 @@ export function generateConfigSubtabScript() {
|
|
|
1073
1118
|
" if(!tagPortal) { tagPortal = document.getElementById('inline-tag-portal'); }",
|
|
1074
1119
|
" return tagPortal;",
|
|
1075
1120
|
" }",
|
|
1121
|
+
// We save the inline tag dropdown's edits in this before-close hook. Every checkbox toggle is batched into a single PATCH when the dropdown closes,
|
|
1122
|
+
// and comparing the sorted tag list against the sorted list captured at open skips the request when the toggles net out to no change.
|
|
1076
1123
|
" async function saveTagDropdownIfChanged() {",
|
|
1077
1124
|
" if(!activeTagDropdown) return;",
|
|
1078
1125
|
" const portal = getTagPortal();",
|
|
@@ -1145,6 +1192,7 @@ export function generateConfigSubtabScript() {
|
|
|
1145
1192
|
" showToast(extractErrorMessage(d, 'Failed to revert channel.'), 'error');",
|
|
1146
1193
|
" }",
|
|
1147
1194
|
" } catch(err) {",
|
|
1195
|
+
" console.error('Channel revert request failed.', err);",
|
|
1148
1196
|
" showToast('Failed to revert channel: ' + err.message, 'error');",
|
|
1149
1197
|
" }",
|
|
1150
1198
|
" };",
|
|
@@ -1164,6 +1212,7 @@ export function generateConfigSubtabScript() {
|
|
|
1164
1212
|
" updateServiceFilterButton(enabledTags);",
|
|
1165
1213
|
" if(result.patch) { channelTable.applyPatch(result.patch); }",
|
|
1166
1214
|
" showToast('Service filter updated.' + PLAYLIST_HINT, 'success');",
|
|
1215
|
+
" if(result.persistenceWarning) { showToast(result.persistenceWarning, 'warning'); }",
|
|
1167
1216
|
" }",
|
|
1168
1217
|
" } catch(err) {",
|
|
1169
1218
|
" console.error('Service filter update failed.', err);",
|
|
@@ -1202,9 +1251,8 @@ export function generateConfigSubtabScript() {
|
|
|
1202
1251
|
" }",
|
|
1203
1252
|
" saveServiceFilter(enabledTags);",
|
|
1204
1253
|
" };",
|
|
1205
|
-
// Update the service filter button text
|
|
1206
|
-
" const filterIcon =
|
|
1207
|
-
"stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M1 2h14l-5 6v5l-4 2V8z\"/></svg>';",
|
|
1254
|
+
// Update the service filter button text, led by the server's own filter icon.
|
|
1255
|
+
" const filterIcon = " + JSON.stringify(ICON_FILTER) + ";",
|
|
1208
1256
|
" function updateServiceFilterButton(enabledTags) {",
|
|
1209
1257
|
" const btn = document.getElementById('provider-filter-btn');",
|
|
1210
1258
|
" if(!btn) return;",
|
|
@@ -1247,17 +1295,10 @@ export function generateConfigSubtabScript() {
|
|
|
1247
1295
|
" }",
|
|
1248
1296
|
" processServiceDisplays();",
|
|
1249
1297
|
" };",
|
|
1250
|
-
// Update bulk assign options in the Quick Actions select to only show enabled services
|
|
1298
|
+
// Update bulk assign options in the Quick Actions select to only show enabled services, through the option filter the row selects share.
|
|
1251
1299
|
" function updateBulkAssignOptions(enabledTags) {",
|
|
1252
1300
|
" const select = document.getElementById('bulk-assign-select');",
|
|
1253
|
-
" if(
|
|
1254
|
-
" const options = select.querySelectorAll('option[data-provider-tag]');",
|
|
1255
|
-
" for(const option of options) {",
|
|
1256
|
-
" const tag = option.getAttribute('data-provider-tag');",
|
|
1257
|
-
" const show = (enabledTags.length === 0) || (tag === 'direct') || enabledTags.includes(tag);",
|
|
1258
|
-
" if(show) option.removeAttribute('hidden');",
|
|
1259
|
-
" else option.setAttribute('hidden', '');",
|
|
1260
|
-
" }",
|
|
1301
|
+
" if(select) channelTable.filterSelectOptions(select, enabledTags);",
|
|
1261
1302
|
" };",
|
|
1262
1303
|
// Bulk assign all channels to a specific service. The response carries the channel-table patch that re-renders every reassigned row, along with the snapshot
|
|
1263
1304
|
// of prior selections that backs the undo action on the toast.
|
|
@@ -1283,6 +1324,7 @@ export function generateConfigSubtabScript() {
|
|
|
1283
1324
|
" showToast(extractErrorMessage(result, 'Failed to assign.'), 'error');",
|
|
1284
1325
|
" }",
|
|
1285
1326
|
" } catch(err) {",
|
|
1327
|
+
" console.error('Bulk service assignment failed.', err);",
|
|
1286
1328
|
" showToast('Failed to assign: ' + err.message, 'error');",
|
|
1287
1329
|
" }",
|
|
1288
1330
|
" };",
|
|
@@ -1302,6 +1344,7 @@ export function generateConfigSubtabScript() {
|
|
|
1302
1344
|
" showToast(extractErrorMessage(result, 'Failed to revert.'), 'error');",
|
|
1303
1345
|
" }",
|
|
1304
1346
|
" } catch(err) {",
|
|
1347
|
+
" console.error('Bulk service restore failed.', err);",
|
|
1305
1348
|
" showToast('Failed to revert: ' + err.message, 'error');",
|
|
1306
1349
|
" }",
|
|
1307
1350
|
" }",
|
|
@@ -1477,7 +1520,7 @@ export function generateConfigSubtabScript() {
|
|
|
1477
1520
|
" const tagToggles = document.querySelectorAll('.tag-bulk-toggle[data-indeterminate=\"true\"]');",
|
|
1478
1521
|
" for(const tagToggle of tagToggles) tagToggle.indeterminate = true;",
|
|
1479
1522
|
// Run channelTable.filter() on page load when a service filter is active. The server renders filtered options with the hidden attribute, but Safari ignores it on
|
|
1480
|
-
// option elements. This initial pass removes those options from the
|
|
1523
|
+
// option elements. This initial pass removes those options from the channel rows' selects and from the bulk assign select to enforce the filter.
|
|
1481
1524
|
" const initFilterTags = channelTable.getEnabledFilterTags();",
|
|
1482
1525
|
" if(initFilterTags.length > 0) { channelTable.filter(initFilterTags); updateBulkAssignOptions(initFilterTags); }",
|
|
1483
1526
|
" const addUrlInput = document.getElementById('add-url');",
|
|
@@ -1557,6 +1600,7 @@ export function generateConfigSubtabScript() {
|
|
|
1557
1600
|
" showToast(extractErrorMessage(result, 'Failed to start login.'), 'error');",
|
|
1558
1601
|
" }",
|
|
1559
1602
|
" } catch(err) {",
|
|
1603
|
+
" console.error('Channel login request failed.', err);",
|
|
1560
1604
|
" showToast('Failed to start login: ' + err.message, 'error');",
|
|
1561
1605
|
" }",
|
|
1562
1606
|
" };",
|
|
@@ -1568,6 +1612,7 @@ export function generateConfigSubtabScript() {
|
|
|
1568
1612
|
" hideLoginModal();",
|
|
1569
1613
|
" showToast('Authentication complete.', 'success');",
|
|
1570
1614
|
" } catch(err) {",
|
|
1615
|
+
" console.error('Login end request failed.', err);",
|
|
1571
1616
|
" showToast('Error ending login: ' + err.message, 'error');",
|
|
1572
1617
|
" hideLoginModal();",
|
|
1573
1618
|
" }",
|
|
@@ -1582,8 +1627,9 @@ export function generateConfigSubtabScript() {
|
|
|
1582
1627
|
" const modal = document.getElementById('login-modal');",
|
|
1583
1628
|
" if(modal) modal.style.display = 'none';",
|
|
1584
1629
|
" }",
|
|
1585
|
-
// Start polling login status to detect when tab is closed externally. Errors are logged only on the first failure
|
|
1586
|
-
// would otherwise spam the console at 1Hz.
|
|
1630
|
+
// Start polling login status to detect when tab is closed externally. Errors are logged only on the first failure of each run of consecutive failures - a
|
|
1631
|
+
// dead endpoint would otherwise spam the console at 1Hz. Both a successful poll and a new startLoginStatusPolling() call reset the flag, so the next failure
|
|
1632
|
+
// after either is logged again.
|
|
1587
1633
|
" function startLoginStatusPolling() {",
|
|
1588
1634
|
" stopLoginStatusPolling();",
|
|
1589
1635
|
" let errorLogged = false;",
|
|
@@ -1631,8 +1677,8 @@ export function generateConfigSubtabScript() {
|
|
|
1631
1677
|
" presetSelect.addEventListener('change', function() { onPresetChange(this.value); });",
|
|
1632
1678
|
" }",
|
|
1633
1679
|
" }",
|
|
1634
|
-
// Checkbox list: collect checked values into the hidden input and update the modified indicator. Called
|
|
1635
|
-
// checkbox-list-grid. The hidden input holds the JSON-encoded array and participates in the standard form submission.
|
|
1680
|
+
// Checkbox list: collect checked values into the hidden input and update the modified indicator. Called through the data-change-action dispatcher for each
|
|
1681
|
+
// checkbox within a checkbox-list-grid. The hidden input holds the JSON-encoded array and participates in the standard form submission.
|
|
1636
1682
|
" window.updateCheckboxList = (checkbox) => {",
|
|
1637
1683
|
" const group = checkbox.closest('.form-group');",
|
|
1638
1684
|
" if(!group) return;",
|
|
@@ -1657,9 +1703,11 @@ export function generateConfigSubtabScript() {
|
|
|
1657
1703
|
" switchFn: switchSubtab",
|
|
1658
1704
|
" });",
|
|
1659
1705
|
/* Action registrations. Each binds an ACTIONS name to a window-exposed handler defined above; the project-wide dispatcher in shared.ts looks up the handler
|
|
1660
|
-
* by name when a click / change / keydown / submit event lands on a matching [data-<event>-action] element.
|
|
1661
|
-
*
|
|
1662
|
-
* data-<event>-
|
|
1706
|
+
* by name when a click / change / keydown / submit event lands on a matching [data-<event>-action] element. The event mechanics a trigger always needs
|
|
1707
|
+
* (default prevention and dropdown close) live declaratively on the trigger element via the event-type-scoped data-<event>-prevent-default and
|
|
1708
|
+
* data-<event>-close-dropdown attributes that the dispatcher processes before the handler runs. A mechanic that depends on the event stays in its handler...the
|
|
1709
|
+
* service picker closes the dropdown only once a service is chosen. The form submit functions also call preventDefault themselves, repeating what their
|
|
1710
|
+
* forms' data-submit-prevent-default already does.
|
|
1663
1711
|
*/
|
|
1664
1712
|
" window.registerAction('" + ACTIONS.autoNumberChannels + "', () => autoNumberChannels());",
|
|
1665
1713
|
" window.registerAction('" + ACTIONS.bulkAssignService + "', (target) => {",
|