@songsid/agend 2.3.0-beta.2 → 2.3.0-beta.20
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/README.md +7 -1
- package/README.zh-TW.md +7 -1
- package/dist/agent-cli-instructions.md +9 -2
- package/dist/agent-cli.js +63 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +4 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +4 -1
- package/dist/agent-file-read.js +189 -16
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-whats-new.d.ts +11 -0
- package/dist/agent-whats-new.js +29 -5
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +30 -1
- package/dist/auth-api.js +45 -13
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +38 -0
- package/dist/authority/authority-state.js +114 -0
- package/dist/authority/authority-state.js.map +1 -0
- package/dist/authority/session-authorizations.d.ts +43 -0
- package/dist/authority/session-authorizations.js +102 -0
- package/dist/authority/session-authorizations.js.map +1 -0
- package/dist/authority/web-authority.d.ts +120 -0
- package/dist/authority/web-authority.js +157 -0
- package/dist/authority/web-authority.js.map +1 -0
- package/dist/backend/claude-code.d.ts +2 -2
- package/dist/backend/claude-code.js +18 -8
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +8 -0
- package/dist/backend/codex-metadata.js +74 -17
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.js +15 -2
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +2 -0
- package/dist/backend/credential-profile.js +2 -0
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js +15 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +120 -0
- package/dist/backend-availability.js +396 -0
- package/dist/backend-availability.js.map +1 -0
- package/dist/backend-command.d.ts +23 -3
- package/dist/backend-command.js +25 -4
- package/dist/backend-command.js.map +1 -1
- package/dist/bug-report-cli.d.ts +30 -0
- package/dist/bug-report-cli.js +138 -0
- package/dist/bug-report-cli.js.map +1 -0
- package/dist/bug-report.d.ts +47 -0
- package/dist/bug-report.js +136 -0
- package/dist/bug-report.js.map +1 -0
- package/dist/bundled-skills.d.ts +14 -0
- package/dist/bundled-skills.js +29 -0
- package/dist/bundled-skills.js.map +1 -0
- package/dist/cache-ledger.d.ts +16 -3
- package/dist/cache-ledger.js +33 -13
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +55 -3
- package/dist/cache-service.js +152 -16
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord-client-retirement.d.ts +5 -0
- package/dist/channel/adapters/discord-client-retirement.js +57 -0
- package/dist/channel/adapters/discord-client-retirement.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +9 -0
- package/dist/channel/adapters/discord.js +49 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +22 -11
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +6 -0
- package/dist/classic-channel-manager.d.ts +10 -5
- package/dist/classic-channel-manager.js +12 -9
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +224 -111
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +6 -0
- package/dist/config-validator.js +129 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +13 -1
- package/dist/config.js +14 -5
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +7 -0
- package/dist/cross-instance-notice.js +10 -0
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +24 -1
- package/dist/daemon.js +151 -19
- package/dist/daemon.js.map +1 -1
- package/dist/decision-list.d.ts +25 -0
- package/dist/decision-list.js +30 -0
- package/dist/decision-list.js.map +1 -0
- package/dist/delivery-outbox.d.ts +10 -0
- package/dist/delivery-outbox.js +17 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/diagnostic-redaction.d.ts +10 -0
- package/dist/diagnostic-redaction.js +167 -0
- package/dist/diagnostic-redaction.js.map +1 -0
- package/dist/fleet-context.d.ts +10 -0
- package/dist/fleet-manager.d.ts +505 -15
- package/dist/fleet-manager.js +1870 -204
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +18 -0
- package/dist/fleet-process-handlers.js +69 -0
- package/dist/fleet-process-handlers.js.map +1 -0
- package/dist/fleet-traffic.d.ts +74 -0
- package/dist/fleet-traffic.js +196 -0
- package/dist/fleet-traffic.js.map +1 -0
- package/dist/general-knowledge/skills/bug-report/SKILL.md +31 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +17 -6
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +6 -1
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +2 -0
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +3 -2
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +1 -1
- package/dist/general-knowledge/skills/scheduling/SKILL.md +4 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +190 -47
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +545 -0
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/human-delivery-receipts.d.ts +24 -1
- package/dist/human-delivery-receipts.js +64 -11
- package/dist/human-delivery-receipts.js.map +1 -1
- package/dist/instance-config-impact.js +7 -1
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -1
- package/dist/instance-lifecycle.js +79 -18
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-names.d.ts +4 -0
- package/dist/instance-names.js +12 -0
- package/dist/instance-names.js.map +1 -0
- package/dist/instructions.d.ts +12 -1
- package/dist/instructions.js +13 -2
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +173 -12
- package/dist/locale.js.map +1 -1
- package/dist/log-steps.d.ts +1 -0
- package/dist/log-steps.js +15 -0
- package/dist/log-steps.js.map +1 -1
- package/dist/login-controller.d.ts +45 -0
- package/dist/login-controller.js +101 -5
- package/dist/login-controller.js.map +1 -1
- package/dist/ls-attach-resolve.d.ts +96 -0
- package/dist/ls-attach-resolve.js +180 -0
- package/dist/ls-attach-resolve.js.map +1 -0
- package/dist/ls-rows.d.ts +12 -0
- package/dist/ls-rows.js +82 -3
- package/dist/ls-rows.js.map +1 -1
- package/dist/memory-hogs.d.ts +27 -0
- package/dist/memory-hogs.js +124 -0
- package/dist/memory-hogs.js.map +1 -0
- package/dist/needs-you.d.ts +10 -0
- package/dist/needs-you.js +2 -1
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +11 -0
- package/dist/outbound-handlers.js +139 -11
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +200 -4
- package/dist/outbound-schemas.js +124 -11
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +17 -4
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +9 -0
- package/dist/public-web-link.js +8 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +19 -1
- package/dist/quickstart-api.js +94 -4
- package/dist/quickstart-api.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +4 -0
- package/dist/reply-buttons-controller.js +2 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/restart-progress.d.ts +80 -1
- package/dist/restart-progress.js +306 -21
- package/dist/restart-progress.js.map +1 -1
- package/dist/rollout-index.d.ts +5 -0
- package/dist/rollout-index.js +26 -5
- package/dist/rollout-index.js.map +1 -1
- package/dist/scheduler/db.d.ts +40 -13
- package/dist/scheduler/db.js +123 -89
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/task-board-text.d.ts +26 -0
- package/dist/scheduler/task-board-text.js +135 -0
- package/dist/scheduler/task-board-text.js.map +1 -0
- package/dist/scheduler/task-board.d.ts +650 -0
- package/dist/scheduler/task-board.js +1938 -0
- package/dist/scheduler/task-board.js.map +1 -0
- package/dist/scheduler/types.d.ts +6 -1
- package/dist/search/search-docs.d.ts +58 -0
- package/dist/search/search-docs.js +63 -0
- package/dist/search/search-docs.js.map +1 -0
- package/dist/search/search-index.d.ts +141 -0
- package/dist/search/search-index.js +440 -0
- package/dist/search/search-index.js.map +1 -0
- package/dist/search/search-store.d.ts +122 -0
- package/dist/search/search-store.js +236 -0
- package/dist/search/search-store.js.map +1 -0
- package/dist/search/search-worker.d.ts +1 -0
- package/dist/search/search-worker.js +163 -0
- package/dist/search/search-worker.js.map +1 -0
- package/dist/service-installer.js +12 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.d.ts +23 -0
- package/dist/settings-api.js +164 -3
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +104 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -1
- package/dist/settings-confirmation.js +2 -0
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +34 -3
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +11 -0
- package/dist/settings-http-confirmation.js +10 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +23 -7
- package/dist/setup-form.js.map +1 -1
- package/dist/spawn-gate.d.ts +1 -0
- package/dist/spawn-gate.js +13 -3
- package/dist/spawn-gate.js.map +1 -1
- package/dist/task-board-tool.d.ts +25 -0
- package/dist/task-board-tool.js +154 -0
- package/dist/task-board-tool.js.map +1 -0
- package/dist/task-board-waker.d.ts +44 -0
- package/dist/task-board-waker.js +128 -0
- package/dist/task-board-waker.js.map +1 -0
- package/dist/tmux-exec-helper.d.ts +94 -0
- package/dist/tmux-exec-helper.js +407 -0
- package/dist/tmux-exec-helper.js.map +1 -0
- package/dist/tmux-manager.d.ts +5 -0
- package/dist/tmux-manager.js +42 -11
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +0 -7
- package/dist/tool-permissions.js +44 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +17 -1
- package/dist/topic-commands.js +45 -19
- package/dist/topic-commands.js.map +1 -1
- package/dist/tunnel/cloudflared.js +37 -2
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/types.d.ts +3 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/types.d.ts +63 -1
- package/dist/ui/app-effects.js +90 -32
- package/dist/ui/backend-signin.js +297 -0
- package/dist/ui/chat-ambient.js +307 -0
- package/dist/ui/chat-effect-posters.js +8 -8
- package/dist/ui/chat-effects.js +110 -15
- package/dist/ui/chat-msg-actions.js +176 -0
- package/dist/ui/chat-render.js +21 -3
- package/dist/ui/chat-store.js +64 -12
- package/dist/ui/chat-strings.js +85 -8
- package/dist/ui/chat-thread.js +204 -21
- package/dist/ui/effect-trial-controller.js +13 -8
- package/dist/ui/effect-trial-panel.js +19 -16
- package/dist/ui/effects-waker.js +70 -0
- package/dist/ui/fleet-canvas-layout.js +117 -0
- package/dist/ui/fleet-canvas.js +555 -0
- package/dist/ui/fleet-org.js +27 -12
- package/dist/ui/fleet-route-scene.js +207 -0
- package/dist/ui/fleet-route-worker.js +21 -0
- package/dist/ui/fleet-route.js +88 -0
- package/dist/ui/fleet-task-card.js +436 -0
- package/dist/ui/fleet-task-common.js +448 -0
- package/dist/ui/fleet-tasks.js +535 -0
- package/dist/ui/fleet-traffic.js +359 -0
- package/dist/ui/fleet-usage.js +435 -0
- package/dist/ui/panel-chat.js +264 -66
- package/dist/ui/panel-details.js +2 -1
- package/dist/ui/panel-fleet.js +65 -83
- package/dist/ui/panel-needs.js +4 -2
- package/dist/ui/panel-settings.js +56 -11
- package/dist/ui/preview.js +3 -2
- package/dist/ui/search-dialog.js +349 -0
- package/dist/ui/settings-appearance.js +44 -19
- package/dist/ui/settings-apply.js +3 -3
- package/dist/ui/settings-catalog.js +92 -0
- package/dist/ui/settings-confirm.js +7 -2
- package/dist/ui/settings-dialogs.js +40 -12
- package/dist/ui/settings-effects.js +4 -3
- package/dist/ui/settings-guests.js +117 -0
- package/dist/ui/settings-learn.js +25 -0
- package/dist/ui/settings-model.js +15 -6
- package/dist/ui/settings-mods.js +23 -9
- package/dist/ui/settings-owner-effects.js +15 -13
- package/dist/ui/settings-strings.js +122 -42
- package/dist/ui/settings-web-authority.js +91 -0
- package/dist/ui/shared/agend-auth.js +1 -1
- package/dist/ui/shared/app-i18n.js +11 -2
- package/dist/ui/shared/app-prefs.js +17 -4
- package/dist/ui/shared/app-route.js +19 -1
- package/dist/ui/shared/app-session.js +2 -0
- package/dist/ui/shared/app-shell.js +174 -30
- package/dist/ui/shared/app-store.js +26 -1
- package/dist/ui/shared/app-stream.js +5 -1
- package/dist/ui/shared/app-themes.js +140 -7
- package/dist/ui/shared/app.css +704 -20
- package/dist/ui/shared/app.js +26 -15
- package/dist/ui/shared/card-strings.js +8 -0
- package/dist/ui/shared/effect-app-runtime.js +68 -19
- package/dist/ui/shared/effect-chat-runtime.js +16 -3
- package/dist/ui/shared/effect-frame.js +26 -7
- package/dist/ui/shared/effect-geometry.js +24 -9
- package/dist/ui/shared/effect-owner-source.js +8 -1
- package/dist/ui/shared/effect-policy.js +21 -7
- package/dist/ui/shared/effect-profiler.js +31 -9
- package/dist/ui/shared/effect-registry.js +97 -50
- package/dist/ui/shared/effect-stock-snapshot.js +9 -0
- package/dist/ui/shared/effect-stock-trust.js +62 -10
- package/dist/ui/shared/effect-trial-shadow.js +62 -0
- package/dist/ui/shared/effect-trial-source.js +1 -1
- package/dist/ui/shared/effect-trial-stage.js +54 -31
- package/dist/ui/shared/effect-trial.css +40 -0
- package/dist/ui/shared/fx-motion.js +67 -0
- package/dist/ui/shared/identity-card.js +100 -11
- package/dist/ui/shared/identity-color.js +14 -10
- package/dist/ui/shared/instance-nav.js +179 -23
- package/dist/ui/shared/owner-css.js +30 -1
- package/dist/ui/shared/panel-view.js +24 -4
- package/dist/ui/shared/tokens.css +9 -3
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/ui-menu.js +20 -7
- package/dist/ui/shared/ui-toast.js +16 -3
- package/dist/ui/shared/usage-dialog.js +34 -3
- package/dist/ui/shared/view-strings.js +72 -0
- package/dist/ui/shared/web-authorize.js +198 -0
- package/dist/ui/web-terminal/terminal.css +4 -4
- package/dist/ui/web-terminal/terminal.html +1 -1
- package/dist/update-launch.d.ts +9 -0
- package/dist/update-launch.js +38 -12
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/format-rich.js +6 -3
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +2 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/i18n.d.ts +13 -1
- package/dist/usage/i18n.js +29 -0
- package/dist/usage/i18n.js.map +1 -1
- package/dist/usage/providers.d.ts +81 -2
- package/dist/usage/providers.js +240 -38
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +5 -1
- package/dist/usage/usage-api.js +184 -58
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +158 -0
- package/dist/usage-report.js +226 -0
- package/dist/usage-report.js.map +1 -0
- package/dist/usage-rollup.d.ts +51 -0
- package/dist/usage-rollup.js +168 -0
- package/dist/usage-rollup.js.map +1 -0
- package/dist/user-systemd.d.ts +24 -0
- package/dist/user-systemd.js +75 -0
- package/dist/user-systemd.js.map +1 -0
- package/dist/view-api.d.ts +39 -0
- package/dist/view-api.js +69 -6
- package/dist/view-api.js.map +1 -1
- package/dist/web-ambient.d.ts +72 -0
- package/dist/web-ambient.js +161 -0
- package/dist/web-ambient.js.map +1 -0
- package/dist/web-api.d.ts +98 -14
- package/dist/web-api.js +1027 -83
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-policy.d.ts +4 -6
- package/dist/web-appearance-policy.js +5 -12
- package/dist/web-appearance-policy.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +3 -0
- package/dist/web-appearance-tools.js +22 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +27 -2
- package/dist/web-appearance-view.js +129 -14
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +17 -3
- package/dist/web-appearance.js +85 -8
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +2 -2
- package/dist/web-asset-parts.js +3 -3
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +3 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-backend-login.d.ts +130 -0
- package/dist/web-backend-login.js +225 -0
- package/dist/web-backend-login.js.map +1 -0
- package/dist/web-card.d.ts +14 -0
- package/dist/web-card.js +40 -6
- package/dist/web-card.js.map +1 -1
- package/dist/web-channel-echo.d.ts +7 -1
- package/dist/web-channel-echo.js +6 -2
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +83 -0
- package/dist/web-chat-history.js +137 -5
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +6 -1
- package/dist/web-commands.js +1 -1
- package/dist/web-commands.js.map +1 -1
- package/dist/web-effect-bootstrap.d.ts +12 -0
- package/dist/web-effect-bootstrap.js +49 -0
- package/dist/web-effect-bootstrap.js.map +1 -0
- package/dist/web-effect-bundle.js +46 -1
- package/dist/web-effect-bundle.js.map +1 -1
- package/dist/web-effect-playback.d.ts +30 -0
- package/dist/web-effect-playback.js +71 -0
- package/dist/web-effect-playback.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +4 -4
- package/dist/web-effect-service.d.ts +11 -4
- package/dist/web-effect-service.js +25 -4
- package/dist/web-effect-service.js.map +1 -1
- package/dist/web-effect-tools.d.ts +1 -0
- package/dist/web-effect-tools.js +4 -4
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-trials.d.ts +2 -2
- package/dist/web-effect-trials.js +3 -3
- package/dist/web-effect-trials.js.map +1 -1
- package/dist/web-effect-upload.js +1 -1
- package/dist/web-effect-upload.js.map +1 -1
- package/dist/web-effect-worker.d.ts +1 -1
- package/dist/web-effect-worker.js +2 -2
- package/dist/web-effect-worker.js.map +1 -1
- package/dist/web-effect.d.ts +1 -1
- package/dist/web-guest-access.d.ts +126 -0
- package/dist/web-guest-access.js +238 -0
- package/dist/web-guest-access.js.map +1 -0
- package/dist/web-guest-rooms.d.ts +82 -0
- package/dist/web-guest-rooms.js +135 -0
- package/dist/web-guest-rooms.js.map +1 -0
- package/dist/web-host-guard.d.ts +11 -0
- package/dist/web-host-guard.js +11 -0
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-identity-color.d.ts +1 -1
- package/dist/web-identity-color.js +2 -2
- package/dist/web-identity-color.js.map +1 -1
- package/dist/web-identity-palette.d.ts +1 -1
- package/dist/web-identity-palette.js +2 -2
- package/dist/web-identity-palette.js.map +1 -1
- package/dist/web-image-meta.d.ts +13 -0
- package/dist/web-image-meta.js +62 -0
- package/dist/web-image-meta.js.map +1 -1
- package/dist/web-libavoid.d.ts +14 -0
- package/dist/web-libavoid.js +46 -0
- package/dist/web-libavoid.js.map +1 -0
- package/dist/web-login.d.ts +47 -0
- package/dist/web-login.js +115 -0
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +1 -1
- package/dist/web-mod-agent.js +7 -5
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +9 -1
- package/dist/web-mod-bundle.js +62 -3
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-export.d.ts +9 -0
- package/dist/web-mod-export.js +21 -0
- package/dist/web-mod-export.js.map +1 -1
- package/dist/web-mod-store.d.ts +7 -1
- package/dist/web-mod-store.js +19 -6
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +44 -1
- package/dist/web-owner-css-store.js +70 -9
- package/dist/web-owner-css-store.js.map +1 -1
- package/dist/web-prefs.d.ts +14 -0
- package/dist/web-prefs.js +37 -4
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +6 -2
- package/dist/web-preview.js +4 -2
- package/dist/web-preview.js.map +1 -1
- package/dist/web-returning-device.d.ts +41 -0
- package/dist/web-returning-device.js +57 -5
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-session-authorization.d.ts +132 -0
- package/dist/web-session-authorization.js +193 -0
- package/dist/web-session-authorization.js.map +1 -0
- package/dist/web-session.d.ts +70 -10
- package/dist/web-session.js +137 -22
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +49 -2
- package/dist/web-settings-patch.js +145 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +94 -0
- package/dist/web-settings-tools.js +301 -0
- package/dist/web-settings-tools.js.map +1 -0
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-terminal-http.js +15 -2
- package/dist/web-terminal-http.js.map +1 -1
- package/dist/web-theme-store.d.ts +39 -0
- package/dist/web-theme-store.js +122 -11
- package/dist/web-theme-store.js.map +1 -1
- package/dist/web-theme.d.ts +31 -0
- package/dist/web-theme.js +99 -2
- package/dist/web-theme.js.map +1 -1
- package/dist/web-upload.d.ts +13 -0
- package/dist/web-upload.js +39 -3
- package/dist/web-upload.js.map +1 -1
- package/dist/windows-shortcuts.d.ts +78 -0
- package/dist/windows-shortcuts.js +203 -0
- package/dist/windows-shortcuts.js.map +1 -0
- package/dist/workflow-templates/default.md +4 -1
- package/dist/yaml-shared-nodes.d.ts +25 -0
- package/dist/yaml-shared-nodes.js +185 -0
- package/dist/yaml-shared-nodes.js.map +1 -0
- package/launcher/agend-install.cjs +553 -0
- package/launcher/install-admission.cjs +4 -39
- package/launcher/postinstall.cjs +14 -12
- package/package.json +4 -2
- package/scripts/preinstall-guard.cjs +2 -5
- package/templates/windows-shortcuts.ps1 +181 -0
- package/launcher/old-updater-note.cjs +0 -48
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
// #1918: Fleet → Org chart as a canvas — the same structure as the list (fleet-org.js: General, the teams fleet.yaml
|
|
2
|
+
// defines, their members, the instances in no team), drawn as cards in team boxes you can pan, zoom and pinch. No
|
|
3
|
+
// library: SVG with attributes and classes (never a style attribute, #1300); the view's transform is an SVG attribute
|
|
4
|
+
// set on one group, so panning re-renders nothing.
|
|
5
|
+
//
|
|
6
|
+
// - Layout: fixed slots, no layout engine. General on top; teams in fleet.yaml's order, packed into rows that wrap at
|
|
7
|
+
// a width; members in a two- or three-column grid in their box. Each team's slot is sized as if expanded, so folding
|
|
8
|
+
// a team or searching never moves another one. Tree lines: a stem from General, a bus over each row, a trunk at the
|
|
9
|
+
// left joining the rows, one drop per team — never a line per member (the box says who is in it). A phone packs
|
|
10
|
+
// narrower (one team a row, at most two columns), so General and the first team open readable.
|
|
11
|
+
// - Per device (localStorage; blocked storage: this page only): Canvas or List, and which teams are folded. The view
|
|
12
|
+
// itself is not kept: it opens on General and the first row (#1918's user decision 2) — on a desktop as wide as the
|
|
13
|
+
// panel allows (no larger than life), on a phone no smaller than readable. 0 / Fit shows the whole fleet.
|
|
14
|
+
// - Presentation only, like the list: a card opens that instance's chat; nothing is edited here (#1689 is how teams
|
|
15
|
+
// change). The live state is the app store's (fleet-org.js buildOrgView), so the canvas moves with the stream.
|
|
16
|
+
// - Live traffic (fleet-traffic.js): the messages between instances, drawn between the team boxes and the cards (never
|
|
17
|
+
// hidden under a box, never across a name). Live on or off per device; the list says it in words ("Sending: …").
|
|
18
|
+
// #2182: routed around the cards and through the boxes' doors by libavoid in a worker (fleet-route-scene.js), each
|
|
19
|
+
// line from card to card; the lanes are the fallback.
|
|
20
|
+
import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "/assets/app-html.js";
|
|
21
|
+
import { appStore } from "/assets/app-store.js";
|
|
22
|
+
import { t, register } from "/assets/app-i18n.js";
|
|
23
|
+
import { chatPath } from "/assets/app-route.js";
|
|
24
|
+
import { navigate } from "/assets/app-nav.js";
|
|
25
|
+
import { Empty, Skeleton } from "/assets/ui-states.js";
|
|
26
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
27
|
+
import { avatarOf, backendLabel, firstCharacter, namesOf } from "/assets/instance-nav.js";
|
|
28
|
+
import { identityClasses, identityOf, paintIdentity, useIdentityColors } from "/assets/identity-color.js";
|
|
29
|
+
import { buildOrgView, OrgChart, OrgSummary } from "./fleet-org.js";
|
|
30
|
+
import { OC, REST_KEY, SUM_LINE, canvasLayout, openingRegion, summaryRows } from "./fleet-canvas-layout.js";
|
|
31
|
+
import { createTrafficStore, FAR_K, POLL_MS, sendingBy, trafficEdges } from "./fleet-traffic.js";
|
|
32
|
+
import { routeScene } from "./fleet-route-scene.js";
|
|
33
|
+
import { createRouteClient } from "./fleet-route.js";
|
|
34
|
+
|
|
35
|
+
register("orgc", {
|
|
36
|
+
en: {
|
|
37
|
+
view: "View", canvas: "Canvas", list: "List", find: "Find an instance", findShort: "Find", found: "{0} found", foundOne: "1 found", noMatch: "No match",
|
|
38
|
+
zoomIn: "Zoom in", zoomOut: "Zoom out", fit: "Fit to screen",
|
|
39
|
+
label: "Org chart canvas. Tab or the arrow keys move between instances, Enter opens a chat, + and − zoom, 0 fits the fleet.",
|
|
40
|
+
expand: "Expand", collapse: "Collapse", members: "{0} members", member: "1 member", inFolded: "{0} found · expand", noMembers: "No members",
|
|
41
|
+
openChat: "Open chat", minimap: "Overview: click to move there",
|
|
42
|
+
live: "Live", liveOn: "Hide live traffic", liveOff: "Show live traffic", lgTask: "delegate_task", lgSend: "send_to_instance",
|
|
43
|
+
lgReport: "report_result", lgBroadcast: "broadcast", lgQueued: "queued", lgFading: "delivered", lgTeam: "between teams", legend: "Key",
|
|
44
|
+
lgWork: "in progress (until its report)", lgReturn: "done (report back)", lgStale: "no report yet",
|
|
45
|
+
},
|
|
46
|
+
"zh-TW": {
|
|
47
|
+
view: "檢視", canvas: "畫布", list: "清單", find: "尋找 instance", findShort: "尋找", found: "找到 {0} 個", foundOne: "找到 1 個", noMatch: "沒有符合的",
|
|
48
|
+
zoomIn: "放大", zoomOut: "縮小", fit: "符合畫面",
|
|
49
|
+
label: "組織圖畫布。用 Tab 或方向鍵在 instance 之間移動,Enter 開啟聊天,+ 和 − 縮放,0 顯示整個 fleet。",
|
|
50
|
+
expand: "展開", collapse: "收合", members: "{0} 名成員", member: "1 名成員", inFolded: "找到 {0} 個 · 展開", noMembers: "沒有成員",
|
|
51
|
+
openChat: "開啟聊天", minimap: "總覽:點一下移到那裡",
|
|
52
|
+
live: "即時", liveOn: "隱藏即時往來", liveOff: "顯示即時往來", lgTask: "delegate_task", lgSend: "send_to_instance",
|
|
53
|
+
lgReport: "report_result", lgBroadcast: "broadcast", lgQueued: "排隊中", lgFading: "已送達", lgTeam: "team 之間", legend: "圖例",
|
|
54
|
+
lgWork: "進行中(直到回報)", lgReturn: "完成(回報送回)", lgStale: "尚未回報",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const tc = (k, ...v) => t(`orgc.${k}`, ...v);
|
|
58
|
+
|
|
59
|
+
// ── Layout: fleet-canvas-layout.js (pure; the traffic overlay routes through the same lanes) ──
|
|
60
|
+
|
|
61
|
+
export { OC, REST_KEY, SUM_LINE, canvasLayout, openingRegion, summaryRows };
|
|
62
|
+
|
|
63
|
+
// ── The view: pan, zoom, fit (pure helpers; the component applies them) ──
|
|
64
|
+
|
|
65
|
+
export const K_MIN = 0.2, K_MAX = 2.5;
|
|
66
|
+
const clampK = (k) => Math.min(K_MAX, Math.max(K_MIN, k));
|
|
67
|
+
/**
|
|
68
|
+
* The view that shows region `r` in a `size` viewport: as large as fits (at most `maxK`), centred across, from the top.
|
|
69
|
+
* `minK`: never smaller than this (a phone's opening); a region that is then wider than the viewport starts at its left.
|
|
70
|
+
* `widthOnly`: fit the width, let the height run on (the opening: the rest is below).
|
|
71
|
+
*/
|
|
72
|
+
export function fitView(r, size, { margin = 24, minK = K_MIN, maxK = 1.25, widthOnly = false } = {}) {
|
|
73
|
+
const across = (size.w - 2 * margin) / r.w, down = widthOnly ? Infinity : (size.h - 2 * margin) / r.h;
|
|
74
|
+
const k = clampK(Math.max(minK, Math.min(across, down, maxK)));
|
|
75
|
+
const x = r.w * k > size.w - 2 * margin ? margin - r.x * k : size.w / 2 - (r.x + r.w / 2) * k;
|
|
76
|
+
return { k, x, y: margin - r.y * k };
|
|
77
|
+
}
|
|
78
|
+
/** A pinch step: the canvas point under the old midpoint goes to the new midpoint, the zoom scaled by the spans' ratio. */
|
|
79
|
+
export function pinchView(v, from, to) {
|
|
80
|
+
const k = clampK(v.k * (to.dist / from.dist));
|
|
81
|
+
const wx = (from.mid.x - v.x) / v.k, wy = (from.mid.y - v.y) / v.k;
|
|
82
|
+
return { k, x: to.mid.x - wx * k, y: to.mid.y - wy * k };
|
|
83
|
+
}
|
|
84
|
+
/** Zoom by `f` about the viewport point (cx, cy): the canvas point under it stays under it. */
|
|
85
|
+
export function zoomAt(v, f, cx, cy) {
|
|
86
|
+
const k = clampK(v.k * f);
|
|
87
|
+
return { k, x: cx - (cx - v.x) * (k / v.k), y: cy - (cy - v.y) * (k / v.k) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Per device ──
|
|
91
|
+
|
|
92
|
+
const MODE_KEY = "agend_org_mode", FOLD_KEY = "agend_org_folded", LIVE_KEY = "agend_org_live", KEY_KEY = "agend_org_key";
|
|
93
|
+
function readMode() { try { return localStorage.getItem(MODE_KEY) === "list" ? "list" : "canvas"; } catch { return "canvas"; } }
|
|
94
|
+
function readLive() { try { return localStorage.getItem(LIVE_KEY) !== "0"; } catch { return true; } }
|
|
95
|
+
function readFolded() {
|
|
96
|
+
try { const a = JSON.parse(localStorage.getItem(FOLD_KEY) || "[]"); return new Set(Array.isArray(a) ? a.filter((x) => typeof x === "string").slice(0, 500) : []); } catch { return new Set(); }
|
|
97
|
+
}
|
|
98
|
+
function save(key, value) { try { localStorage.setItem(key, value); } catch { /* this page only */ } }
|
|
99
|
+
|
|
100
|
+
// ── Drawing ──
|
|
101
|
+
|
|
102
|
+
const STATE_LABEL = (s) => t(`org.${s}`);
|
|
103
|
+
// #2047: SVG text does not wrap or ellipsize, so names are shortened before they are drawn. A budget counts Latin-width
|
|
104
|
+
// characters: a CJK or fullwidth character draws about 1.6 of them. A grapheme (an emoji sequence, a letter with its
|
|
105
|
+
// marks) is never split; an emoji sequence a font cannot join is drawn as its parts, so each pictograph counts 2.
|
|
106
|
+
const WIDE = /[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6\u{20000}-\u{3fffd}]/u;
|
|
107
|
+
const PICTO = /\p{Extended_Pictographic}|\p{Regional_Indicator}/u;
|
|
108
|
+
const charWidth = (g) => {
|
|
109
|
+
const cps = Array.from(g), pictos = cps.filter((c) => PICTO.test(c)).length;
|
|
110
|
+
return pictos ? 2 * pictos : cps.some((c) => WIDE.test(c)) ? 1.6 : 1;
|
|
111
|
+
};
|
|
112
|
+
const SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
113
|
+
export function clipText(s, n) {
|
|
114
|
+
const chars = Array.from(SEGMENTER.segment(String(s)), (x) => x.segment);
|
|
115
|
+
if (chars.reduce((w, g) => w + charWidth(g), 0) <= n) return chars.join("");
|
|
116
|
+
let used = 1, out = ""; // the ellipsis takes one
|
|
117
|
+
for (const g of chars) { if (used + charWidth(g) > n) break; used += charWidth(g); out += g; }
|
|
118
|
+
return `${out}…`;
|
|
119
|
+
}
|
|
120
|
+
const clip = clipText;
|
|
121
|
+
const needle = (q) => String(q || "").trim().toLowerCase();
|
|
122
|
+
const hits = (node, q) => !!q && (node.name.toLowerCase().includes(q) || (!!node.i && typeof node.i.display_name === "string" && node.i.display_name.toLowerCase().includes(q)));
|
|
123
|
+
|
|
124
|
+
function Face({ it, name, cx, cy, r }) {
|
|
125
|
+
const a = it ? avatarOf(it) : { kind: "initial", text: firstCharacter(name) };
|
|
126
|
+
const [broken, setBroken] = useState("");
|
|
127
|
+
const src = a.kind !== "initial" && a.src && broken !== a.src ? a.src : "";
|
|
128
|
+
const text = a.kind === "emoji" && a.text ? a.text : a.kind === "initial" ? a.text : firstCharacter(it ? namesOf(it).primary : name);
|
|
129
|
+
return html`<g class=${`oc-face${a.kind === "emoji" ? " emoji" : ""}`} aria-hidden="true">
|
|
130
|
+
<circle class="oc-av" cx=${cx} cy=${cy} r=${r} />
|
|
131
|
+
${src ? html`<image href=${src} x=${cx - r} y=${cy - r} width=${r * 2} height=${r * 2} preserveAspectRatio="xMidYMid slice"
|
|
132
|
+
clip-path=${a.kind === "image" ? "url(#oc-clip-av)" : undefined} onError=${() => setBroken(src)} />`
|
|
133
|
+
: html`<text class="oc-av-t" x=${cx} y=${cy + 4} text-anchor="middle">${text}</text>`}
|
|
134
|
+
</g>`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function Card({ c, lead, cls, onOpen }) {
|
|
138
|
+
const n = c.node, it = n.i;
|
|
139
|
+
const names = it ? namesOf(it) : { primary: n.name, secondary: "" };
|
|
140
|
+
const id = it ? identityOf(it) : null;
|
|
141
|
+
const sub = it ? `${STATE_LABEL(n.state)} · ${backendLabel(it.backend)}` : STATE_LABEL(n.state);
|
|
142
|
+
const full = `${names.primary}${names.secondary ? ` (${names.secondary})` : ""} — ${sub}`;
|
|
143
|
+
const cy = c.h / 2;
|
|
144
|
+
return html`<g class=${`oc-node st-${n.state}${lead ? " lead" : ""}${cls ? ` ${cls}` : ""} ${identityClasses(id)}`} ref=${(el) => paintIdentity(el, id)}
|
|
145
|
+
transform=${`translate(${c.x} ${c.y})`} data-name=${n.name} data-key=${c.key} tabindex="0" role=${it ? "link" : "img"}
|
|
146
|
+
aria-label=${it ? `${full}. ${tc("openChat")}` : full} onClick=${it ? () => onOpen(n.name) : undefined}>
|
|
147
|
+
<title>${full}</title>
|
|
148
|
+
<rect class="oc-card" width=${c.w} height=${c.h} rx="10" />
|
|
149
|
+
<rect class="oc-idbar" x="0" y="8" width="4" height=${c.h - 16} rx="2" />
|
|
150
|
+
<${Face} it=${it} name=${n.name} cx=${26} cy=${cy} r=${16} />
|
|
151
|
+
<circle class=${`oc-dot st-${n.state}`} cx=${38} cy=${cy + 12} r=${5} />
|
|
152
|
+
<text class="oc-name" x=${50} y=${cy - 4}>${clip(names.primary, lead ? 20 : 15)}</text>
|
|
153
|
+
<text class="oc-sub" x=${50} y=${cy + 13}><tspan class=${`oc-st st-${n.state}`}>${clip(sub, lead ? 27 : 20)}</tspan></text>
|
|
154
|
+
</g>`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function teamName(b) { return b.name === null ? t("org.unteamed") : b.name; }
|
|
158
|
+
const foundIn = (b, q) => (q ? b.nodes.filter((n) => hits(n, q)).length : 0);
|
|
159
|
+
|
|
160
|
+
/** A team's box: its frame, header band and (folded) summary — under the traffic lines. */
|
|
161
|
+
function TeamBox({ b, q }) {
|
|
162
|
+
const found = foundIn(b, q);
|
|
163
|
+
return html`<g class=${`oc-team${b.loose ? " loose" : ""}${q && !found ? " dim" : ""}`} data-team=${b.key}>
|
|
164
|
+
<rect class=${`oc-box${b.folded && found ? " match" : ""}`} x=${b.x} y=${b.y} width=${b.w} height=${b.h} rx="12" />
|
|
165
|
+
<rect class="oc-head" x=${b.x + 1} y=${b.y + 1} width=${b.w - 2} height=${OC.HEAD - 2} rx="11" />
|
|
166
|
+
${!b.nodes.length ? html`<text class="oc-team-sum" x=${b.x + 14} y=${b.y + OC.HEAD + 18}>${tc("noMembers")}</text>` : null}
|
|
167
|
+
${b.folded ? b.summary.flatMap((row, i) => row.map((it) => html`<g key=${it.s}>
|
|
168
|
+
<circle class=${`oc-dot st-${it.s}`} cx=${b.x + it.dx + 5} cy=${b.y + OC.HEAD + 14 + i * SUM_LINE} r="5" />
|
|
169
|
+
<text class="oc-team-sum" x=${b.x + it.dx + 14} y=${b.y + OC.HEAD + 18 + i * SUM_LINE}>${it.label}</text></g>`)) : null}
|
|
170
|
+
</g>`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A team's header: the fold control, its name and count — above the traffic lines (#1918 review: a line leaving the box
|
|
175
|
+
* through its header ran over the count), so it stays readable and clickable.
|
|
176
|
+
*/
|
|
177
|
+
function TeamHead({ b, q, onFold }) {
|
|
178
|
+
const name = teamName(b), found = foundIn(b, q);
|
|
179
|
+
const count = b.nodes.length === 1 ? tc("member") : tc("members", b.nodes.length);
|
|
180
|
+
const toggle = (e) => { if (e.type === "click" || e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); onFold(b.key); } };
|
|
181
|
+
return html`<g class=${`oc-team-head${q && !found ? " dim" : ""}`} data-team=${b.key}>
|
|
182
|
+
<g class=${`oc-toggle${b.nodes.length ? "" : " empty"}`} tabindex=${b.nodes.length ? "0" : undefined} role=${b.nodes.length ? "button" : undefined}
|
|
183
|
+
aria-expanded=${b.nodes.length ? String(!b.folded) : undefined} aria-label=${`${name}, ${count}.${b.nodes.length ? ` ${b.folded ? tc("expand") : tc("collapse")}.` : ""}`}
|
|
184
|
+
onClick=${b.nodes.length ? toggle : undefined} onKeyDown=${b.nodes.length ? toggle : undefined}>
|
|
185
|
+
<rect class="oc-hit" x=${b.x} y=${b.y} width=${b.w} height=${OC.HEAD} />
|
|
186
|
+
${b.nodes.length ? html`<text class="oc-caret" x=${b.x + 12} y=${b.y + 22}>${b.folded ? "▸" : "▾"}</text>` : null}
|
|
187
|
+
<text class="oc-team-name" x=${b.x + (b.nodes.length ? 28 : 14)} y=${b.y + 22}>${clip(name, Math.max(8, Math.floor((b.w - 90) / 7.5)))}</text>
|
|
188
|
+
<text class="oc-team-meta" x=${b.x + b.w - 12} y=${b.y + 22} text-anchor="end">${b.folded && found ? tc("inFolded", found) : String(b.nodes.length)}</text>
|
|
189
|
+
</g>
|
|
190
|
+
</g>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Every card, in the order Tab walks them (General, then each team's members in reading order). */
|
|
194
|
+
const cardsOf = (L) => [...L.tops, ...L.boxes.flatMap((b) => b.cells)];
|
|
195
|
+
|
|
196
|
+
const reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The traffic lines: dotted while queued, a dash and a pulse while delivering, fading once delivered; a delegation's
|
|
200
|
+
* slow moving dash while its work is in progress, one pulse back when its report comes, a faint dash when none came;
|
|
201
|
+
* ×N on shared lines.
|
|
202
|
+
*/
|
|
203
|
+
const MARKER = (e) => (e.phase === "fading" || e.phase === "stale" ? "fading" : e.hue);
|
|
204
|
+
function TrafficLayer({ edges, q }) {
|
|
205
|
+
const still = reducedMotion();
|
|
206
|
+
return html`<g class="oc-flows" aria-hidden="true">${edges.map((e) => {
|
|
207
|
+
const dim = q && !e.names.some((n) => n.toLowerCase().includes(q));
|
|
208
|
+
return html`<g key=${e.key} class=${`oc-fl${dim ? " dim" : ""}`} data-routed=${e.routed ? "1" : undefined}>
|
|
209
|
+
<path class="oc-flow-halo" d=${e.d} />
|
|
210
|
+
<path class=${`oc-flow h-${e.hue} ph-${e.phase}`} d=${e.d} marker-start=${`url(#oc-src-${MARKER(e)})`} marker-end=${`url(#oc-ah-${MARKER(e)})`} />
|
|
211
|
+
${e.phase === "live" && !still ? html`<circle class=${`oc-pulse h-${e.hue}`} r="4"><animateMotion dur=${`${Math.max(1.6, e.pts.length * 0.3)}s`} repeatCount="indefinite" path=${e.d} /></circle>` : null}
|
|
212
|
+
${e.phase === "return" && !still ? html`<circle class=${`oc-pulse h-${e.hue}`} r="5"><animateMotion dur="1.6s" repeatCount="1" fill="freeze" path=${e.d} /></circle>` : null}
|
|
213
|
+
${e.badge ? html`<g class="oc-badge" transform=${`translate(${e.badge.x - 14} ${e.badge.y - 10})`}><rect width="28" height="20" rx="10" /><text x="14" y="14" text-anchor="middle">${`×${e.n}`}</text></g>` : null}
|
|
214
|
+
</g>`;
|
|
215
|
+
})}</g>`;
|
|
216
|
+
}
|
|
217
|
+
const ARROWS = ["task", "send", "report", "broadcast", "team", "fading"];
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The key to the lines (#2052): a **Key** toggle in the toolbar; open, its rows are a strip between the toolbar and the
|
|
221
|
+
* canvas — never over the canvas, so it covers no card (it used to sit in the canvas's bottom-left corner, over the
|
|
222
|
+
* team there at the opening view). Folded until the person opens it; kept per device.
|
|
223
|
+
*/
|
|
224
|
+
function readKeyOpen() { try { return localStorage.getItem(KEY_KEY) === "1"; } catch { return false; } }
|
|
225
|
+
function KeyToggle({ open, onToggle }) {
|
|
226
|
+
return html`<button type="button" class=${`btn btn-sm oc-key${open ? " on" : ""}`} aria-expanded=${open ? "true" : "false"} aria-controls="oc-legend"
|
|
227
|
+
onClick=${onToggle}>${tc("legend")}</button>`;
|
|
228
|
+
}
|
|
229
|
+
function Legend() {
|
|
230
|
+
const row = (cls, k) => html`<span class="oc-lg"><i class=${`oc-sw ${cls}`} aria-hidden="true"></i>${tc(k)}</span>`;
|
|
231
|
+
return html`<div id="oc-legend" class="oc-legend" role="region" aria-label=${tc("legend")}>
|
|
232
|
+
<div class="oc-lg-rows">${row("h-task", "lgTask")}${row("h-send", "lgSend")}${row("h-report", "lgReport")}${row("h-broadcast", "lgBroadcast")}
|
|
233
|
+
${row("ph-queued", "lgQueued")}${row("ph-fading", "lgFading")}${row("h-task ph-work", "lgWork")}${row("h-report ph-return", "lgReturn")}${row("ph-stale", "lgStale")}</div></div>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The page's traffic while the canvas (or its list) is open and Live is on: the snapshot on opening; then the stream's
|
|
238
|
+
* items — or, with no live stream (the public link, or a stream that fell back to polling), the snapshot every
|
|
239
|
+
* POLL_MS while the page is shown; read again when the stream comes back. Everything is the lease's (#1408 §4).
|
|
240
|
+
*/
|
|
241
|
+
function useTraffic(lease, on) {
|
|
242
|
+
const store = useMemo(createTrafficStore, []);
|
|
243
|
+
const [rev, setRev] = useState(0);
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (!on || !lease) return undefined;
|
|
246
|
+
const bump = () => setRev((n) => n + 1);
|
|
247
|
+
const read = async () => {
|
|
248
|
+
// What the stream had said when this was asked for: a stream event after it makes the answer possibly older.
|
|
249
|
+
const since = store.streamRev();
|
|
250
|
+
try {
|
|
251
|
+
const r = await lease.fetch("/ui/traffic", { method: "GET", headers: { "Content-Type": "application/json" } });
|
|
252
|
+
if (!lease.current() || !r.ok) return;
|
|
253
|
+
const d = await r.json();
|
|
254
|
+
if (lease.current() && store.load(d, undefined, since)) bump();
|
|
255
|
+
} catch { /* the next poll, or the stream, brings it */ }
|
|
256
|
+
};
|
|
257
|
+
void read();
|
|
258
|
+
const sub = appStore.get().streamOn;
|
|
259
|
+
// No live stream to bring traffic: the public link (poll transport), or a stream that fell back to /ui/poll.
|
|
260
|
+
const polled = () => globalThis.document?.body?.dataset?.webTransport === "poll" || appStore.get().connection === "polling";
|
|
261
|
+
const offs = [];
|
|
262
|
+
if (sub) {
|
|
263
|
+
offs.push(lease.hold(sub("traffic", (item) => { if (store.apply(item)) bump(); })));
|
|
264
|
+
offs.push(lease.hold(sub("delegation", (d) => { if (store.delegation(d)) bump(); })));
|
|
265
|
+
offs.push(lease.hold(sub("connection", (c) => { if (c === "live") void read(); })));
|
|
266
|
+
}
|
|
267
|
+
const hs = [
|
|
268
|
+
lease.interval(() => { if (polled() && !globalThis.document?.hidden) void read(); }, POLL_MS),
|
|
269
|
+
lease.interval(() => { if (store.sweep()) bump(); }, 1000),
|
|
270
|
+
];
|
|
271
|
+
return () => { for (const off of offs) off(); for (const h of hs) lease.clear(h); };
|
|
272
|
+
}, [lease, on]);
|
|
273
|
+
return useMemo(() => (on ? store.items() : []), [rev, on]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* #2182: the traffic lines, routed around the canvas by libavoid in a worker (fleet-route.js) once it answers; until
|
|
278
|
+
* then, and zoomed out (one line per team pair), along the lanes (fleet-traffic.js route()). An answer is kept while the
|
|
279
|
+
* layout it was routed on is the one drawn: a line it has not seen yet takes its lane until the next answer.
|
|
280
|
+
*/
|
|
281
|
+
let routeClient = null;
|
|
282
|
+
function useTrafficRoutes(traffic, L, far) {
|
|
283
|
+
const lanes = useMemo(() => (traffic ? trafficEdges(traffic, L, { far }) : []), [traffic, L, far]);
|
|
284
|
+
const scene = useMemo(() => (far || !lanes.length ? null : routeScene(L, lanes)), [lanes, L, far]);
|
|
285
|
+
const [answer, setAnswer] = useState(null); // { shapes, map }
|
|
286
|
+
// The layout as the router sees it: a status frame re-making the same layout keeps the answer (and asks nothing).
|
|
287
|
+
const shapes = scene ? JSON.stringify(scene.shapes) : null;
|
|
288
|
+
const ask = scene && scene.conns.length ? JSON.stringify(scene) : null;
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
if (!ask) return undefined;
|
|
291
|
+
routeClient ??= createRouteClient();
|
|
292
|
+
if (!routeClient.available) return undefined;
|
|
293
|
+
let live = true;
|
|
294
|
+
routeClient.route(JSON.parse(ask)).then((map) => { if (live && map) setAnswer({ shapes, map }); });
|
|
295
|
+
return () => { live = false; };
|
|
296
|
+
}, [ask]);
|
|
297
|
+
useEffect(() => () => { routeClient?.close(); routeClient = null; }, []);
|
|
298
|
+
return useMemo(() => (answer && answer.shapes === shapes && traffic ? trafficEdges(traffic, L, { far, routed: answer.map }) : lanes),
|
|
299
|
+
[lanes, answer, shapes, L, far, traffic]);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The canvas body: `view` buildOrgView's answer; `folded` the folded teams' keys; `q` the search needle; `traffic` the live items (null: Live is off). */
|
|
303
|
+
function Canvas({ view, folded, onFold, q, jump, traffic }) {
|
|
304
|
+
const [narrow] = useState(() => typeof matchMedia === "function" && matchMedia("(max-width: 640px)").matches);
|
|
305
|
+
const [far, setFar] = useState(false);
|
|
306
|
+
const L = useMemo(() => canvasLayout(view, folded, { narrow, label: (s, n) => `${n} ${STATE_LABEL(s)}` }), [view, folded, narrow]);
|
|
307
|
+
const svgRef = useRef(null), worldRef = useRef(null), miniRef = useRef(null), miniViewRef = useRef(null);
|
|
308
|
+
const vp = useRef(null);
|
|
309
|
+
const drag = useRef({ pts: new Map(), moved: 0, span: null, swallow: false });
|
|
310
|
+
const size = () => {
|
|
311
|
+
const r = svgRef.current ? svgRef.current.getBoundingClientRect() : null;
|
|
312
|
+
return r && r.width > 0 && r.height > 0 ? { w: r.width, h: r.height } : { w: 960, h: 600 };
|
|
313
|
+
};
|
|
314
|
+
const apply = () => {
|
|
315
|
+
const v = vp.current;
|
|
316
|
+
if (!v || !worldRef.current) return;
|
|
317
|
+
worldRef.current.setAttribute("transform", `translate(${v.x} ${v.y}) scale(${v.k})`);
|
|
318
|
+
const s = size(), m = miniViewRef.current;
|
|
319
|
+
if (m) for (const [k, val] of [["x", -v.x / v.k], ["y", -v.y / v.k], ["width", s.w / v.k], ["height", s.h / v.k]]) m.setAttribute(k, String(val));
|
|
320
|
+
};
|
|
321
|
+
// Zoomed out past FAR_K, traffic is drawn per team pair: a re-render only when that changes.
|
|
322
|
+
const set = (v) => { vp.current = v; apply(); if ((v.k < FAR_K) !== far) setFar(v.k < FAR_K); };
|
|
323
|
+
const whole = () => ({ x: OC.TRUNK - 20, y: 0, w: Math.max(1, L.width - OC.TRUNK + 40), h: Math.max(1, L.height + 10) });
|
|
324
|
+
const fit = () => set(fitView(whole(), size()));
|
|
325
|
+
const zoomBy = (f) => { const s = size(); set(zoomAt(vp.current, f, s.w / 2, s.h / 2)); };
|
|
326
|
+
/** Bring a card into view (keyboard focus, a search's Enter): centred, at a readable size when `zoom`. */
|
|
327
|
+
const show = (c, zoom) => {
|
|
328
|
+
const s = size(), v = vp.current;
|
|
329
|
+
const k = zoom ? Math.max(v.k, 1) : v.k;
|
|
330
|
+
const sx = c.x * k + v.x, sy = c.y * k + v.y;
|
|
331
|
+
if (!zoom && sx >= 8 && sy >= 8 && sx + c.w * k <= s.w - 8 && sy + c.h * k <= s.h - 8) return;
|
|
332
|
+
set({ k, x: s.w / 2 - (c.x + c.w / 2) * k, y: s.h / 2 - (c.y + c.h / 2) * k });
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// The opening view: General and the first row — a desktop as wide as fits, no larger than life; a phone no smaller
|
|
336
|
+
// than readable.
|
|
337
|
+
useLayoutEffect(() => {
|
|
338
|
+
vp.current = narrow ? fitView(openingRegion(L), size(), { margin: 12, minK: 0.75 }) : fitView(whole(), size(), { widthOnly: true, maxK: 1, minK: 0.6 });
|
|
339
|
+
setFar(vp.current.k < FAR_K);
|
|
340
|
+
}, []);
|
|
341
|
+
const edges = useTrafficRoutes(traffic, L, far);
|
|
342
|
+
// Preact owns the markup, not the transform: put it back after every render.
|
|
343
|
+
useLayoutEffect(apply);
|
|
344
|
+
// A search's Enter: the first match, unfolded (by the panel) and then brought into view — once. It waits for the
|
|
345
|
+
// render that has its card (the unfold); a later render (the stream, a fold, a new search text) keeps the person's view.
|
|
346
|
+
const pendingJump = useRef(null);
|
|
347
|
+
useLayoutEffect(() => { pendingJump.current = jump; }, [jump]);
|
|
348
|
+
useLayoutEffect(() => {
|
|
349
|
+
const j = pendingJump.current;
|
|
350
|
+
if (!j) return;
|
|
351
|
+
const c = cardsOf(L).find((x) => x.node.name === j.name);
|
|
352
|
+
if (c) { pendingJump.current = null; show(c, true); return; }
|
|
353
|
+
if (!L.boxes.some((b) => b.folded && b.nodes.some((n) => n.name === j.name))) pendingJump.current = null; // gone: nothing to wait for
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const open = (name) => navigate(chatPath(name));
|
|
357
|
+
// The click that ends a drag or a pinch opens nothing and folds nothing.
|
|
358
|
+
const onClickCapture = (e) => { if (drag.current.swallow) { e.preventDefault(); e.stopPropagation(); } };
|
|
359
|
+
const onWheel = (e) => {
|
|
360
|
+
e.preventDefault();
|
|
361
|
+
const r = svgRef.current.getBoundingClientRect();
|
|
362
|
+
set(zoomAt(vp.current, Math.exp(-e.deltaY * 0.0015), e.clientX - r.left, e.clientY - r.top));
|
|
363
|
+
};
|
|
364
|
+
// One pointer pans (after a few pixels, so a tap still opens a card); two pinch: the canvas point under their old
|
|
365
|
+
// midpoint goes to the new one, scaled by how their span changed (so a two-finger move pans too). The pointer is
|
|
366
|
+
// captured only once it drags: a capture at once would send the tap's click to the canvas, not the card.
|
|
367
|
+
const spanOf = () => {
|
|
368
|
+
const [a, b] = [...drag.current.pts.values()], r = svgRef.current.getBoundingClientRect();
|
|
369
|
+
return { mid: { x: (a.x + b.x) / 2 - r.left, y: (a.y + b.y) / 2 - r.top }, dist: Math.hypot(a.x - b.x, a.y - b.y) };
|
|
370
|
+
};
|
|
371
|
+
const onPointerDown = (e) => {
|
|
372
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
373
|
+
const d = drag.current;
|
|
374
|
+
d.pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
375
|
+
if (d.pts.size === 1) { d.moved = 0; d.swallow = false; d.span = null; }
|
|
376
|
+
else if (d.pts.size === 2) { d.swallow = true; d.span = spanOf(); }
|
|
377
|
+
};
|
|
378
|
+
const onPointerMove = (e) => {
|
|
379
|
+
const d = drag.current, prev = d.pts.get(e.pointerId);
|
|
380
|
+
if (!prev) return;
|
|
381
|
+
d.pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
382
|
+
if (d.pts.size === 1) {
|
|
383
|
+
d.moved += Math.abs(e.clientX - prev.x) + Math.abs(e.clientY - prev.y);
|
|
384
|
+
if (d.moved < 5) return;
|
|
385
|
+
if (!d.swallow) { d.swallow = true; try { svgRef.current.setPointerCapture(e.pointerId); } catch { /* gone already */ } }
|
|
386
|
+
set({ ...vp.current, x: vp.current.x + e.clientX - prev.x, y: vp.current.y + e.clientY - prev.y });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (d.pts.size !== 2) return;
|
|
390
|
+
d.swallow = true;
|
|
391
|
+
const next = spanOf();
|
|
392
|
+
if (d.span && d.span.dist > 0 && next.dist > 0) set(pinchView(vp.current, d.span, next));
|
|
393
|
+
d.span = next;
|
|
394
|
+
};
|
|
395
|
+
const onPointerUp = (e) => {
|
|
396
|
+
const d = drag.current;
|
|
397
|
+
d.pts.delete(e.pointerId);
|
|
398
|
+
// Back to one finger: it pans on at once (the pinch already swallows the click); two again: a new span.
|
|
399
|
+
d.span = d.pts.size === 2 ? spanOf() : null;
|
|
400
|
+
if (d.pts.size === 1) d.moved = 5;
|
|
401
|
+
// The click that ends a drag comes after this: let it be swallowed, then forget.
|
|
402
|
+
if (d.swallow && !d.pts.size) setTimeout(() => { if (!d.pts.size) d.swallow = false; }, 0);
|
|
403
|
+
};
|
|
404
|
+
const onKeyDown = (e) => {
|
|
405
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
406
|
+
if (e.key === "+" || e.key === "=") { e.preventDefault(); zoomBy(1.25); return; }
|
|
407
|
+
if (e.key === "-" || e.key === "_") { e.preventDefault(); zoomBy(0.8); return; }
|
|
408
|
+
if (e.key === "0") { e.preventDefault(); fit(); return; }
|
|
409
|
+
const dir = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[e.key];
|
|
410
|
+
const card = e.target && e.target.closest ? e.target.closest(".oc-node") : null;
|
|
411
|
+
if (card && (e.key === "Enter" || e.key === " ")) {
|
|
412
|
+
e.preventDefault();
|
|
413
|
+
const c = cardsOf(L).find((x) => x.key === card.getAttribute("data-key"));
|
|
414
|
+
if (c && c.node.i) open(c.node.name);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (!dir) return;
|
|
418
|
+
e.preventDefault();
|
|
419
|
+
if (!card) { set({ ...vp.current, x: vp.current.x - dir[0] * 80, y: vp.current.y - dir[1] * 80 }); return; }
|
|
420
|
+
const next = nearest(cardsOf(L), card.getAttribute("data-key"), dir);
|
|
421
|
+
if (next) focusCard(svgRef.current, next.key);
|
|
422
|
+
};
|
|
423
|
+
const onFocusIn = (e) => {
|
|
424
|
+
const card = e.target && e.target.closest ? e.target.closest(".oc-node") : null;
|
|
425
|
+
const c = card ? cardsOf(L).find((x) => x.key === card.getAttribute("data-key")) : null;
|
|
426
|
+
if (c) show(c, false);
|
|
427
|
+
};
|
|
428
|
+
const onMini = (e) => {
|
|
429
|
+
const m = miniRef.current, r = m.getBoundingClientRect(), box = miniBox(L);
|
|
430
|
+
if (!r.width || !r.height) return;
|
|
431
|
+
const scale = Math.min(r.width / box.w, r.height / box.h);
|
|
432
|
+
const wx = box.x + (e.clientX - r.left - (r.width - box.w * scale) / 2) / scale, wy = box.y + (e.clientY - r.top - (r.height - box.h * scale) / 2) / scale;
|
|
433
|
+
const s = size(), v = vp.current;
|
|
434
|
+
set({ k: v.k, x: s.w / 2 - wx * v.k, y: s.h / 2 - wy * v.k });
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const box = miniBox(L);
|
|
438
|
+
return html`<div class="oc-stage">
|
|
439
|
+
<svg ref=${svgRef} class="oc-svg" role="application" tabindex="0" aria-label=${tc("label")} onWheel=${onWheel} onPointerDown=${onPointerDown}
|
|
440
|
+
onPointerMove=${onPointerMove} onPointerUp=${onPointerUp} onPointerCancel=${onPointerUp} onKeyDown=${onKeyDown} onFocusIn=${onFocusIn} onClickCapture=${onClickCapture}>
|
|
441
|
+
<defs><clipPath id="oc-clip-av" clipPathUnits="objectBoundingBox"><circle cx="0.5" cy="0.5" r="0.5" /></clipPath>
|
|
442
|
+
${ARROWS.map((a) => html`<marker key=${a} id=${`oc-ah-${a}`} viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
|
|
443
|
+
<path d="M0 0 L10 5 L0 10 z" class=${`oc-ah h-${a}`} /></marker>`)}
|
|
444
|
+
${ARROWS.map((a) => html`<marker key=${`s-${a}`} id=${`oc-src-${a}`} viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5">
|
|
445
|
+
<circle cx="5" cy="5" r="4" class=${`oc-ah h-${a}`} /></marker>`)}</defs>
|
|
446
|
+
<g ref=${worldRef} class="oc-world">
|
|
447
|
+
<g class="oc-tree">${L.tree.map((d, i) => html`<path key=${i} d=${d} />`)}</g>
|
|
448
|
+
<g class="oc-teams">${L.boxes.map((b) => html`<${TeamBox} key=${b.key} b=${b} q=${q} />`)}</g>
|
|
449
|
+
<${TrafficLayer} edges=${edges} q=${q} />
|
|
450
|
+
<g class="oc-heads">${L.boxes.map((b) => html`<${TeamHead} key=${b.key} b=${b} q=${q} onFold=${onFold} />`)}</g>
|
|
451
|
+
<g class="oc-cards">
|
|
452
|
+
${L.tops.map((c) => html`<${Card} key=${c.key} c=${c} lead=${true} cls=${q ? (hits(c.node, q) ? "match" : "dim") : ""} onOpen=${open} />`)}
|
|
453
|
+
${L.boxes.flatMap((b) => b.cells.map((c) => html`<${Card} key=${c.key} c=${c} lead=${false} cls=${q ? (hits(c.node, q) ? "match" : "dim") : ""} onOpen=${open} />`))}
|
|
454
|
+
</g>
|
|
455
|
+
</g>
|
|
456
|
+
</svg>
|
|
457
|
+
<div class="oc-zoom" role="group" aria-label=${tc("view")}>
|
|
458
|
+
<button type="button" class="icon-btn" aria-label=${tc("zoomIn")} title=${tc("zoomIn")} onClick=${() => zoomBy(1.25)}><${Icon} name="plus" size=${16} /></button>
|
|
459
|
+
<button type="button" class="icon-btn" aria-label=${tc("zoomOut")} title=${tc("zoomOut")} onClick=${() => zoomBy(0.8)}><span aria-hidden="true">−</span></button>
|
|
460
|
+
<button type="button" class="icon-btn" aria-label=${tc("fit")} title=${tc("fit")} onClick=${fit}><${Icon} name="org" size=${16} /></button>
|
|
461
|
+
</div>
|
|
462
|
+
<svg ref=${miniRef} class="oc-mini" viewBox=${`${box.x} ${box.y} ${box.w} ${box.h}`} aria-hidden="true" onClick=${onMini}>
|
|
463
|
+
<title>${tc("minimap")}</title>
|
|
464
|
+
${L.tops.map((c) => html`<rect key=${c.key} class="oc-mini-top" x=${c.x} y=${c.y} width=${c.w} height=${c.h} rx="10" />`)}
|
|
465
|
+
${L.boxes.map((b) => html`<rect key=${b.key} class=${`oc-mini-team${b.nodes.some((n) => n.state === "needs") ? " needs" : ""}${q && b.nodes.some((n) => hits(n, q)) ? " match" : ""}`}
|
|
466
|
+
x=${b.x} y=${b.y} width=${b.w} height=${b.h} rx="12" />`)}
|
|
467
|
+
${q ? cardsOf(L).filter((c) => hits(c.node, q)).map((c) => html`<rect key=${`m:${c.key}`} class="oc-mini-hit" x=${c.x} y=${c.y} width=${c.w} height=${c.h} rx="8" />`) : null}
|
|
468
|
+
<rect ref=${miniViewRef} class="oc-mini-view" />
|
|
469
|
+
</svg>
|
|
470
|
+
</div>`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const miniBox = (L) => ({ x: OC.TRUNK - 30, y: 0, w: Math.max(1, L.width - OC.TRUNK + 60), h: Math.max(1, L.height + 20) });
|
|
474
|
+
|
|
475
|
+
/** The card nearest the one with `key` in direction `dir` ([dx, dy]); null when there is none that way. */
|
|
476
|
+
export function nearest(cards, key, dir) {
|
|
477
|
+
const from = cards.find((c) => c.key === key);
|
|
478
|
+
if (!from) return null;
|
|
479
|
+
const fx = from.x + from.w / 2, fy = from.y + from.h / 2;
|
|
480
|
+
let best = null, bestScore = Infinity;
|
|
481
|
+
for (const c of cards) {
|
|
482
|
+
if (c === from) continue;
|
|
483
|
+
const dx = c.x + c.w / 2 - fx, dy = c.y + c.h / 2 - fy;
|
|
484
|
+
const along = dx * dir[0] + dy * dir[1];
|
|
485
|
+
if (along <= 1) continue;
|
|
486
|
+
const across = Math.abs(dx * dir[1]) + Math.abs(dy * dir[0]);
|
|
487
|
+
const score = along + across * 2;
|
|
488
|
+
if (score < bestScore) { best = c; bestScore = score; }
|
|
489
|
+
}
|
|
490
|
+
return best;
|
|
491
|
+
}
|
|
492
|
+
function focusCard(svg, key) {
|
|
493
|
+
const el = svg && [...svg.querySelectorAll(".oc-node")].find((n) => n.getAttribute("data-key") === key);
|
|
494
|
+
if (el && typeof el.focus === "function") el.focus();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* The tab's body (the Fleet panel's loader passes /ui/org's answer and the app store): Canvas or List, chosen per
|
|
499
|
+
* device; the list is fleet-org.js's chart, which is also what a screen reader is pointed to.
|
|
500
|
+
*/
|
|
501
|
+
export function OrgCanvas({ org, app, lease }) {
|
|
502
|
+
useIdentityColors();
|
|
503
|
+
const [mode, setMode] = useState(readMode);
|
|
504
|
+
const [live, setLive] = useState(readLive);
|
|
505
|
+
const traffic = useTraffic(lease, live);
|
|
506
|
+
const sending = useMemo(() => sendingBy(traffic), [traffic]);
|
|
507
|
+
const [folded, setFolded] = useState(readFolded);
|
|
508
|
+
const [query, setQuery] = useState("");
|
|
509
|
+
const [jump, setJump] = useState(null);
|
|
510
|
+
const view = useMemo(() => buildOrgView(org, app), [org, app]);
|
|
511
|
+
useEffect(() => { save(MODE_KEY, mode); }, [mode]);
|
|
512
|
+
useEffect(() => { save(LIVE_KEY, live ? "1" : "0"); }, [live]);
|
|
513
|
+
if (!app.ready) return html`<${Skeleton} lines=${4} />`;
|
|
514
|
+
if (!view.total) return html`<${Empty} icon="team" title=${t("org.empty")} hint=${t("org.emptyHint")} />`;
|
|
515
|
+
const q = needle(query);
|
|
516
|
+
const all = [...view.top, ...view.teams.flatMap((tm) => tm.nodes), ...view.rest.nodes];
|
|
517
|
+
const found = q ? new Set(all.filter((n) => hits(n, q)).map((n) => n.name)).size : 0;
|
|
518
|
+
const onFold = (key) => setFolded((cur) => {
|
|
519
|
+
const next = new Set(cur);
|
|
520
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
521
|
+
save(FOLD_KEY, JSON.stringify([...next]));
|
|
522
|
+
return next;
|
|
523
|
+
});
|
|
524
|
+
const onFind = (e) => {
|
|
525
|
+
if (e.key !== "Enter" || !q) return;
|
|
526
|
+
e.preventDefault();
|
|
527
|
+
const first = all.find((n) => hits(n, q));
|
|
528
|
+
if (!first) return;
|
|
529
|
+
const groups = [...view.teams.map((tm) => [tm.name, tm.nodes]), [REST_KEY, view.rest.nodes]];
|
|
530
|
+
const holder = view.top.includes(first) ? null : groups.find(([, nodes]) => nodes.includes(first));
|
|
531
|
+
if (holder && folded.has(holder[0])) onFold(holder[0]);
|
|
532
|
+
setJump({ name: first.name, at: Date.now() });
|
|
533
|
+
};
|
|
534
|
+
const [keyOpen, setKeyOpen] = useState(readKeyOpen);
|
|
535
|
+
const toggleKey = () => setKeyOpen((v) => { save(KEY_KEY, v ? "0" : "1"); return !v; });
|
|
536
|
+
const modeBtn = (m) => html`<button type="button" class=${`btn btn-sm${mode === m ? " btn-primary" : ""}`} aria-pressed=${mode === m ? "true" : "false"}
|
|
537
|
+
onClick=${() => setMode(m)}>${tc(m)}</button>`;
|
|
538
|
+
return html`<div class=${`org oc oc-${mode}`}>
|
|
539
|
+
<div class="list-head org-head oc-bar">
|
|
540
|
+
<span>${view.total === 1 ? t("org.instance") : t("org.instances", view.total)}</span><${OrgSummary} counts=${view.counts} />
|
|
541
|
+
<div class="oc-tools">
|
|
542
|
+
${mode === "canvas" ? html`<label class="s-search oc-find"><${Icon} name="search" size=${14} /><span class="sr-only">${tc("find")}</span>
|
|
543
|
+
<input type="search" value=${query} placeholder=${tc("findShort")} autocomplete="off" onInput=${(e) => setQuery(e.target.value)} onKeyDown=${onFind} />
|
|
544
|
+
${q ? html`<span class="oc-found" role="status">${found ? (found === 1 ? tc("foundOne") : tc("found", found)) : tc("noMatch")}</span>` : null}</label>` : null}
|
|
545
|
+
${mode === "canvas" && live ? html`<${KeyToggle} open=${keyOpen} onToggle=${toggleKey} />` : null}
|
|
546
|
+
<button type="button" class=${`btn btn-sm oc-live${live ? " on" : ""}`} aria-pressed=${live ? "true" : "false"} title=${live ? tc("liveOn") : tc("liveOff")}
|
|
547
|
+
onClick=${() => setLive((v) => !v)}><span class="oc-live-dot" aria-hidden="true"></span>${tc("live")}</button>
|
|
548
|
+
<div class="seg-inline" role="group" aria-label=${tc("view")}>${modeBtn("canvas")}${modeBtn("list")}</div>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
${mode === "canvas" && live && keyOpen ? html`<${Legend} />` : null}
|
|
552
|
+
${mode === "list" ? html`<${OrgChart} org=${org} app=${app} bare=${true} sending=${sending} />`
|
|
553
|
+
: html`<${Canvas} view=${view} folded=${folded} onFold=${onFold} q=${q} jump=${jump} traffic=${live ? traffic : null} />`}
|
|
554
|
+
</div>`;
|
|
555
|
+
}
|