@songsid/agend 2.2.0-beta.9 → 2.3.0-beta.1
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 +40 -24
- package/README.zh-TW.md +40 -24
- package/dist/agent-cli.js +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +28 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +20 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +3 -1
- package/dist/auth-api.js +32 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +33 -0
- package/dist/backend/antigravity.js +129 -21
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +9 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +3 -0
- package/dist/backend/grok.js +9 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -1
- package/dist/backend/kiro.js +7 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +24 -0
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +14 -0
- package/dist/channel/adapters/discord.js +66 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +26 -1
- package/dist/channel/adapters/telegram.js +98 -7
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +1 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +31 -0
- package/dist/cli.js +124 -24
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +5 -2
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.d.ts +86 -0
- package/dist/compact-watch.js +288 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/config-validator.js +55 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.js +394 -29
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -1
- package/dist/delivery-outbox.js +6 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/doctor.js +7 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +12 -0
- package/dist/fleet-manager.d.ts +247 -9
- package/dist/fleet-manager.js +1227 -142
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/install-lock.d.ts +18 -1
- package/dist/install-lock.js +30 -3
- package/dist/install-lock.js.map +1 -1
- package/dist/instance-avatar.d.ts +5 -4
- package/dist/instance-avatar.js +15 -8
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +2 -2
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/launchd-handoff.d.ts +43 -0
- package/dist/launchd-handoff.js +56 -0
- package/dist/launchd-handoff.js.map +1 -0
- package/dist/locale.js +202 -30
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +10 -2
- package/dist/log-service.js.map +1 -1
- package/dist/login-shell.d.ts +21 -0
- package/dist/login-shell.js +98 -0
- package/dist/login-shell.js.map +1 -0
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +32 -0
- package/dist/outbound-handlers.js +150 -45
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +76 -0
- package/dist/outbound-schemas.js +65 -4
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +29 -0
- package/dist/provider-probe.js +198 -2
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +25 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +8 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +12 -0
- package/dist/quickstart.js +36 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +86 -5
- package/dist/reply-buttons-controller.js +271 -7
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +0 -2
- package/dist/restart-progress.js +0 -2
- package/dist/restart-progress.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/sd-notify.d.ts +22 -1
- package/dist/sd-notify.js +63 -10
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +42 -26
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +24 -0
- package/dist/service-installer.js +49 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +11 -0
- package/dist/service-plan.js +23 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tool-permissions.js +53 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +72 -2
- package/dist/topic-commands.js +156 -38
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/transcript-jsonl.d.ts +7 -2
- package/dist/transcript-jsonl.js +13 -7
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-sources.js +6 -1
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +59 -14
- package/dist/ui/chat-store.js +78 -8
- package/dist/ui/chat-strings.js +43 -16
- package/dist/ui/chat-thread.js +175 -31
- package/dist/ui/chat-tour.js +18 -15
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +101 -27
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +18 -5
- package/dist/ui/panel-log.js +10 -10
- package/dist/ui/panel-needs.js +13 -10
- package/dist/ui/panel-settings.js +66 -12
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-dialogs.js +11 -6
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-model.js +3 -1
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +269 -10
- package/dist/ui/settings-token.js +7 -0
- package/dist/ui/settings-wizard.js +5 -3
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +3 -1
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +15 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +266 -11
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +115 -0
- package/dist/ui/shared/effect-policy.js +137 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +35 -15
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/instance-switch.js +5 -2
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +96 -12
- package/dist/ui/shared/signin.js +17 -9
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/view-strings.js +40 -2
- package/dist/ui/signin.html +9 -3
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-launch.d.ts +48 -1
- package/dist/update-launch.js +159 -5
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/providers.d.ts +28 -8
- package/dist/usage/providers.js +96 -41
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +214 -17
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +86 -2
- package/dist/web-api.js +1160 -92
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +39 -1
- package/dist/web-chat-history.js +69 -9
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +39 -3
- package/dist/web-commands.js +22 -5
- package/dist/web-commands.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +29 -0
- package/dist/web-effect-upload.js +190 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +180 -0
- package/dist/web-owner-css-store.js +384 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +66 -0
- package/dist/web-prefs.js +243 -35
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-session.d.ts +14 -0
- package/dist/web-session.js +10 -0
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.js.map +1 -1
- 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-theme-store.d.ts +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-upload.d.ts +119 -4
- package/dist/web-upload.js +252 -39
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/launcher/install-admission.cjs +61 -6
- package/package.json +3 -3
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// #1629 r4 §3: Settings → Appearance — the person's own custom CSS (owner CSS): the editor, keep-or-revert, earlier
|
|
2
|
+
// versions, the pictures and fonts it may name, and this device's off switch. Settings itself is always drawn stock
|
|
3
|
+
// (owner-css.js removes the link here), so a stylesheet that hides things can always be fixed from this page.
|
|
4
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
5
|
+
import { t } from "/assets/app-i18n.js";
|
|
6
|
+
import { appStore, useStore } from "/assets/app-store.js";
|
|
7
|
+
import { ModsCard } from "./settings-mods.js";
|
|
8
|
+
import { EffectsCard } from "./settings-effects.js";
|
|
9
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
10
|
+
import { toast } from "/assets/ui-toast.js";
|
|
11
|
+
import { ownerCssOffHere, ownerCssTrial, safeMode, setOwnerCssActive, setOwnerCssOffHere, startOwnerCssTrial } from "/assets/owner-css.js";
|
|
12
|
+
import { setThemesShown, themesStore } from "/assets/app-themes.js";
|
|
13
|
+
import { CARD_SIZES, cardSizeDefault, cardSizeStore, setCardSizeDefault } from "/assets/identity-card.js";
|
|
14
|
+
import { AgentWidget, checkWidget, fleetBinds, setSidebarWidget, sidebarWidgetRaw, sidebarWidgetStore, widgetProblem } from "/assets/agent-widget.js";
|
|
15
|
+
import { confirmedWrite } from "./settings-confirm.js";
|
|
16
|
+
import { savePref } from "/assets/app-prefs.js";
|
|
17
|
+
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
18
|
+
import { colorsOff, defaultPlaces, PLACEMENTS, setColorsOff, setDefaultPlaces, useIdentityColors } from "/assets/identity-color.js";
|
|
19
|
+
import "./settings-strings.js";
|
|
20
|
+
|
|
21
|
+
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
22
|
+
const short = (sha) => (sha ? sha.slice(0, 8) : "");
|
|
23
|
+
const kib = (n) => `${Math.max(1, Math.round(n / 1024))} KiB`;
|
|
24
|
+
|
|
25
|
+
async function call(path, init) {
|
|
26
|
+
let res;
|
|
27
|
+
try { res = await fetch(path, { credentials: "same-origin", ...init }); } catch { return { ok: false, status: 0, body: null }; }
|
|
28
|
+
let body = null;
|
|
29
|
+
try { body = await res.json(); } catch { /* not JSON */ }
|
|
30
|
+
return { ok: res.ok, status: res.status, body };
|
|
31
|
+
}
|
|
32
|
+
const postJson = (path, body) => call(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
33
|
+
|
|
34
|
+
/** Why the host refused a text, in the person's words. */
|
|
35
|
+
function refusal(body) {
|
|
36
|
+
const problem = body && body.error, detail = body && body.detail;
|
|
37
|
+
const known = ["at_rule", "url", "unknown_asset", "function", "property", "script_scheme", "too_large", "malformed", "nul", "unserialisable",
|
|
38
|
+
"asset_type", "asset_unreadable", "asset_quota", "asset_in_use", "owner_css_read_only", "owner_css_needs_a_signed_in_person"];
|
|
39
|
+
return known.includes(problem) ? tn(`cssError_${problem}`, detail || "") : tn("cssError_other", problem || "?");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function AppearanceSection() {
|
|
43
|
+
const publicLink = typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1";
|
|
44
|
+
if (publicLink) return html`<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="sub">${tn("cssPublicLink")}</p></section>`;
|
|
45
|
+
return html`<${ThemesCard} /><${IdentityColorCard} /><${CardSizeCard} /><${SidebarWidgetCard} /><${OwnerCssCard} /><${ModsCard} /><${EffectsCard} /><${DeviceCard} /><${CardLimitsCard} /><${RestoreDefaultsCard} />`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* #1958: identity colours on this host — where they show for an instance nobody chose for (ring and bar unless
|
|
50
|
+
* changed), and a switch that turns them off everywhere while every pick is kept.
|
|
51
|
+
*/
|
|
52
|
+
function IdentityColorCard() {
|
|
53
|
+
useIdentityColors();
|
|
54
|
+
const places = defaultPlaces(), off = colorsOff();
|
|
55
|
+
const toggle = (p) => setDefaultPlaces(places.includes(p) ? places.filter((x) => x !== p) : [...places, p]);
|
|
56
|
+
return html`<section class="card s-appearance s-synced s-colors"><h3>${tn("colorTitle")}</h3>
|
|
57
|
+
<p class="sub">${tn("colorLead")}</p>
|
|
58
|
+
<fieldset class="idc-places" disabled=${off}><legend>${tn("colorPlaces")}</legend>
|
|
59
|
+
${PLACEMENTS.map((p) => html`<label key=${p} class="check"><input type="checkbox" checked=${places.includes(p)} onChange=${() => toggle(p)} /> ${t(`color.place_${p}`)}</label>`)}
|
|
60
|
+
</fieldset>
|
|
61
|
+
<label class="check"><input type="checkbox" class="s-colors-off" checked=${off} onChange=${(e) => setColorsOff(e.target.checked)} /> <span>${tn("colorOff")}</span></label>
|
|
62
|
+
<p class="sub">${tn("colorOffLead")}</p>
|
|
63
|
+
</section>`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* #1958 decision 5: Restore defaults — the host's appearance settings back to their defaults after a confirm that says
|
|
68
|
+
* what it resets; this person's custom CSS turned off (its versions stay). Not settings, so never touched: the colours
|
|
69
|
+
* people picked per instance, installed mods, uploaded effects and the fleet's card limits.
|
|
70
|
+
*/
|
|
71
|
+
export async function restoreAppearanceDefaults() {
|
|
72
|
+
setThemesShown(true);
|
|
73
|
+
setCardSizeDefault("m");
|
|
74
|
+
savePref("card.sizeByInstance", null);
|
|
75
|
+
setSidebarWidget(null);
|
|
76
|
+
setDefaultPlaces(null);
|
|
77
|
+
setColorsOff(false);
|
|
78
|
+
setOwnerCssOffHere(false);
|
|
79
|
+
const r = await postJson("/ui/owner-css/restore", { sha: null });
|
|
80
|
+
return r && r.error ? { ok: false, error: r.error } : { ok: true };
|
|
81
|
+
}
|
|
82
|
+
function RestoreDefaultsCard() {
|
|
83
|
+
const [busy, setBusy] = useState(false);
|
|
84
|
+
async function go() {
|
|
85
|
+
if (!(await confirmDialog({ title: tn("restoreTitle"), message: tn("restoreConfirm"), confirmLabel: tn("restoreGo"), danger: true }))) return;
|
|
86
|
+
setBusy(true);
|
|
87
|
+
const r = await restoreAppearanceDefaults();
|
|
88
|
+
setBusy(false);
|
|
89
|
+
if (r.ok) toast(tn("restoreDone")); else toast(tn("restoreCssFailed", r.error), false);
|
|
90
|
+
}
|
|
91
|
+
return html`<section class="card s-appearance s-restore"><h3>${tn("restoreTitle")}</h3>
|
|
92
|
+
<p class="sub">${tn("restoreLead")}</p>
|
|
93
|
+
<button type="button" class="btn" disabled=${busy} onClick=${go}>${tn("restoreTitle")}</button>
|
|
94
|
+
</section>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** #1629 r4.1 Q3 (a): every identity card's size on this person's screens (a card's own control sets it apart). */
|
|
98
|
+
function CardSizeCard() {
|
|
99
|
+
useStore(cardSizeStore);
|
|
100
|
+
const size = cardSizeDefault();
|
|
101
|
+
return html`<section class="card s-appearance s-synced"><h3>${tn("cardSizeTitle")}</h3>
|
|
102
|
+
<div class="card-size" role="group" aria-label=${tn("cardSizeTitle")}>
|
|
103
|
+
${CARD_SIZES.map((s) => html`<button key=${s} type="button" class=${`seg-item${s === size ? " active" : ""}`} aria-pressed=${s === size ? "true" : "false"}
|
|
104
|
+
onClick=${() => setCardSizeDefault(s)}>${t(`card.size${s.toUpperCase()}Name`)}</button>`)}
|
|
105
|
+
</div>
|
|
106
|
+
<p class="sub">${tn("cardSizeLead")}</p>
|
|
107
|
+
</section>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const LIMIT_FIELDS = [["portrait", "max_width"], ["portrait", "max_height"], ["portrait", "max_kib"], ["banner", "max_width"], ["banner", "max_height"], ["banner", "max_kib"],
|
|
111
|
+
["instance_quota", "files"], ["instance_quota", "mib"]];
|
|
112
|
+
/**
|
|
113
|
+
* #1629 r4.1 Q3 (b): the identity card's upload limits (fleet.yaml web.appearance). Each field shows what is in force
|
|
114
|
+
* when it differs from what is written (a value beyond its bound is clamped). A lower limit applies at once; a higher
|
|
115
|
+
* one waits for a fleet admin's confirmation (the server decides). Stored pictures are never touched.
|
|
116
|
+
*/
|
|
117
|
+
function CardLimitsCard() {
|
|
118
|
+
const [view, setView] = useState(null);
|
|
119
|
+
const [draft, setDraft] = useState({});
|
|
120
|
+
const [busy, setBusy] = useState(false);
|
|
121
|
+
const [msg, setMsg] = useState(null);
|
|
122
|
+
const load = async () => {
|
|
123
|
+
const r = await call("/api/settings/fleet/appearance");
|
|
124
|
+
// Only an answer of the expected shape is drawn: every group's in-force values, defaults and bounds.
|
|
125
|
+
const ok = r.ok && r.body && ["portrait", "banner", "instance_quota"].every((g) => r.body.effective && r.body.effective[g] && r.body.defaults && r.body.defaults[g] && r.body.bounds && r.body.bounds[g]);
|
|
126
|
+
if (!ok) { setView({ error: true }); return; }
|
|
127
|
+
setView(r.body);
|
|
128
|
+
const d = {};
|
|
129
|
+
for (const [g, k] of LIMIT_FIELDS) { const v = r.body.configured && r.body.configured[g] && r.body.configured[g][k]; d[`${g}.${k}`] = typeof v === "number" ? String(v) : ""; }
|
|
130
|
+
setDraft(d);
|
|
131
|
+
};
|
|
132
|
+
useEffect(() => { load(); }, []);
|
|
133
|
+
if (!view) return null;
|
|
134
|
+
if (view.error) return html`<section class="card s-appearance"><h3>${tn("cardLimitsTitle")}</h3><p class="sub">${tn("cardLimitsUnavailable")}</p></section>`;
|
|
135
|
+
const configured = (g, k) => (view.configured && view.configured[g] && typeof view.configured[g][k] === "number" ? view.configured[g][k] : null);
|
|
136
|
+
const save = async () => {
|
|
137
|
+
const body = {};
|
|
138
|
+
for (const [g, k] of LIMIT_FIELDS) {
|
|
139
|
+
const raw = (draft[`${g}.${k}`] || "").trim();
|
|
140
|
+
const next = raw === "" ? null : Number(raw);
|
|
141
|
+
if (next !== null && !(Number.isInteger(next) && next > 0)) { setMsg(tn("cardLimitsWhole")); return; }
|
|
142
|
+
if (next !== configured(g, k)) (body[g] ??= {})[k] = next;
|
|
143
|
+
}
|
|
144
|
+
if (!Object.keys(body).length) { setMsg(tn("cardLimitsNoChange")); return; }
|
|
145
|
+
setBusy(true); setMsg(null);
|
|
146
|
+
const r = await confirmedWrite("/api/settings/fleet/appearance", { method: "PUT", body, label: tn("cardLimitsTitle") });
|
|
147
|
+
setBusy(false);
|
|
148
|
+
if (r.ok) { setMsg(tn("cardLimitsSaved")); load(); } else setMsg(tn("cardLimitsFailed", (r.body && r.body.error) || r.status));
|
|
149
|
+
};
|
|
150
|
+
const warnings = Array.isArray(view.warnings) ? view.warnings : [];
|
|
151
|
+
return html`<section class="card s-appearance"><h3>${tn("cardLimitsTitle")}</h3>
|
|
152
|
+
<p class="sub">${tn("cardLimitsLead")}</p>
|
|
153
|
+
<fieldset class="form" disabled=${busy}>
|
|
154
|
+
${["portrait", "banner", "instance_quota"].map((g) => html`<div key=${g} class="grid3">
|
|
155
|
+
${LIMIT_FIELDS.filter(([gg]) => gg === g).map(([, k]) => {
|
|
156
|
+
const id = `cardLimit-${g}-${k}`, eff = view.effective[g][k], conf = configured(g, k);
|
|
157
|
+
return html`<div key=${k} class="field"><label for=${id}>${tn(`cardLimit_${g}_${k}`)}</label>
|
|
158
|
+
<input id=${id} type="number" min="1" step="1" placeholder=${String(view.defaults[g][k])} value=${draft[`${g}.${k}`] || ""}
|
|
159
|
+
onInput=${(e) => setDraft({ ...draft, [`${g}.${k}`]: e.target.value })} />
|
|
160
|
+
${conf !== null && conf !== eff ? html`<span class="note">${tn("cardLimitInForce", eff, view.bounds[g][k][0], view.bounds[g][k][1])}</span>` : null}</div>`;
|
|
161
|
+
})}
|
|
162
|
+
</div>`)}
|
|
163
|
+
</fieldset>
|
|
164
|
+
${warnings.length ? html`<ul class="note s-limit-warnings">${warnings.map((w, i) => html`<li key=${i}><code>${w.path}</code>: ${w.message}</li>`)}</ul>` : null}
|
|
165
|
+
<div class="save-row"><button type="button" class="btn btn-primary" disabled=${busy} onClick=${save}>${tn("cardLimitsSave")}</button>
|
|
166
|
+
${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>
|
|
167
|
+
</section>`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** #1629 r2 (d): whether this person's screens show agents' themes (and, as they come, effects) — synced. */
|
|
171
|
+
function ThemesCard() {
|
|
172
|
+
const s = useStore(themesStore);
|
|
173
|
+
return html`<section class="card s-appearance s-synced"><h3>${tn("themesTitle")}</h3>
|
|
174
|
+
<label class="check"><input type="checkbox" checked=${s.shown} disabled=${safeMode()} onChange=${(e) => setThemesShown(e.target.checked)} />
|
|
175
|
+
<span>${tn("themesShow")}</span></label>
|
|
176
|
+
<p class="sub">${safeMode() ? tn("themesSafeMode") : tn("themesShowLead")}</p>
|
|
177
|
+
</section>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** An example to start from: the fleet's counts, as a person's sidebar widget may show them. */
|
|
181
|
+
export const SIDEBAR_WIDGET_EXAMPLE = {
|
|
182
|
+
spec: { v: 1, title: "Fleet", items: [
|
|
183
|
+
{ type: "stat", label: "Working", bind: "fleet.working" },
|
|
184
|
+
{ type: "stat", label: "Needs you", bind: "fleet.needs_you" },
|
|
185
|
+
{ type: "stat", label: "Idle", bind: "fleet.idle" },
|
|
186
|
+
{ type: "stat", label: "Stopped", bind: "fleet.stopped" },
|
|
187
|
+
] },
|
|
188
|
+
data: {},
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* #1629 r4-4: the person's own sidebar widget — a declarative spec and its values, as JSON; drawn under the instance
|
|
192
|
+
* list by the page's own components, with the fleet's counts as the only live values. Synced like the other choices.
|
|
193
|
+
*/
|
|
194
|
+
function SidebarWidgetCard() {
|
|
195
|
+
useStore(sidebarWidgetStore);
|
|
196
|
+
const app = useStore(appStore);
|
|
197
|
+
const saved = sidebarWidgetRaw();
|
|
198
|
+
const [text, setText] = useState(() => (saved ? JSON.stringify(saved, null, 2) : ""));
|
|
199
|
+
const [error, setError] = useState("");
|
|
200
|
+
let parsed = null, problem = "";
|
|
201
|
+
if (text.trim()) {
|
|
202
|
+
try { parsed = JSON.parse(text); problem = widgetProblem("sidebar", parsed) || ""; } catch { problem = tn("widgetNotJson"); }
|
|
203
|
+
}
|
|
204
|
+
const preview = parsed && !problem ? checkWidget("sidebar", parsed) : null;
|
|
205
|
+
const save = () => {
|
|
206
|
+
if (!parsed || problem) { setError(problem || tn("widgetNotJson")); return; }
|
|
207
|
+
setError("");
|
|
208
|
+
setSidebarWidget(parsed);
|
|
209
|
+
toast(tn("widgetSaved"));
|
|
210
|
+
};
|
|
211
|
+
const remove = () => { setSidebarWidget(null); setText(""); setError(""); toast(tn("widgetRemoved")); };
|
|
212
|
+
return html`<section class="card s-appearance s-synced s-widget"><h3>${tn("widgetTitle")}</h3>
|
|
213
|
+
<p class="sub">${tn("widgetLead")}</p>
|
|
214
|
+
<label class="field"><span class="sr-only">${tn("widgetTitle")}</span>
|
|
215
|
+
<textarea class="s-widget-text" rows="10" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text}
|
|
216
|
+
onInput=${(e) => { setText(e.target.value); setError(""); }}></textarea></label>
|
|
217
|
+
${error || problem ? html`<p class="err" role="alert">${error || problem}</p>` : null}
|
|
218
|
+
${preview ? html`<div class="s-widget-preview"><${AgentWidget} slot="sidebar" widget=${preview} binds=${fleetBinds(app.instances, app.exec, app.awaiting)} /></div>` : null}
|
|
219
|
+
<div class="s-widget-actions">
|
|
220
|
+
<button type="button" class="btn btn-primary" disabled=${!parsed || !!problem} onClick=${save}><${Icon} name="check" size=${16} />${tn("widgetSave")}</button>
|
|
221
|
+
${!text.trim() ? html`<button type="button" class="btn" onClick=${() => setText(JSON.stringify(SIDEBAR_WIDGET_EXAMPLE, null, 2))}>${tn("widgetExample")}</button>` : null}
|
|
222
|
+
${saved ? html`<button type="button" class="btn" onClick=${remove}>${tn("widgetRemove")}</button>` : null}
|
|
223
|
+
</div>
|
|
224
|
+
</section>`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function OwnerCssCard() {
|
|
228
|
+
const [state, setState] = useState(null);
|
|
229
|
+
const [text, setText] = useState("");
|
|
230
|
+
const [busy, setBusy] = useState(false);
|
|
231
|
+
const [error, setError] = useState("");
|
|
232
|
+
const trial = useStore(ownerCssTrial);
|
|
233
|
+
const file = useRef(null);
|
|
234
|
+
const load = async (opts = {}) => {
|
|
235
|
+
const r = await call("/ui/owner-css/state");
|
|
236
|
+
if (!r.ok || !r.body) { setState({ failed: true }); return; }
|
|
237
|
+
setState(r.body);
|
|
238
|
+
if (opts.text !== false) setText(r.body.text || "");
|
|
239
|
+
};
|
|
240
|
+
useEffect(() => { load(); }, []);
|
|
241
|
+
// A trial answered (kept or reverted) changes what is in force: read it again, keeping the editor's text.
|
|
242
|
+
useEffect(() => { if (state) load({ text: false }); }, [trial && trial.sha]);
|
|
243
|
+
if (!state) return html`<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="sub">…</p></section>`;
|
|
244
|
+
if (state.failed) return html`<section class="card s-appearance"><h3>${tn("cssTitle")}</h3><p class="err">${tn("cssLoadFailed")}</p></section>`;
|
|
245
|
+
|
|
246
|
+
const save = async () => {
|
|
247
|
+
setBusy(true); setError("");
|
|
248
|
+
const r = await postJson("/ui/owner-css", { text });
|
|
249
|
+
setBusy(false);
|
|
250
|
+
if (!r.ok) { setError(refusal(r.body)); return; }
|
|
251
|
+
startOwnerCssTrial(r.body.trial, r.body.sha, r.body.previous);
|
|
252
|
+
toast(tn("cssTrying"));
|
|
253
|
+
load({ text: false });
|
|
254
|
+
};
|
|
255
|
+
const restore = async (sha) => {
|
|
256
|
+
setBusy(true); setError("");
|
|
257
|
+
const r = await postJson("/ui/owner-css/restore", { sha });
|
|
258
|
+
setBusy(false);
|
|
259
|
+
if (!r.ok) { setError(refusal(r.body)); return; }
|
|
260
|
+
setOwnerCssActive(r.body.active);
|
|
261
|
+
load();
|
|
262
|
+
};
|
|
263
|
+
const addAsset = async () => {
|
|
264
|
+
const f = file.current && file.current.files && file.current.files[0];
|
|
265
|
+
if (!f) return;
|
|
266
|
+
setBusy(true); setError("");
|
|
267
|
+
const r = await call("/ui/owner-css/asset", { method: "POST", headers: { "Content-Type": "application/octet-stream" }, body: f });
|
|
268
|
+
setBusy(false);
|
|
269
|
+
if (file.current) file.current.value = "";
|
|
270
|
+
if (!r.ok) { setError(refusal(r.body)); return; }
|
|
271
|
+
load({ text: false });
|
|
272
|
+
};
|
|
273
|
+
const removeAsset = async (id) => {
|
|
274
|
+
setBusy(true); setError("");
|
|
275
|
+
const r = await call(`/ui/owner-css/asset/${id}`, { method: "DELETE" });
|
|
276
|
+
setBusy(false);
|
|
277
|
+
if (!r.ok) { setError(refusal(r.body)); return; }
|
|
278
|
+
load({ text: false });
|
|
279
|
+
};
|
|
280
|
+
const copyRef = (id) => {
|
|
281
|
+
const ref = `url("asset:${id}")`;
|
|
282
|
+
try { navigator.clipboard.writeText(ref).then(() => toast(tn("cssCopied", ref)), () => toast(ref)); } catch { toast(ref); }
|
|
283
|
+
};
|
|
284
|
+
const inForce = state.active
|
|
285
|
+
? (state.pending ? tn("cssOnTrial", short(state.active)) : tn("cssInForce", short(state.active)))
|
|
286
|
+
: tn("cssNone");
|
|
287
|
+
const assetBytes = state.assets.reduce((n, a) => n + a.bytes, 0);
|
|
288
|
+
return html`<section class="card s-appearance s-owner-css"><h3>${tn("cssTitle")}</h3>
|
|
289
|
+
<p class="sub">${tn("cssLead")}</p>
|
|
290
|
+
<p class="s-css-status" role="status">${inForce}</p>
|
|
291
|
+
<label class="field"><span class="sr-only">${tn("cssTitle")}</span>
|
|
292
|
+
<textarea class="s-css-text" rows="14" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text} disabled=${busy}
|
|
293
|
+
onInput=${(e) => setText(e.target.value)}></textarea></label>
|
|
294
|
+
${error ? html`<p class="err" role="alert">${error}</p>` : null}
|
|
295
|
+
<div class="s-css-actions">
|
|
296
|
+
<button type="button" class="btn btn-primary" disabled=${busy || !text.trim()} onClick=${save}><${Icon} name="check" size=${16} />${tn("cssSave")}</button>
|
|
297
|
+
${state.active ? html`<button type="button" class="btn" disabled=${busy} onClick=${() => restore(null)}>${tn("cssOff")}</button>` : null}
|
|
298
|
+
</div>
|
|
299
|
+
${state.versions.length ? html`<details class="s-css-versions"><summary>${tn("cssVersions", state.versions.length)}</summary>
|
|
300
|
+
<ul class="s-css-list">${state.versions.map((v) => html`<li key=${v.sha}>
|
|
301
|
+
<code>${short(v.sha)}</code><span class="muted">${new Date(v.at).toLocaleString()} · ${kib(v.bytes)}</span>
|
|
302
|
+
${v.sha === state.active ? html`<span class="pill">${tn("cssCurrent")}</span>`
|
|
303
|
+
: html`<button type="button" class="btn btn-sm" disabled=${busy} onClick=${() => restore(v.sha)}>${tn("cssRestore")}</button>`}
|
|
304
|
+
</li>`)}</ul></details>` : null}
|
|
305
|
+
<h4>${tn("cssAssets")}</h4>
|
|
306
|
+
<p class="sub">${tn("cssAssetsLead", state.assets.length, state.limits.maxAssets, kib(assetBytes))}</p>
|
|
307
|
+
${state.assets.length ? html`<ul class="s-css-list">${state.assets.map((a) => html`<li key=${a.id}>
|
|
308
|
+
<code>${a.id}</code><span class="muted">${a.kind} · ${kib(a.bytes)}</span>
|
|
309
|
+
<button type="button" class="btn btn-sm" onClick=${() => copyRef(a.id)}>${tn("cssAssetCopy")}</button>
|
|
310
|
+
<button type="button" class="btn btn-sm btn-ghost" disabled=${busy} onClick=${() => removeAsset(a.id)}>${tn("cssAssetRemove")}</button>
|
|
311
|
+
</li>`)}</ul>` : null}
|
|
312
|
+
<label class="field"><span>${tn("cssAssetAdd")}</span>
|
|
313
|
+
<input ref=${file} type="file" accept="image/png,image/webp,image/jpeg,image/gif,font/woff2,.woff2" disabled=${busy} onChange=${addAsset} /></label>
|
|
314
|
+
</section>`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function DeviceCard() {
|
|
318
|
+
const [off, setOff] = useState(ownerCssOffHere());
|
|
319
|
+
return html`<section class="card s-appearance s-local"><h3>${tn("cssDeviceTitle")}</h3>
|
|
320
|
+
<label class="check"><input type="checkbox" checked=${off} onChange=${(e) => { setOwnerCssOffHere(e.target.checked); setOff(ownerCssOffHere()); }} />
|
|
321
|
+
<span>${tn("cssDeviceOff")}</span></label>
|
|
322
|
+
<p class="sub">${tn("cssDeviceOffLead")}</p>
|
|
323
|
+
<p class="sub">${tn("cssSafe")}</p>
|
|
324
|
+
</section>`;
|
|
325
|
+
}
|
|
@@ -10,11 +10,11 @@ import { Icon } from "/assets/ui-icons.js";
|
|
|
10
10
|
import { toast } from "/assets/ui-toast.js";
|
|
11
11
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
12
12
|
import { api, confirmedWrite, newKey } from "./settings-confirm.js";
|
|
13
|
-
import { botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
|
|
13
|
+
import { adminUserIdError, adminUserIdPattern, botHandle, TokenEnvNote, TokenField, verifyBotToken } from "./settings-token.js";
|
|
14
14
|
import { DiscordServerPicker } from "./settings-discord.js";
|
|
15
15
|
import { startOperation } from "./settings-apply.js";
|
|
16
16
|
import {
|
|
17
|
-
ACCESS_MODES, AGENT_FIELDS, AGENT_MODES, BOT_FIELDS, CH_TYPES, CLASSIC_FIELDS, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
|
|
17
|
+
ACCESS_MODES, AGENT_FIELDS, AGENT_MODES, BOT_FIELDS, CH_TYPES, CLASSIC_FIELDS, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
|
|
18
18
|
VISIBILITY_MODES, accessConfirmations, agentForm, agentPatch, backendOptions, changedFields, channelId, channelsOf,
|
|
19
19
|
chLabel, hasOwn, impactOf, nonNegative, nonNegativeInteger, positive, replyGuardSupported, sameValue, visibilityDefault,
|
|
20
20
|
webEchoIsGroup, worstImpact,
|
|
@@ -242,7 +242,7 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
|
|
|
242
242
|
<div class="field">${L(`${tn("hangTimeout")} (${tn("minutes")})`, "instance.hang_detector", "ag-hang")}
|
|
243
243
|
<${Override} inherit=${f.inherit.hang} onInherit=${setInherit("hang")}>
|
|
244
244
|
<input id="ag-hang" type="number" min="0.1" step="0.5" class=${errors.hang ? "invalid" : ""} disabled=${f.inherit.hang}
|
|
245
|
-
value=${shown("hang", f.hang, String((defaults.hang_detector && defaults.hang_detector.timeout_minutes) ??
|
|
245
|
+
value=${shown("hang", f.hang, String((defaults.hang_detector && defaults.hang_detector.timeout_minutes) ?? HANG_TIMEOUT_DEFAULT_MINUTES))} onInput=${(e) => set("hang")(e.target.value)} /></${Override}>
|
|
246
246
|
<${Feedback} error=${errors.hang} /></div>
|
|
247
247
|
${[["agent_mode", "agentMode", AGENT_MODES, defaults.agent_mode || "mcp"], ["tool_set", "toolSet", TOOL_SETS, defaults.tool_set || "full"],
|
|
248
248
|
["tool_progress", "toolProgress", TOOL_PROGRESS, defaults.tool_progress ?? "off"],
|
|
@@ -466,17 +466,18 @@ export function BotDialog({ id, ctx, onClose }) {
|
|
|
466
466
|
*/
|
|
467
467
|
export function NewBotDialog({ ctx, onClose }) {
|
|
468
468
|
const lease = useLease("new-bot");
|
|
469
|
-
const [f, setF] = useState({ type: "discord", id: "", token: "", identity: null, guilds: [], group: "", general: "" });
|
|
469
|
+
const [f, setF] = useState({ type: "discord", id: "", token: "", identity: null, guilds: [], group: "", general: "", admin: "" });
|
|
470
470
|
const [plan, setPlan] = useState(null);
|
|
471
471
|
const [busy, setBusy] = useState(false);
|
|
472
472
|
const [err, setErr] = useState("");
|
|
473
473
|
// The form's revision (#1529 review): any change to the token, platform, id or group moves it; a Verify or a plan
|
|
474
474
|
// that began before is dropped when it lands — never restoring an old token, naming it, or planning for it.
|
|
475
475
|
const rev = useRef(0);
|
|
476
|
-
const body = (x) => ({ platform: x.type, connection_only: true, ...(x.id.trim() ? { channel_id: x.id.trim() } : {}),
|
|
476
|
+
const body = (x) => ({ platform: x.type, connection_only: true, admin_user_id: x.admin.trim(), ...(x.id.trim() ? { channel_id: x.id.trim() } : {}),
|
|
477
477
|
...(x.group.trim() ? (x.type === "discord" ? { guild_id: x.group.trim() } : { group_id: x.group.trim() }) : {}),
|
|
478
478
|
...(x.type === "discord" && x.general.trim() ? { general_channel_id: x.general.trim() } : {}) });
|
|
479
479
|
const planFor = async (x) => {
|
|
480
|
+
if (adminUserIdError(x.type, x.admin.trim())) { setPlan(null); return null; }
|
|
480
481
|
const at = rev.current;
|
|
481
482
|
const res = await api("/api/settings/quickstart/plan", { method: "POST", body: JSON.stringify(body(x)) }).catch(() => ({ ok: false, body: {} }));
|
|
482
483
|
if (!lease.current() || rev.current !== at) return null;
|
|
@@ -501,6 +502,8 @@ export function NewBotDialog({ ctx, onClose }) {
|
|
|
501
502
|
};
|
|
502
503
|
const save = async () => {
|
|
503
504
|
if (!f.identity || !f.identity.valid) { setErr(tn("wizardNeedVerify")); return; }
|
|
505
|
+
const adminError = adminUserIdError(f.type, f.admin.trim());
|
|
506
|
+
if (adminError) { setErr(adminError); return; }
|
|
504
507
|
const x = f;
|
|
505
508
|
setBusy(true);
|
|
506
509
|
const p = await planFor(x);
|
|
@@ -514,7 +517,7 @@ export function NewBotDialog({ ctx, onClose }) {
|
|
|
514
517
|
};
|
|
515
518
|
// A change of platform or id is another connection: what was verified and planned no longer applies. The server or
|
|
516
519
|
// group does not change the id or the token's name, so the plan (and its name under Advanced) stays.
|
|
517
|
-
const set = (k) => (v) => { rev.current++; setF((x) => ({ ...x, [k]: v, ...(k === "type" ? { identity: null, guilds: [], group: "", general: "" } : {}) })); if (k !== "group" && k !== "general") setPlan(null); };
|
|
520
|
+
const set = (k) => (v) => { rev.current++; setF((x) => ({ ...x, [k]: v, ...(k === "type" ? { identity: null, guilds: [], group: "", general: "", admin: "" } : {}) })); if (k !== "group" && k !== "general") setPlan(null); };
|
|
518
521
|
return html`<${Dialog} title=${tn("newBot")} onClose=${onClose} busy=${busy}
|
|
519
522
|
actions=${html`<button type="button" class="btn" disabled=${busy} onClick=${onClose}>${tn("cancel")}</button>
|
|
520
523
|
<button type="button" class="btn btn-primary" disabled=${busy || !f.identity || !f.identity.valid} onClick=${save}>${tn("save")}</button>`}>
|
|
@@ -528,6 +531,8 @@ export function NewBotDialog({ ctx, onClose }) {
|
|
|
528
531
|
guild=${f.group} onGuild=${set("group")} channel=${f.general} onChannel=${set("general")} />`
|
|
529
532
|
: html`<div class="field"><label for="nb-group">${tn("groupIdField")}</label>
|
|
530
533
|
<input id="nb-group" type="text" value=${f.group} onInput=${(e) => set("group")(e.target.value.trim())} /></div>`}
|
|
534
|
+
<div class="field"><label for="nb-user">${tn("wizardAdminUser")}</label>
|
|
535
|
+
<input id="nb-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(f.type)} value=${f.admin} onInput=${(e) => set("admin")(e.target.value.trim())} /></div>
|
|
531
536
|
<${Drawer} title=${tn("advancedSection")}>
|
|
532
537
|
<div class="field"><label for="nb-id">${tn("connectionIdOptional")}</label><input id="nb-id" type="text" placeholder=${plan ? plan.channel_id : f.type}
|
|
533
538
|
value=${f.id} onInput=${(e) => set("id")(e.target.value)} onBlur=${() => { if (f.identity && f.identity.valid) planFor(f); }} /></div>
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// #1629 B2: library management only. This module never loads Worker code or creates a preview/frame.
|
|
2
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
3
|
+
import { register, t } from "/assets/app-i18n.js";
|
|
4
|
+
import { appStore, useStore } from "/assets/app-store.js";
|
|
5
|
+
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
6
|
+
import { OwnerEffects } from "./settings-owner-effects.js";
|
|
7
|
+
|
|
8
|
+
register("effectSettings", {
|
|
9
|
+
en: {
|
|
10
|
+
title: "Effects", lead: "Manage effect files and save selections for a chat. Saving replaces that chat’s parameters with the values below.", folder: "Upload an effect folder", files: "Choose effect.json and worker.js, with optional raster assets. Up to 1 MiB after processing.",
|
|
11
|
+
inUse: "This effect is still in use and cannot be deleted.", quota: "Your library reached its 10-effect or 8 MiB limit. Delete unused copies first.", rateLimited: "Too many changes. Wait a moment, then try again.",
|
|
12
|
+
loading: "Loading effects…", retry: "Reload library", empty: "No effects in your library.", failed: "Couldn't load the library. Try again.", actionFailed: "Couldn't save this change. Try again.", savedRefresh: "Saved. Reload the library to see the result.",
|
|
13
|
+
uploadFailed: "Couldn't upload this folder. Check its files and size, then try again.", public: "Effect management isn't available on a temporary public link.", readOnly: "Read-only session: sign in as an admin to change effects.",
|
|
14
|
+
yours: "Your copy", shared: "Shared with you", share: "Share your copy with agents and people on this fleet", delete: "Delete your copy", deleteConfirm: "Delete your unused copy of this effect? Other people's copies are kept.",
|
|
15
|
+
chat: "Chat", chooseChat: "Choose a chat", apply: "Save for this chat", remove: "Remove from this chat", saved: "Saved.", history: "Profile history", unmeasured: "Not measured yet", unavailable: "Profile unavailable", reported: "Reported host observations; they don't prove an effect's cost.",
|
|
16
|
+
fps: "Host FPS p50 / p10", tasks: "Host long task p95", noTasks: "Not observed", standard: "Standard device", constrained: "Constrained device", params: "Parameters", badParams: "Check the parameter values.", more: "Show more", count: "{0} effects", one: "1 effect", profileFailed: "Couldn't read profile history. Try again.",
|
|
17
|
+
},
|
|
18
|
+
"zh-TW": {
|
|
19
|
+
title: "效果", lead: "管理效果檔案,並儲存聊天的效果選擇。儲存時會以下方數值取代該聊天的效果參數。", folder: "上傳效果資料夾", files: "選擇 effect.json、worker.js,以及可選的點陣圖素材;處理後最多 1 MiB。",
|
|
20
|
+
inUse: "這個效果仍在使用中,無法刪除。", quota: "你的程式庫已達 10 個效果或 8 MiB 上限,請先刪除未使用的副本。", rateLimited: "變更太頻繁,請稍候再試。",
|
|
21
|
+
loading: "正在載入效果…", retry: "重新載入程式庫", empty: "程式庫中還沒有效果。", failed: "無法載入程式庫,請重試。", actionFailed: "無法儲存變更,請重試。", savedRefresh: "已儲存,請重新載入程式庫查看結果。",
|
|
22
|
+
uploadFailed: "無法上傳這個資料夾,請檢查檔案與大小後重試。", public: "臨時公開連結不提供效果管理。", readOnly: "目前為唯讀登入,請以管理員身分登入才能修改效果。",
|
|
23
|
+
yours: "你持有的副本", shared: "他人與你共享", share: "將你的副本共享給這個 fleet 的 agent 與使用者", delete: "刪除你的副本", deleteConfirm: "刪除你未使用的效果副本?其他人的副本會保留。",
|
|
24
|
+
chat: "聊天", chooseChat: "選擇聊天", apply: "儲存到這個聊天", remove: "從這個聊天移除", saved: "已儲存。", history: "效能回報紀錄", unmeasured: "尚未測量", unavailable: "無法取得效能回報", reported: "這是主機觀測回報,不能證明效果本身的資源用量。",
|
|
25
|
+
fps: "主機 FPS p50/p10", tasks: "主機長任務 p95", noTasks: "未觀測到", standard: "一般裝置", constrained: "受限裝置", params: "參數", badParams: "請檢查參數值。", more: "顯示更多", count: "{0} 個效果", one: "1 個效果", profileFailed: "無法讀取效能回報紀錄,請重試。",
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const tn = (key, ...args) => t(`effectSettings.${key}`, ...args);
|
|
29
|
+
const REFUSALS = Object.freeze({ effect_in_use: "inUse", effect_quota: "quota", rate_limited: "rateLimited" });
|
|
30
|
+
const refusal = (body, fallback) => tn(typeof body?.error === "string" && Object.hasOwn(REFUSALS, body.error) ? REFUSALS[body.error] : fallback);
|
|
31
|
+
const ID = /^[0-9a-f]{32}$/;
|
|
32
|
+
const PATH = /^(?:effect\.json|worker\.js|poster\.png|cursor\.png|assets\/[a-z0-9][a-z0-9_-]{0,63}\.(?:png|jpe?g|webp|gif))$/;
|
|
33
|
+
const RAW_MAX = 8 * 1024 * 1024, WIRE_MAX = 12 * 1024 * 1024;
|
|
34
|
+
const plain = (v) => !!v && typeof v === "object" && Object.getPrototypeOf(v) === Object.prototype;
|
|
35
|
+
const publicLink = () => typeof document !== "undefined" && document.body?.dataset.publicLink === "1";
|
|
36
|
+
|
|
37
|
+
/** File metadata bounds are checked before reading. No directory path reaches the server. */
|
|
38
|
+
export async function encodeEffectFolder(input, signal) {
|
|
39
|
+
const files = Array.from(input || []);
|
|
40
|
+
if (files.length < 2 || files.length > 20) throw Error("effect_files");
|
|
41
|
+
const nested = files.some(f => !!f?.webkitRelativePath), paths = new Set(); let root = null, raw = 0, assets = 0;
|
|
42
|
+
const rows = files.map(file => {
|
|
43
|
+
if (!file || typeof file.name !== "string" || !Number.isSafeInteger(file.size) || file.size < 0 || (raw += file.size) > RAW_MAX || typeof file.arrayBuffer !== "function") throw Error("effect_files");
|
|
44
|
+
let path = file.name;
|
|
45
|
+
if (nested) {
|
|
46
|
+
const relative = file.webkitRelativePath;
|
|
47
|
+
if (typeof relative !== "string" || relative.startsWith("/") || relative.includes("\\")) throw Error("effect_files");
|
|
48
|
+
const at = relative.indexOf("/"); if (at < 1) throw Error("effect_files");
|
|
49
|
+
const prefix = relative.slice(0, at); if (root === null) root = prefix; if (root !== prefix) throw Error("effect_files");
|
|
50
|
+
path = relative.slice(at + 1);
|
|
51
|
+
}
|
|
52
|
+
if (!PATH.test(path) || paths.has(path) || (path.startsWith("assets/") && ++assets > 16)) throw Error("effect_files");
|
|
53
|
+
paths.add(path); return { file, path };
|
|
54
|
+
});
|
|
55
|
+
if (!paths.has("effect.json") || !paths.has("worker.js")) throw Error("effect_files");
|
|
56
|
+
const encoded = [];
|
|
57
|
+
for (const { file, path } of rows) {
|
|
58
|
+
if (signal?.aborted) throw Error("cancelled");
|
|
59
|
+
const buffer = await file.arrayBuffer(); if (signal?.aborted) throw Error("cancelled");
|
|
60
|
+
if (!(buffer instanceof ArrayBuffer) || buffer.byteLength !== file.size) throw Error("effect_files");
|
|
61
|
+
const bytes = new Uint8Array(buffer), chunks = [];
|
|
62
|
+
for (let i = 0; i < bytes.length; i += 8192) chunks.push(String.fromCharCode(...bytes.subarray(i, i + 8192)));
|
|
63
|
+
encoded.push({ path, data_b64: btoa(chunks.join("")) });
|
|
64
|
+
}
|
|
65
|
+
const body = JSON.stringify(encoded); if (new TextEncoder().encode(body).byteLength > WIRE_MAX) throw Error("effect_files");
|
|
66
|
+
return body;
|
|
67
|
+
}
|
|
68
|
+
export function effectCatalog(body) {
|
|
69
|
+
if (!plain(body) || typeof body.canWrite !== "boolean" || !Array.isArray(body.effects)) return null;
|
|
70
|
+
const seen = new Set(), effects = [];
|
|
71
|
+
for (const e of body.effects) {
|
|
72
|
+
if (!plain(e) || typeof e.id !== "string" || !ID.test(e.id) || seen.has(e.id) || !plain(e.manifest) || typeof e.manifest.title !== "string" || e.manifest.title.length < 1 || e.manifest.title.length > 40 || !Number.isInteger(e.bytes) || e.bytes < 1 || e.bytes > 1048576 || typeof e.ownClaim !== "boolean" || typeof e.sharedByYou !== "boolean" || !e.ownClaim && e.sharedByYou) return null;
|
|
73
|
+
if (effectParamOverrides(e.manifest) === null) return null;
|
|
74
|
+
seen.add(e.id); effects.push(e);
|
|
75
|
+
}
|
|
76
|
+
return { canWrite: body.canWrite, effects };
|
|
77
|
+
}
|
|
78
|
+
/** UI draft conversion only; the host independently checks the manifest and every override on apply. */
|
|
79
|
+
export function effectParamOverrides(manifest, draft = {}) {
|
|
80
|
+
if (!plain(manifest) || !plain(draft) || manifest.params !== undefined && !plain(manifest.params)) return null;
|
|
81
|
+
const defs = manifest.params || {}, out = Object.create(null);
|
|
82
|
+
if (Object.keys(defs).length > 16 || Object.keys(draft).some(k => !Object.hasOwn(defs, k))) return null;
|
|
83
|
+
for (const [key, d] of Object.entries(defs)) {
|
|
84
|
+
if (!/^[a-z][a-z0-9_]{0,31}$/.test(key) || !plain(d)) return null;
|
|
85
|
+
const supplied = Object.hasOwn(draft, key); let v = supplied ? draft[key] : d.default;
|
|
86
|
+
if (d.type === "int" || d.type === "number") {
|
|
87
|
+
if (supplied && typeof v === "string" && v.trim() !== "") v = Number(v);
|
|
88
|
+
if (typeof v !== "number" || !Number.isFinite(v) || !Number.isFinite(d.min) || !Number.isFinite(d.max) || v < d.min || v > d.max || d.type === "int" && !Number.isSafeInteger(v)) return null;
|
|
89
|
+
if (d.type === "number" && d.step !== undefined) {
|
|
90
|
+
const n = (v - d.min) / d.step;
|
|
91
|
+
if (!(d.step > 0) || !Number.isFinite(n) || Math.abs(n - Math.round(n)) > 1e-8) return null;
|
|
92
|
+
}
|
|
93
|
+
} else if (d.type === "bool") { if (typeof v !== "boolean") return null; }
|
|
94
|
+
else if (d.type === "enum") { if (!Array.isArray(d.values) || d.values.length > 16 || typeof v !== "string" || !d.values.includes(v)) return null; }
|
|
95
|
+
else if (d.type === "color") { if (typeof v !== "string" || !/^#[0-9a-fA-F]{6}$/.test(v)) return null; v = v.toLowerCase(); }
|
|
96
|
+
else return null;
|
|
97
|
+
if (v !== d.default) out[key] = v;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
function Params({ manifest, draft, update, disabled }) {
|
|
102
|
+
const defs = plain(manifest.params) ? Object.entries(manifest.params).slice(0, 16) : [];
|
|
103
|
+
if (!defs.length) return null;
|
|
104
|
+
return html`<fieldset class="s-fx-params" disabled=${disabled}><legend>${tn("params")}</legend>${defs.map(([key, d]) => {
|
|
105
|
+
const value = Object.hasOwn(draft, key) ? draft[key] : d.default;
|
|
106
|
+
if (d.type === "bool") return html`<label key=${key} class="check"><input data-param=${key} type="checkbox" checked=${value} onChange=${e => update(key, e.target.checked)} />${key}</label>`;
|
|
107
|
+
if (d.type === "enum") return html`<label key=${key}>${key}<select data-param=${key} value=${value} onChange=${e => update(key, e.target.value)}>${d.values.map(v => html`<option key=${v} value=${v}>${v}</option>`)}</select></label>`;
|
|
108
|
+
return html`<label key=${key}>${key}<input data-param=${key} type=${d.type === "color" ? "color" : "number"} min=${d.min} max=${d.max} step=${d.type === "int" ? 1 : d.step || "any"} value=${value} onInput=${e => update(key, e.target.value)} /></label>`;
|
|
109
|
+
})}</fieldset>`;
|
|
110
|
+
}
|
|
111
|
+
async function call(path, init, signal) {
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetch(path, { credentials: "same-origin", ...init, signal });
|
|
114
|
+
let body = null; try { body = await res.json(); } catch { /* unknown response refuses */ }
|
|
115
|
+
return { ok: res.ok && !body?.error, body };
|
|
116
|
+
} catch { return { ok: false, body: null }; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Profile({ value }) {
|
|
120
|
+
if (!value || value.error || !Array.isArray(value.records)) return html`<p class="sub">${tn("unavailable")}</p>`;
|
|
121
|
+
if (value.status === "unmeasured" || !value.records.length) return html`<p class="sub">${tn("unmeasured")}</p>`;
|
|
122
|
+
return html`<div class="s-fx-profile"><p class="sub">${tn("reported")}</p>${value.records.map((p, i) => {
|
|
123
|
+
const valid = p && ["standard", "constrained"].includes(p.device) && [p.fps_p50, p.fps_p10].every(n => typeof n === "number" && Number.isFinite(n) && n >= 0 && n <= 1000);
|
|
124
|
+
if (!valid) return html`<p key=${i} class="sub">${tn("unavailable")}</p>`;
|
|
125
|
+
return html`<div key=${i}><strong>${tn(p.device)}</strong><p class="sub">${tn("fps")}: ${p.fps_p50} / ${p.fps_p10} · ${tn("tasks")}: ${typeof p.long_task_p95 === "number" && Number.isFinite(p.long_task_p95) ? `${p.long_task_p95} ms` : tn("noTasks")}</p></div>`;
|
|
126
|
+
})}</div>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Admin changes remain authoritative on the host; canWrite is a display hint only. */
|
|
130
|
+
export function EffectsCard() {
|
|
131
|
+
const app = useStore(appStore), [view, setView] = useState(null), [error, setError] = useState(null), [busy, setBusy] = useState(false), [message, setMessage] = useState(null);
|
|
132
|
+
const [target, setTarget] = useState(""), [limit, setLimit] = useState(50), [history, setHistory] = useState({}), [drafts, setDrafts] = useState({});
|
|
133
|
+
const uploadInput = useRef(null);
|
|
134
|
+
const life = useRef({ alive: false, seq: 0, controller: null, pending: false });
|
|
135
|
+
const current = (seq) => life.current.alive && seq === life.current.seq;
|
|
136
|
+
const begin = () => { if (!life.current.alive || life.current.pending || publicLink()) return null; life.current.pending = true; life.current.controller?.abort(); const controller = new AbortController(), seq = ++life.current.seq; life.current.controller = controller; return { seq, signal: controller.signal }; };
|
|
137
|
+
const finish = seq => { if (current(seq)) { life.current.pending = false; setBusy(false); } };
|
|
138
|
+
const load = async (op) => {
|
|
139
|
+
const r = await call("/ui/effects", {}, op.signal); if (!current(op.seq)) return false;
|
|
140
|
+
const data = r.ok ? effectCatalog(r.body) : null; setView(data); setHistory({}); setError(data ? null : tn("failed")); return !!data;
|
|
141
|
+
};
|
|
142
|
+
const reload = async () => { const op = begin(); if (!op) return; setBusy(true); setMessage(null); await load(op); finish(op.seq); };
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
life.current.alive = true;
|
|
145
|
+
if (!publicLink()) reload();
|
|
146
|
+
return () => { life.current.alive = false; life.current.seq++; life.current.controller?.abort(); };
|
|
147
|
+
}, []);
|
|
148
|
+
if (publicLink()) return html`<section class="card s-appearance s-effects"><h3>${tn("title")}</h3><p class="sub">${tn("public")}</p></section>`;
|
|
149
|
+
const names = app.ready ? [...new Set((app.instances || []).map(i => i?.name).filter(n => typeof n === "string" && n.length > 0 && n.length <= 128))] : [];
|
|
150
|
+
const selected = names.includes(target) ? target : "";
|
|
151
|
+
const change = async (path, body, method = "POST") => {
|
|
152
|
+
if (busy || !view?.canWrite) return;
|
|
153
|
+
const op = begin(); if (!op) return; setBusy(true); setMessage(null);
|
|
154
|
+
const r = await call(path, { method, ...(body === undefined ? {} : { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }) }, op.signal);
|
|
155
|
+
if (!current(op.seq)) return;
|
|
156
|
+
if (!r.ok) setMessage(refusal(r.body, "actionFailed")); else { const refreshed = await load(op); if (current(op.seq)) setMessage(tn(refreshed ? "saved" : "savedRefresh")); }
|
|
157
|
+
finish(op.seq);
|
|
158
|
+
};
|
|
159
|
+
const upload = async (event) => {
|
|
160
|
+
const input = event.currentTarget, files = Array.from(input.files || []); input.value = "";
|
|
161
|
+
if (busy || !view?.canWrite || !files.length) return;
|
|
162
|
+
const op = begin(); if (!op) return; setBusy(true); setMessage(null);
|
|
163
|
+
try {
|
|
164
|
+
const body = await encodeEffectFolder(files, op.signal); if (!current(op.seq)) return;
|
|
165
|
+
const r = await call("/ui/effects/upload", { method: "POST", headers: { "Content-Type": "application/json" }, body }, op.signal);
|
|
166
|
+
if (!current(op.seq)) return;
|
|
167
|
+
if (!r.ok) setMessage(refusal(r.body, "uploadFailed")); else { const refreshed = await load(op); if (current(op.seq)) setMessage(tn(refreshed ? "saved" : "savedRefresh")); }
|
|
168
|
+
} catch { if (current(op.seq)) setMessage(tn("uploadFailed")); }
|
|
169
|
+
finish(op.seq);
|
|
170
|
+
};
|
|
171
|
+
const profile = async (id) => {
|
|
172
|
+
if (busy) return; const op = begin(); if (!op) return; setBusy(true);
|
|
173
|
+
const r = await call(`/ui/effects/profile/${id}`, {}, op.signal); if (!current(op.seq)) return;
|
|
174
|
+
if (r.ok && r.body && Array.isArray(r.body.records) && r.body.records.length <= 40) setHistory(h => ({ ...h, [id]: r.body })); else setMessage(tn("profileFailed"));
|
|
175
|
+
finish(op.seq);
|
|
176
|
+
};
|
|
177
|
+
const askDelete = async id => {
|
|
178
|
+
if (!view?.canWrite) return; const op = begin(); if (!op) return; setBusy(true);
|
|
179
|
+
const yes = await confirmDialog({ message: tn("deleteConfirm"), confirmLabel: tn("delete"), danger: true });
|
|
180
|
+
if (!current(op.seq)) return; finish(op.seq);
|
|
181
|
+
if (yes) await change(`/ui/effects/${id}`, undefined, "DELETE");
|
|
182
|
+
};
|
|
183
|
+
return html`<section class="card s-appearance s-effects"><h3>${tn("title")}</h3><p class="sub">${tn("lead")}</p>
|
|
184
|
+
<div class="s-fx-actions"><button type="button" class="btn" disabled=${busy} onClick=${reload}>${tn("retry")}</button>
|
|
185
|
+
${view?.canWrite ? html`<button class="btn s-fx-choose" type="button" disabled=${busy} onClick=${() => uploadInput.current?.click()}>${tn("folder")}</button><input ref=${uploadInput} class="offscreen s-fx-upload" aria-label=${tn("folder")} tabindex="-1" type="file" webkitdirectory=${true} directory=${true} multiple disabled=${busy} onChange=${upload} />` : null}</div>
|
|
186
|
+
${busy ? html`<p class="sub" role="status">${tn("loading")}</p>` : null}${error ? html`<p class="err" role="status">${error}</p>` : null}${message ? html`<p class="sub" role="status">${message}</p>` : null}
|
|
187
|
+
${view ? html`<p class="sub">${tn(view.effects.length === 1 ? "one" : "count", view.effects.length)}${!view.canWrite ? ` · ${tn("readOnly")}` : ""}</p>` : null}
|
|
188
|
+
${view?.canWrite ? html`<p class="sub">${tn("files")}</p><label class="s-fx-chat">${tn("chat")}<select class="s-fx-target" value=${selected} disabled=${busy} onChange=${e => setTarget(e.target.value)}><option value="">${tn("chooseChat")}</option>${names.map(n => html`<option key=${n} value=${n}>${n}</option>`)}</select></label>` : null}
|
|
189
|
+
${view && !view.effects.length ? html`<p class="sub">${tn("empty")}</p>` : null}
|
|
190
|
+
<${OwnerEffects} catalog=${view} busy=${busy} />
|
|
191
|
+
<div class="s-fx-list">${(view?.effects || []).slice(0, limit).map(e => html`<article class="s-fx-item" key=${e.id} data-effect-id=${e.id}><h4>${e.manifest.title}</h4><p class="sub"><code>${e.id.slice(0, 8)}</code> · ${Math.ceil(e.bytes / 1024)} KiB · ${tn(e.ownClaim ? "yours" : "shared")}</p>
|
|
192
|
+
${e.manifest.poster === "poster.png" ? html`<img class="s-fx-poster" src=${`/ui/effects/image/${e.id}/poster.png`} alt="" loading="lazy" />` : null}
|
|
193
|
+
<${Profile} value=${history[e.id] || e.profiles} />
|
|
194
|
+
${view.canWrite ? html`<${Params} manifest=${e.manifest} draft=${drafts[e.id] || {}} disabled=${busy} update=${(key, value) => setDrafts(s => ({ ...s, [e.id]: { ...(s[e.id] || {}), [key]: value } }))} />` : null}<div class="s-fx-actions"><button class="btn" type="button" disabled=${busy} onClick=${() => profile(e.id)}>${tn("history")}</button>
|
|
195
|
+
${view.canWrite ? html`<button class="btn s-fx-apply" type="button" disabled=${busy || !selected} onClick=${() => { const params = effectParamOverrides(e.manifest, drafts[e.id] || {}); if (params === null) setMessage(tn("badParams")); else change("/ui/effects/apply", { instance: selected, id: e.id, params }); }}>${tn("apply")}</button><button class="btn s-fx-remove" type="button" disabled=${busy || !selected} onClick=${() => change("/ui/effects/remove", { instance: selected, id: e.id })}>${tn("remove")}</button>` : null}
|
|
196
|
+
${view.canWrite && e.ownClaim ? html`<button class="btn s-fx-delete" type="button" disabled=${busy} onClick=${() => askDelete(e.id)}>${tn("delete")}</button>` : null}</div>
|
|
197
|
+
${view.canWrite && e.ownClaim ? html`<label class="check"><input class="s-fx-share" type="checkbox" checked=${e.sharedByYou} disabled=${busy} onChange=${event => change("/ui/effects/share", { id: e.id, shared: event.target.checked })} />${tn("share")}</label>` : null}
|
|
198
|
+
</article>`)}</div>
|
|
199
|
+
${view && limit < view.effects.length ? html`<button class="btn" type="button" onClick=${() => setLimit(n => n + 50)}>${tn("more")}</button>` : null}
|
|
200
|
+
</section>`;
|
|
201
|
+
}
|