@oar-sh/oar 0.9.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/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { DEVICE_ID, IS_SHARED_VIEW, showTransientRelayNotice } from './store.js';
|
|
2
|
+
import { apiFetch } from './api-client.js';
|
|
3
|
+
|
|
4
|
+
// Per-device Web Push settings, following the shape of settings-modal.js:
|
|
5
|
+
// module state + sync functions driven by the DOM ids in index.html.
|
|
6
|
+
|
|
7
|
+
const PUSH_EVENT_CONTROL_IDS = {
|
|
8
|
+
question: 'push-event-question-toggle',
|
|
9
|
+
turnComplete: 'push-event-turn-complete-toggle',
|
|
10
|
+
turnFailed: 'push-event-turn-failed-toggle',
|
|
11
|
+
board: 'push-event-board-toggle',
|
|
12
|
+
cliOffline: 'push-event-cli-offline-toggle',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let pushUpdateInFlight = false;
|
|
16
|
+
let pushDevices = [];
|
|
17
|
+
let currentDeviceRow = null;
|
|
18
|
+
let localSubscriptionEndpoint = '';
|
|
19
|
+
|
|
20
|
+
function pushSupportStatus() {
|
|
21
|
+
if (!window.isSecureContext) {
|
|
22
|
+
return {
|
|
23
|
+
supported: false,
|
|
24
|
+
reason: 'Push requires a secure context. Open the app over HTTPS (or localhost) to enable notifications on this device.',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (!('serviceWorker' in navigator)) {
|
|
28
|
+
return { supported: false, reason: 'This browser does not support service workers, which push notifications require.' };
|
|
29
|
+
}
|
|
30
|
+
if (!('PushManager' in window) || !('Notification' in window)) {
|
|
31
|
+
return { supported: false, reason: 'This browser does not support Web Push notifications.' };
|
|
32
|
+
}
|
|
33
|
+
return { supported: true, reason: '' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function urlBase64ToUint8Array(base64String) {
|
|
37
|
+
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
38
|
+
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
39
|
+
const rawData = atob(base64);
|
|
40
|
+
const outputArray = new Uint8Array(rawData.length);
|
|
41
|
+
for (let i = 0; i < rawData.length; i += 1) {
|
|
42
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
43
|
+
}
|
|
44
|
+
return outputArray;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function defaultDeviceLabel() {
|
|
48
|
+
const ua = String(navigator.userAgent || '');
|
|
49
|
+
const platform = /Android/i.test(ua)
|
|
50
|
+
? 'Android'
|
|
51
|
+
: (/iPhone|iPad/i.test(ua) ? 'iOS' : (/Windows/i.test(ua) ? 'Windows' : (/Mac/i.test(ua) ? 'macOS' : (/Linux/i.test(ua) ? 'Linux' : 'Device'))));
|
|
52
|
+
const browser = /Edg\//i.test(ua)
|
|
53
|
+
? 'Edge'
|
|
54
|
+
: (/Chrome\//i.test(ua) ? 'Chrome' : (/Firefox\//i.test(ua) ? 'Firefox' : (/Safari\//i.test(ua) ? 'Safari' : 'Browser')));
|
|
55
|
+
return `${platform} ${browser}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getPushRegistration() {
|
|
59
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
60
|
+
if (registration) return registration;
|
|
61
|
+
return navigator.serviceWorker.ready;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectPreferencesFromControls({ enabled } = {}) {
|
|
65
|
+
const previewSelect = document.getElementById('push-preview-select');
|
|
66
|
+
const previewChars = Number(document.getElementById('push-preview-chars-input')?.value);
|
|
67
|
+
const events = {};
|
|
68
|
+
for (const [type, id] of Object.entries(PUSH_EVENT_CONTROL_IDS)) {
|
|
69
|
+
const toggle = document.getElementById(id);
|
|
70
|
+
events[type] = toggle instanceof HTMLInputElement ? toggle.checked : true;
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
enabled: enabled !== undefined ? enabled : currentDeviceRow?.preferences?.enabled !== false,
|
|
74
|
+
events,
|
|
75
|
+
content: {
|
|
76
|
+
includeTitle: document.getElementById('push-include-title-toggle')?.checked === true,
|
|
77
|
+
preview: String(previewSelect?.value || 'none'),
|
|
78
|
+
previewChars: Number.isFinite(previewChars) ? previewChars : 80,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function setPushStatus(text, state = '') {
|
|
84
|
+
const status = document.getElementById('push-settings-status');
|
|
85
|
+
if (!status) return;
|
|
86
|
+
status.textContent = text;
|
|
87
|
+
if (state) status.dataset.state = state;
|
|
88
|
+
else delete status.dataset.state;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setPushControlsDisabled(disabled) {
|
|
92
|
+
const ids = [
|
|
93
|
+
'push-include-title-toggle',
|
|
94
|
+
'push-preview-select',
|
|
95
|
+
'push-preview-chars-input',
|
|
96
|
+
...Object.values(PUSH_EVENT_CONTROL_IDS),
|
|
97
|
+
];
|
|
98
|
+
for (const id of ids) {
|
|
99
|
+
const element = document.getElementById(id);
|
|
100
|
+
if (element) element.disabled = disabled;
|
|
101
|
+
}
|
|
102
|
+
const charsInput = document.getElementById('push-preview-chars-input');
|
|
103
|
+
if (charsInput && !disabled) {
|
|
104
|
+
charsInput.disabled = String(document.getElementById('push-preview-select')?.value || 'none') !== 'truncated';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatDeviceTimestamp(value) {
|
|
109
|
+
if (!value) return 'never';
|
|
110
|
+
const date = new Date(value);
|
|
111
|
+
if (Number.isNaN(date.getTime())) return 'unknown';
|
|
112
|
+
return date.toLocaleString();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderDeviceList() {
|
|
116
|
+
const host = document.getElementById('push-device-list');
|
|
117
|
+
if (!host) return;
|
|
118
|
+
host.textContent = '';
|
|
119
|
+
if (!pushDevices.length) {
|
|
120
|
+
const empty = document.createElement('div');
|
|
121
|
+
empty.className = 'settings-help';
|
|
122
|
+
empty.textContent = 'No devices are subscribed yet.';
|
|
123
|
+
host.appendChild(empty);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const device of pushDevices) {
|
|
127
|
+
const isCurrent = device.deviceId === DEVICE_ID
|
|
128
|
+
|| (localSubscriptionEndpoint && device.endpoint === localSubscriptionEndpoint);
|
|
129
|
+
const row = document.createElement('div');
|
|
130
|
+
row.className = 'push-device-row settings-row';
|
|
131
|
+
row.style.gap = '8px';
|
|
132
|
+
|
|
133
|
+
const info = document.createElement('div');
|
|
134
|
+
info.style.flex = '1';
|
|
135
|
+
info.style.minWidth = '0';
|
|
136
|
+
const name = document.createElement('div');
|
|
137
|
+
name.textContent = `${device.label || 'Unnamed device'}${isCurrent ? ' (this device)' : ''}`;
|
|
138
|
+
if (!device.preferences?.enabled) name.textContent += ' — disabled';
|
|
139
|
+
const meta = document.createElement('div');
|
|
140
|
+
meta.className = 'settings-help';
|
|
141
|
+
meta.style.marginTop = '2px';
|
|
142
|
+
meta.textContent = `Last delivery: ${formatDeviceTimestamp(device.lastSuccessAt)}${device.lastError ? ` · last error: ${device.lastError}` : ''}`;
|
|
143
|
+
info.appendChild(name);
|
|
144
|
+
info.appendChild(meta);
|
|
145
|
+
|
|
146
|
+
const renameBtn = document.createElement('button');
|
|
147
|
+
renameBtn.type = 'button';
|
|
148
|
+
renameBtn.className = 'summary-btn';
|
|
149
|
+
renameBtn.textContent = 'Rename';
|
|
150
|
+
renameBtn.addEventListener('click', () => {
|
|
151
|
+
void renamePushDevice(device.id, device.label || '');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const revokeBtn = document.createElement('button');
|
|
155
|
+
revokeBtn.type = 'button';
|
|
156
|
+
revokeBtn.className = 'summary-btn';
|
|
157
|
+
revokeBtn.textContent = 'Revoke';
|
|
158
|
+
revokeBtn.addEventListener('click', () => {
|
|
159
|
+
void revokePushDevice(device.id, isCurrent);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
row.appendChild(info);
|
|
163
|
+
row.appendChild(renameBtn);
|
|
164
|
+
row.appendChild(revokeBtn);
|
|
165
|
+
host.appendChild(row);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function syncPreferenceControls(preferences) {
|
|
170
|
+
const prefs = preferences || {};
|
|
171
|
+
const includeTitle = document.getElementById('push-include-title-toggle');
|
|
172
|
+
if (includeTitle instanceof HTMLInputElement) includeTitle.checked = prefs.content?.includeTitle === true;
|
|
173
|
+
const previewSelect = document.getElementById('push-preview-select');
|
|
174
|
+
if (previewSelect) previewSelect.value = ['none', 'truncated', 'full'].includes(prefs.content?.preview) ? prefs.content.preview : 'none';
|
|
175
|
+
const charsInput = document.getElementById('push-preview-chars-input');
|
|
176
|
+
if (charsInput instanceof HTMLInputElement) {
|
|
177
|
+
charsInput.value = String(Number(prefs.content?.previewChars) || 80);
|
|
178
|
+
charsInput.disabled = previewSelect?.value !== 'truncated';
|
|
179
|
+
}
|
|
180
|
+
for (const [type, id] of Object.entries(PUSH_EVENT_CONTROL_IDS)) {
|
|
181
|
+
const toggle = document.getElementById(id);
|
|
182
|
+
if (toggle instanceof HTMLInputElement) {
|
|
183
|
+
const fallback = type !== 'cliOffline';
|
|
184
|
+
toggle.checked = typeof prefs.events?.[type] === 'boolean' ? prefs.events[type] : fallback;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function loadLocalSubscriptionEndpoint() {
|
|
190
|
+
localSubscriptionEndpoint = '';
|
|
191
|
+
try {
|
|
192
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
193
|
+
const subscription = await registration?.pushManager?.getSubscription();
|
|
194
|
+
localSubscriptionEndpoint = String(subscription?.endpoint || '');
|
|
195
|
+
} catch {
|
|
196
|
+
localSubscriptionEndpoint = '';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveCurrentDeviceRow() {
|
|
201
|
+
currentDeviceRow = pushDevices.find((device) => device.deviceId === DEVICE_ID)
|
|
202
|
+
|| (localSubscriptionEndpoint
|
|
203
|
+
? pushDevices.find((device) => device.endpoint === localSubscriptionEndpoint)
|
|
204
|
+
: null)
|
|
205
|
+
|| null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function refreshPushSettingsSection() {
|
|
209
|
+
const section = document.getElementById('push-settings-section');
|
|
210
|
+
if (!section) return;
|
|
211
|
+
if (IS_SHARED_VIEW) {
|
|
212
|
+
section.hidden = true;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const enableToggle = document.getElementById('push-enabled-toggle');
|
|
216
|
+
const support = pushSupportStatus();
|
|
217
|
+
if (!support.supported) {
|
|
218
|
+
if (enableToggle instanceof HTMLInputElement) {
|
|
219
|
+
enableToggle.checked = false;
|
|
220
|
+
enableToggle.disabled = true;
|
|
221
|
+
}
|
|
222
|
+
setPushControlsDisabled(true);
|
|
223
|
+
setPushStatus(support.reason, 'unconfigured');
|
|
224
|
+
renderDeviceList();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (Notification.permission === 'denied') {
|
|
229
|
+
if (enableToggle instanceof HTMLInputElement) {
|
|
230
|
+
enableToggle.checked = false;
|
|
231
|
+
enableToggle.disabled = true;
|
|
232
|
+
}
|
|
233
|
+
setPushControlsDisabled(true);
|
|
234
|
+
setPushStatus('Notifications are blocked for this site. Re-enable them in the browser\'s site settings, then reopen this dialog.', 'error');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await loadLocalSubscriptionEndpoint();
|
|
238
|
+
const result = await apiFetch('/api/push/devices');
|
|
239
|
+
pushDevices = Array.isArray(result?.devices) ? result.devices : [];
|
|
240
|
+
resolveCurrentDeviceRow();
|
|
241
|
+
renderDeviceList();
|
|
242
|
+
|
|
243
|
+
if (Notification.permission === 'denied') return;
|
|
244
|
+
|
|
245
|
+
const enabledHere = !!currentDeviceRow && currentDeviceRow.preferences?.enabled !== false
|
|
246
|
+
&& !!localSubscriptionEndpoint && currentDeviceRow.endpoint === localSubscriptionEndpoint;
|
|
247
|
+
if (enableToggle instanceof HTMLInputElement) {
|
|
248
|
+
enableToggle.checked = enabledHere;
|
|
249
|
+
enableToggle.disabled = pushUpdateInFlight;
|
|
250
|
+
}
|
|
251
|
+
syncPreferenceControls(currentDeviceRow?.preferences);
|
|
252
|
+
setPushControlsDisabled(!enabledHere || pushUpdateInFlight);
|
|
253
|
+
setPushStatus(
|
|
254
|
+
enabledHere
|
|
255
|
+
? 'Push notifications are enabled on this device. They are only sent while no device has the app open.'
|
|
256
|
+
: 'Enable to get notified about questions and finished turns while the app is in the background. Your browser will ask for notification permission.',
|
|
257
|
+
enabledHere ? 'active' : 'unconfigured',
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function subscribeThisDevice() {
|
|
262
|
+
// The permission prompt fires only from this explicit user action; denial is
|
|
263
|
+
// permanent, so it must never run on page load.
|
|
264
|
+
const permission = await Notification.requestPermission();
|
|
265
|
+
if (permission !== 'granted') {
|
|
266
|
+
throw new Error(permission === 'denied'
|
|
267
|
+
? 'Notification permission was denied. Re-enable it in the browser\'s site settings.'
|
|
268
|
+
: 'Notification permission was not granted.');
|
|
269
|
+
}
|
|
270
|
+
const keyResult = await apiFetch('/api/push/vapid-public-key');
|
|
271
|
+
const publicKey = String(keyResult?.publicKey || '').trim();
|
|
272
|
+
if (!publicKey) throw new Error('The relay did not provide a push key.');
|
|
273
|
+
const registration = await getPushRegistration();
|
|
274
|
+
let subscription = await registration.pushManager.getSubscription();
|
|
275
|
+
if (!subscription) {
|
|
276
|
+
subscription = await registration.pushManager.subscribe({
|
|
277
|
+
userVisibleOnly: true,
|
|
278
|
+
applicationServerKey: urlBase64ToUint8Array(publicKey),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const payload = subscription.toJSON ? subscription.toJSON() : subscription;
|
|
282
|
+
localSubscriptionEndpoint = String(payload?.endpoint || '');
|
|
283
|
+
const preferences = currentDeviceRow?.preferences
|
|
284
|
+
? { ...currentDeviceRow.preferences, enabled: true }
|
|
285
|
+
: collectPreferencesFromControls({ enabled: true });
|
|
286
|
+
const result = await apiFetch('/api/push/subscribe', {
|
|
287
|
+
method: 'POST',
|
|
288
|
+
body: JSON.stringify({
|
|
289
|
+
deviceId: DEVICE_ID,
|
|
290
|
+
label: currentDeviceRow?.label || defaultDeviceLabel(),
|
|
291
|
+
subscription: payload,
|
|
292
|
+
preferences,
|
|
293
|
+
}),
|
|
294
|
+
});
|
|
295
|
+
if (!result?.device) throw new Error('Failed to register the subscription with the relay.');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function togglePushOnThisDevice(enabled) {
|
|
299
|
+
if (pushUpdateInFlight) {
|
|
300
|
+
void refreshPushSettingsSection();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
pushUpdateInFlight = true;
|
|
304
|
+
const enableToggle = document.getElementById('push-enabled-toggle');
|
|
305
|
+
if (enableToggle) enableToggle.disabled = true;
|
|
306
|
+
try {
|
|
307
|
+
if (enabled) {
|
|
308
|
+
await subscribeThisDevice();
|
|
309
|
+
showTransientRelayNotice('Push notifications enabled on this device.');
|
|
310
|
+
} else if (currentDeviceRow) {
|
|
311
|
+
// Keep the subscription row so re-enabling is instant; just stop sending.
|
|
312
|
+
await apiFetch(`/api/push/devices/${encodeURIComponent(currentDeviceRow.id)}`, {
|
|
313
|
+
method: 'PATCH',
|
|
314
|
+
body: JSON.stringify({ preferences: { ...currentDeviceRow.preferences, enabled: false } }),
|
|
315
|
+
});
|
|
316
|
+
showTransientRelayNotice('Push notifications disabled on this device.');
|
|
317
|
+
}
|
|
318
|
+
} catch (error) {
|
|
319
|
+
alert(error?.message || 'Failed to update push notifications.');
|
|
320
|
+
} finally {
|
|
321
|
+
pushUpdateInFlight = false;
|
|
322
|
+
void refreshPushSettingsSection();
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function updatePushPreferencesFromControls() {
|
|
327
|
+
if (!currentDeviceRow || pushUpdateInFlight) return;
|
|
328
|
+
pushUpdateInFlight = true;
|
|
329
|
+
try {
|
|
330
|
+
const preferences = collectPreferencesFromControls();
|
|
331
|
+
const result = await apiFetch(`/api/push/devices/${encodeURIComponent(currentDeviceRow.id)}`, {
|
|
332
|
+
method: 'PATCH',
|
|
333
|
+
body: JSON.stringify({ preferences }),
|
|
334
|
+
});
|
|
335
|
+
if (result?.device) currentDeviceRow = result.device;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
alert(error?.message || 'Failed to save push preferences.');
|
|
338
|
+
} finally {
|
|
339
|
+
pushUpdateInFlight = false;
|
|
340
|
+
void refreshPushSettingsSection();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function renamePushDevice(id, currentLabel) {
|
|
345
|
+
const label = prompt('Device name', currentLabel || '');
|
|
346
|
+
if (label === null) return;
|
|
347
|
+
const result = await apiFetch(`/api/push/devices/${encodeURIComponent(id)}`, {
|
|
348
|
+
method: 'PATCH',
|
|
349
|
+
body: JSON.stringify({ label: String(label).trim() }),
|
|
350
|
+
});
|
|
351
|
+
if (!result?.device) alert('Failed to rename the device.');
|
|
352
|
+
void refreshPushSettingsSection();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function revokePushDevice(id, isCurrent) {
|
|
356
|
+
if (!confirm('Revoke push notifications for this device?')) return;
|
|
357
|
+
const result = await apiFetch(`/api/push/devices/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
358
|
+
if (!result?.ok) {
|
|
359
|
+
alert('Failed to revoke the device.');
|
|
360
|
+
} else if (isCurrent) {
|
|
361
|
+
try {
|
|
362
|
+
const registration = await navigator.serviceWorker.getRegistration();
|
|
363
|
+
const subscription = await registration?.pushManager?.getSubscription();
|
|
364
|
+
await subscription?.unsubscribe();
|
|
365
|
+
} catch {}
|
|
366
|
+
localSubscriptionEndpoint = '';
|
|
367
|
+
showTransientRelayNotice('Push notifications revoked for this device.');
|
|
368
|
+
}
|
|
369
|
+
void refreshPushSettingsSection();
|
|
370
|
+
}
|