@lifeaitools/clauth 2.1.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/.clauth-skill/references/keys-guide.md +270 -270
  2. package/cli/api.js +238 -238
  3. package/cli/commands/install.js +413 -396
  4. package/cli/commands/login.test.js +49 -1
  5. package/cli/commands/ops-install.js +11 -10
  6. package/cli/commands/ops.test.js +270 -0
  7. package/cli/commands/scrub.test.js +70 -0
  8. package/cli/commands/serve.js +2614 -11189
  9. package/cli/commands/uninstall.js +164 -164
  10. package/cli/dashboard/dashboard.js +592 -0
  11. package/cli/dashboard/global.css +65 -0
  12. package/cli/dashboard/panel-element.js +79 -0
  13. package/cli/dashboard/panels/build-status-panel.css +12 -0
  14. package/cli/dashboard/panels/build-status-panel.js +98 -0
  15. package/cli/dashboard/panels/panel-registry.js +58 -0
  16. package/cli/dashboard/panels/services-panel.css +95 -0
  17. package/cli/dashboard/panels/services-panel.js +872 -0
  18. package/cli/dashboard/panels/surfaces-panel.css +59 -0
  19. package/cli/dashboard/panels/surfaces-panel.js +474 -0
  20. package/cli/dashboard/panels/toolbar-panel.css +42 -0
  21. package/cli/dashboard/panels/toolbar-panel.js +374 -0
  22. package/cli/dashboard/panels/tunnel-panel.css +61 -0
  23. package/cli/dashboard/panels/tunnel-panel.js +688 -0
  24. package/cli/dashboard/panels/webdav-panel.css +38 -0
  25. package/cli/dashboard/panels/webdav-panel.js +166 -0
  26. package/cli/fingerprint.test.js +95 -0
  27. package/cli/http/components/callagent-terminal-component.js +310 -0
  28. package/cli/http/components/dashboard-component.js +123 -0
  29. package/cli/http/components/inbox-component.js +41 -0
  30. package/cli/http/components/mcp-transport-component.js +275 -0
  31. package/cli/http/components/oauth-component.js +207 -0
  32. package/cli/http/components/service-crud-component.js +288 -0
  33. package/cli/http/components/supervisor-component.js +104 -0
  34. package/cli/http/components/system-component.js +184 -0
  35. package/cli/http/components/tunnel-component.js +508 -0
  36. package/cli/http/components/vault-auth-component.js +264 -0
  37. package/cli/http/components/watchdog-component.js +28 -0
  38. package/cli/http/request-utils.js +21 -0
  39. package/cli/http/servers/ops-server.js +335 -0
  40. package/cli/index.js +5 -91
  41. package/cli/lib/fs-git.test.js +467 -0
  42. package/cli/mcp/providers/call-agent-provider.js +30 -0
  43. package/cli/mcp/providers/clauth-core-provider.js +461 -0
  44. package/cli/mcp/providers/ops-provider.js +42 -0
  45. package/cli/mcp/providers/terminal-provider.js +81 -0
  46. package/cli/mcp/tool-registry.js +48 -0
  47. package/cli/mcp/tool-schemas.js +822 -0
  48. package/cli/mcp/tool-schemas.test.js +49 -0
  49. package/cli/ops/pm2-preflight.js +54 -0
  50. package/cli/recovery.js +6 -0
  51. package/cli/recovery.test.js +179 -0
  52. package/cli/services/agent-pool-instance.js +38 -0
  53. package/cli/services/claude-binary.js +29 -0
  54. package/cli/services/delegation-lane-instance.js +35 -0
  55. package/cli/services/delegation-lane-instance.test.js +37 -0
  56. package/cli/services/file-io.js +38 -0
  57. package/cli/services/file-io.test.js +10 -0
  58. package/cli/services/fs-mount.js +66 -0
  59. package/cli/services/fs-mount.test.js +14 -0
  60. package/cli/services/fs-upload-sessions.js +20 -0
  61. package/cli/services/fs-upload-sessions.test.js +14 -0
  62. package/cli/services/git-exec.js +37 -0
  63. package/cli/services/git-exec.test.js +41 -0
  64. package/cli/services/git-import-guard.js +28 -0
  65. package/cli/services/git-import-guard.test.js +18 -0
  66. package/cli/services/service-status.js +191 -0
  67. package/cli/services/service-status.test.js +39 -0
  68. package/cli/services/terminal-dispatch.js +135 -0
  69. package/cli/services/terminal-dispatch.test.js +9 -0
  70. package/cli/services/terminal-registry.js +39 -0
  71. package/cli/services/terminal-registry.test.js +31 -0
  72. package/cli/services/terminal-sessions.js +100 -0
  73. package/cli/services/terminal-sessions.test.js +26 -0
  74. package/cli/services/terminal-window.js +106 -0
  75. package/cli/services/vault-read.js +14 -0
  76. package/cli/services/vault-read.test.js +35 -0
  77. package/cli/supervisor-registry.js +116 -0
  78. package/cli/supervisor-registry.test.js +165 -0
  79. package/cli/supervisor-ui.test.js +230 -30
  80. package/cli/webdav-config.js +84 -0
  81. package/cli/webdav-config.test.js +79 -0
  82. package/cli/webdav-obscure.js +23 -0
  83. package/cli/webdav-obscure.test.js +41 -0
  84. package/cli/webdav-rclone-discovery.js +44 -0
  85. package/cli/webdav-service.js +30 -118
  86. package/cli/webdav-service.test.js +107 -0
  87. package/install.ps1 +102 -102
  88. package/install.sh +49 -49
  89. package/package.json +2 -2
  90. package/scripts/bootstrap.cjs +121 -121
  91. package/supabase/functions/auth-vault/index.ts +350 -350
  92. package/supabase/migrations/001_clauth_schema.sql +94 -94
  93. package/supabase/migrations/002_vault_helpers.sql +90 -90
  94. package/supabase/migrations/20260317_lockout.sql +26 -26
  95. package/cli/assets/codevelop/launcher-active.cmd.template +0 -20
  96. package/cli/assets/codevelop/launcher-static.cmd.template +0 -7
  97. package/cli/assets/codevelop/windows-terminal.profiles.json +0 -48
  98. package/cli/commands/codevelop.js +0 -1190
@@ -0,0 +1,79 @@
1
+ // cli/dashboard/panel-element.js
2
+ // The real component contract every clauth panel implements: a native Web
3
+ // Component with its own Shadow DOM. This is what "panels" means from here
4
+ // on -- not a file sharing the page's global CSS/DOM, but a genuinely
5
+ // isolated custom element. An edit inside one panel's shadow root has zero
6
+ // effect on any other panel: the browser enforces this, not a naming
7
+ // convention.
8
+ //
9
+ // Loaded FIRST, before any panel script, as a plain classic <script> (no
10
+ // build step, no bundler -- matches clauth's whole delivery model: every
11
+ // dashboard asset is a static file served verbatim).
12
+ //
13
+ // Contract a panel subclass implements:
14
+ // static tagName -- the custom element name, e.g. "clauth-tunnel-panel"
15
+ // static cssHref -- path to this panel's own stylesheet (served
16
+ // from the SAME static-asset route already
17
+ // registered in serve.js -- no new serving
18
+ // mechanism, just loaded inside the shadow root
19
+ // instead of the light DOM <head>)
20
+ // template() -- returns the panel's own inner HTML (a string)
21
+ // mount() -- called once the shadow root's template is in
22
+ // the DOM; do polling/fetch/init work here
23
+ // actions -- a plain object mapping action names (used via
24
+ // data-action="name" in the template) to methods
25
+ // on the instance; a single delegated listener
26
+ // replaces every inline onclick="globalFn()"
27
+ //
28
+ // this.$(selector) / this.$$(selector) look inside the panel's OWN shadow
29
+ // root, never the page's light DOM -- the isolation boundary in code form.
30
+ class ClauthPanelElement extends HTMLElement {
31
+ // connectedCallback() fires the moment the browser's HTML parser reaches
32
+ // this tag -- which, with every panel's own <script> loaded at the bottom
33
+ // of <body> AFTER the tag itself, is BEFORE dashboard.js (and its globals
34
+ // like apiFetch) has executed. Building the shadow DOM here is safe (no
35
+ // external dependency); calling the panel's real mount() here is NOT --
36
+ // it ran into exactly that ordering bug the first time this was written
37
+ // (a live "apiFetch is not defined" error a vm-based test harness could
38
+ // never catch, since it doesn't emulate the browser's synchronous custom-
39
+ // element-upgrade-during-parse behavior). mount() is instead called
40
+ // explicitly by ClauthPanelRegistry.mountAll(), same as every panel
41
+ // before this one -- the registry's whole job is guaranteeing "all
42
+ // scripts have loaded" before any panel's real init logic runs.
43
+ connectedCallback() {
44
+ if (this.shadowRoot) return; // already built (re-attach is a no-op)
45
+ const root = this.attachShadow({ mode: "open" });
46
+ const link = document.createElement("link");
47
+ link.rel = "stylesheet";
48
+ link.href = this.constructor.cssHref;
49
+ root.appendChild(link);
50
+ const body = document.createElement("div");
51
+ body.innerHTML = this.template();
52
+ root.appendChild(body);
53
+
54
+ // One delegated listener replaces every inline onclick="globalFn()" --
55
+ // clicks anywhere in this shadow root bubble to here (shadow DOM
56
+ // retargets bubbling events to the shadow host's listeners), so a
57
+ // single listener covers the whole panel regardless of how deep the
58
+ // clicked element is nested, including elements added later by re-render.
59
+ root.addEventListener("click", (e) => {
60
+ const el = e.target.closest("[data-action]");
61
+ if (!el || !root.contains(el)) return;
62
+ const name = el.dataset.action;
63
+ const fn = this.actions && this.actions[name];
64
+ if (typeof fn === "function") fn.call(this, e, el);
65
+ });
66
+ root.addEventListener("keydown", (e) => {
67
+ const el = e.target.closest("[data-keydown]");
68
+ if (!el || !root.contains(el)) return;
69
+ const name = el.dataset.keydown;
70
+ const fn = this.actions && this.actions[name];
71
+ if (typeof fn === "function") fn.call(this, e, el);
72
+ });
73
+ }
74
+
75
+ // Shadow-scoped lookup helpers -- the isolation boundary in code form.
76
+ $(selector) { return this.shadowRoot.querySelector(selector); }
77
+ $$(selector) { return this.shadowRoot.querySelectorAll(selector); }
78
+ }
79
+ window.ClauthPanelElement = ClauthPanelElement;
@@ -0,0 +1,12 @@
1
+ /* cli/dashboard/panels/build-status-panel.css
2
+ The "Deploy" identity badge -- lives inline in the header row
3
+ (data-panel="title-status"), 50/50 alongside the status-bar, not in its
4
+ own section. Owned by build-status-panel.js. Depends on the tokens in
5
+ ../global.css, loaded before this file. */
6
+ .deploy-badge{display:flex;align-items:center;gap:8px;background:#0f1a2d;border:1px solid var(--border-dark);border-radius:8px;padding:.5rem .85rem;font-size:.82rem;color:var(--grey)}
7
+ .deploy-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0;background:var(--grey-dim)}
8
+ .deploy-dot.live{background:var(--green-bright)}
9
+ .deploy-dot.test{background:var(--blue)}
10
+ .deploy-dot.local{box-shadow:0 0 0 2px var(--orange) inset}
11
+ #deploy-label{color:var(--text-bright);font-weight:500}
12
+ .deploy-sha{font-family:'Courier New',monospace;font-size:.72rem;color:var(--grey-dim)}
@@ -0,0 +1,98 @@
1
+ // cli/dashboard/panels/build-status-panel.js
2
+ // The "Deploy" identity badge, as a real Web Component: <clauth-build-status-panel>.
3
+ // Answers ONE question: what am I looking at? Live (port 52437, the daemon
4
+ // Dave uses directly) vs Test (any staged/isolated/other-port instance),
5
+ // crossed with Local (this checkout diverges from origin/develop -- dirty
6
+ // working tree, or a different commit) vs Develop (HEAD matches
7
+ // origin/develop exactly, clean tree -- what's actually published).
8
+ //
9
+ // Previously this panel polled GET /builds, which is regen-root's OWN build
10
+ // pipeline status (borrowed from Supabase's prt_storage.build_status blob) --
11
+ // unrelated to clauth's own runtime identity, and the source of "not sure
12
+ // what that sync message is" confusion. GET /deploy-status (serve.js) is
13
+ // clauth's own answer instead. /builds and its backing poller are untouched
14
+ // in case another consumer still needs regen-root's build status; this panel
15
+ // simply no longer surfaces it.
16
+ //
17
+ // Same conversion shape as every panel before it (tunnel-panel.js is the
18
+ // reference implementation): a class extending window.ClauthPanelElement,
19
+ // its own template()/mount(), instance state instead of module-level
20
+ // globals, and mount() -- not connectedCallback() -- doing the real fetch
21
+ // work. connectedCallback() (panel-element.js) fires during HTML parsing,
22
+ // before dashboard.js (and its BASE global) has loaded; calling real logic
23
+ // there throws a live "BASE is not defined" a vm-based test harness would
24
+ // never catch. mount() is instead called explicitly by
25
+ // ClauthPanelRegistry.mountAll(), after every script has loaded.
26
+ //
27
+ // This panel owns none of WRITE_ACTIONS (it only reads GET /deploy-status),
28
+ // so it needs no write-guard bridge -- unlike tunnel-panel.js/webdav-panel.js.
29
+ //
30
+ // Loaded as a plain classic <script> (not type="module") BEFORE dashboard.js
31
+ // -- same pattern as every other panel component.
32
+ class ClauthBuildStatusPanel extends window.ClauthPanelElement {
33
+ static tagName = "clauth-build-status-panel";
34
+ static cssHref = "/dashboard/panels/build-status-panel.css";
35
+
36
+ template() {
37
+ return `
38
+ <div class="deploy-badge" id="deploy-badge">
39
+ <div class="deploy-dot unknown" id="deploy-dot"></div>
40
+ <span id="deploy-label">checking…</span>
41
+ <span id="deploy-sha" class="deploy-sha"></span>
42
+ </div>
43
+ `;
44
+ }
45
+
46
+ mount() {
47
+ this.pollTimer = null;
48
+ this.updateDeployStatus();
49
+ // A git-sync check is cheap but not free -- 30s keeps the badge honest
50
+ // (a fresh push or a new local edit shows up within half a minute)
51
+ // without polling as aggressively as a per-request build/test pipeline
52
+ // status would need.
53
+ this.pollTimer = setInterval(() => this.updateDeployStatus(), 30000);
54
+ }
55
+
56
+ async updateDeployStatus() {
57
+ try {
58
+ const data = await fetch(BASE + "/deploy-status").then(r => r.json());
59
+ const dot = this.$("#deploy-dot");
60
+ const label = this.$("#deploy-label");
61
+ const sha = this.$("#deploy-sha");
62
+
63
+ const envWord = data.is_live ? "Live" : "Test";
64
+ // synced === null means git wasn't available or origin/develop has
65
+ // never been fetched locally -- treat as "Local" (unconfirmed is not
66
+ // the same claim as "matches develop").
67
+ const syncWord = data.synced === true ? "Develop" : "Local";
68
+ const text = envWord + " - " + syncWord;
69
+
70
+ dot.className = "deploy-dot " + (data.is_live ? "live" : "test") + " " + (data.synced === true ? "synced" : "local");
71
+ label.textContent = text;
72
+ sha.textContent = data.head_sha ? data.head_sha : "";
73
+ } catch {
74
+ const label = this.$("#deploy-label");
75
+ if (label) label.textContent = "unknown";
76
+ }
77
+ }
78
+
79
+ // No data-action elements in this panel's template -- it is pure display,
80
+ // polled on a timer. Declared anyway so the contract is explicit rather
81
+ // than implicit-via-undefined.
82
+ get actions() {
83
+ return {};
84
+ }
85
+ }
86
+ customElements.define(ClauthBuildStatusPanel.tagName, ClauthBuildStatusPanel);
87
+
88
+ // Panel interface registration — see ./panel-registry.js. mount() is called
89
+ // explicitly by ClauthPanelRegistry.mountAll(), after every script has
90
+ // loaded -- connectedCallback() (panel-element.js) only builds the shadow
91
+ // DOM during HTML parsing, before dashboard.js (and BASE) exists.
92
+ window.ClauthPanelRegistry.register({
93
+ id: "build-status",
94
+ mount: () => {
95
+ const el = document.querySelector(ClauthBuildStatusPanel.tagName);
96
+ if (el) el.mount();
97
+ },
98
+ });
@@ -0,0 +1,58 @@
1
+ // cli/dashboard/panels/panel-registry.js
2
+ // The one shared interface every dashboard panel implements. Loaded FIRST
3
+ // (before any panel script), as a classic script so ClauthPanelRegistry is a
4
+ // window global every later panel script can call at its own top level.
5
+ //
6
+ // Why this exists: before this file, dashboard.js's showMain()/boot() called
7
+ // each panel's init function BY NAME (pollTunnel(), loadWebdavMounts(),
8
+ // updateBuildStatus(), loadSupervisorCockpit(), loadServices(), ...) — the
9
+ // app shell had hardcoded knowledge of every panel's internals, so adding a
10
+ // new panel meant editing showMain() itself. That is a Dependency Inversion
11
+ // violation: the shell should depend on an abstraction ("a panel can be
12
+ // mounted"), not on each panel's concrete function name.
13
+ //
14
+ // The interface is deliberately tiny (Interface Segregation): a panel is
15
+ // { id: string, mount: function }. mount() is called once, when the
16
+ // dashboard has just unlocked and is about to show. A panel with nothing to
17
+ // do on mount still registers with a no-op mount — every registered panel
18
+ // implements the exact same shape (Liskov: the shell never needs to special-
19
+ // case one panel's registration over another's).
20
+ (() => {
21
+ const registry = [];
22
+
23
+ window.ClauthPanelRegistry = {
24
+ // Called once per panel script, at that script's own top level —
25
+ // e.g. `ClauthPanelRegistry.register({ id: "tunnel", mount: pollTunnel });`
26
+ // at the bottom of tunnel-panel.js.
27
+ register(panel) {
28
+ if (!panel || typeof panel.id !== "string" || typeof panel.mount !== "function") {
29
+ console.error("ClauthPanelRegistry.register: invalid panel definition", panel);
30
+ return;
31
+ }
32
+ if (registry.some((p) => p.id === panel.id)) {
33
+ console.error(`ClauthPanelRegistry.register: panel id "${panel.id}" already registered`);
34
+ return;
35
+ }
36
+ registry.push(panel);
37
+ },
38
+
39
+ // Called once by dashboard.js's showMain(), after unlock — mounts every
40
+ // registered panel. A single panel throwing during mount is logged and
41
+ // does not stop the rest from mounting.
42
+ mountAll() {
43
+ for (const panel of registry) {
44
+ try {
45
+ panel.mount();
46
+ } catch (err) {
47
+ console.error(`ClauthPanelRegistry: panel "${panel.id}" mount() threw:`, err);
48
+ }
49
+ }
50
+ },
51
+
52
+ // Introspection — the dashboard shell (or a future admin panel) can list
53
+ // what's registered without needing to know panel internals.
54
+ list() {
55
+ return registry.map((p) => p.id);
56
+ },
57
+ };
58
+ })();
@@ -0,0 +1,95 @@
1
+ /* cli/dashboard/panels/services-panel.css
2
+ The credential grid (data-panel="search-credentials"): project tabs,
3
+ search bar, the credential cards themselves, the Set/Add-Service modals,
4
+ and the OAuth import fields the Set modal uses for gmail/gcal. Shared
5
+ across services-panel.js, services-key-lifecycle.js, and
6
+ services-metadata-crud.js -- none of those three register an independent
7
+ panel identity (see services-metadata-crud.js's own header comment), so
8
+ one stylesheet for all three is the correct mirror of the JS split, not
9
+ an inconsistency. Depends on the tokens in ../global.css, loaded before
10
+ this file. */
11
+ .project-tabs{display:flex;gap:0;margin-bottom:1rem;border-bottom:1px solid var(--bg-card);overflow-x:auto}
12
+ .project-tab{background:none;border:none;border-bottom:2px solid transparent;color:var(--grey-dim);padding:8px 16px;font-size:.82rem;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s}
13
+ .project-tab:hover{color:var(--grey);background:rgba(59,130,246,.05)}
14
+ .project-tab.active{color:var(--blue-light);border-bottom-color:var(--blue);background:rgba(59,130,246,.08)}
15
+ .project-tab .tab-count{font-size:.7rem;color:var(--amber);margin-left:4px;font-weight:400}
16
+ .project-tab.active .tab-count{color:var(--blue)}
17
+ .service-search{display:flex;align-items:center;gap:10px;margin:-.35rem 0 1rem;background:var(--bg-dark);border:1px solid var(--bg-card);border-radius:8px;padding:9px 12px}
18
+ .service-search-label{font-size:.76rem;color:var(--amber);font-weight:600;letter-spacing:.02em;text-transform:uppercase;white-space:nowrap}
19
+ .service-search-input{flex:1;min-width:180px;background:var(--bg-darker);border:1px solid var(--border);border-radius:6px;color:var(--text-bright);font-family:'Courier New',monospace;font-size:.88rem;padding:8px 11px;outline:none;transition:border-color .15s}
20
+ .service-search-input:focus{border-color:var(--blue)}
21
+ .service-search-count{font-size:.78rem;color:var(--amber);white-space:nowrap}
22
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:1rem}
23
+ .card{background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:1.25rem;transition:border-color .2s}
24
+ .card:hover{border-color:var(--blue)}
25
+ .card-top{display:flex;align-items:flex-start;justify-content:space-between}
26
+ .card-main{flex:1;min-width:0}
27
+ .card-title-row{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
28
+ .card-name{font-size:1rem;font-weight:600;color:var(--text-white);margin-bottom:3px}
29
+ .card-slug{font-family:'Courier New',monospace;font-size:.75rem;color:var(--amber);background:rgba(217,168,108,.1);padding:1px 6px;border-radius:3px;letter-spacing:.3px}
30
+ .card-meta-row{display:flex;align-items:center;gap:6px;margin-top:2px;flex-wrap:wrap}
31
+ .card-type{font-size:.78rem;color:var(--amber);text-transform:uppercase;letter-spacing:.5px}
32
+ .card-desc{font-size:.78rem;color:var(--amber);margin-top:4px;line-height:1.3}
33
+ .expiry-badge{font-size:.65rem;border-radius:3px;padding:1px 6px}
34
+ .btn-rotate{background:#0e7490;border:1px solid #06b6d4;color:var(--teal-light);font-size:.75rem;padding:3px 8px;border-radius:4px;cursor:pointer}
35
+ .card-getkey{font-size:.75rem;color:var(--blue-light);text-decoration:none;transition:color .15s}
36
+ .card-getkey:hover{color:#93c5fd;text-decoration:underline}
37
+ .card-corner{display:flex;align-items:center;gap:8px;flex-shrink:0}
38
+ .card-edit-corner{background:none;border:1px solid var(--border);color:var(--amber);border-radius:4px;padding:2px 6px;font-size:.85rem;cursor:pointer;transition:all .15s;line-height:1}
39
+ .card-edit-corner:hover{border-color:var(--blue-light);color:var(--blue-light)}
40
+ .card-project{display:inline-flex;align-items:center;gap:3px;font-size:.68rem;color:var(--blue);background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.2);border-radius:3px;padding:1px 4px 1px 6px}
41
+ .card-project-name{color:var(--blue)}
42
+ .card-project-pencil{background:none;border:none;color:var(--blue);cursor:pointer;font-size:.75rem;padding:0 2px;opacity:.75;transition:opacity .15s}
43
+ .card-project-pencil:hover{opacity:1}
44
+ .card-value{font-family:'Courier New',monospace;font-size:.82rem;color:var(--green-bright);background:var(--bg-dark);border-radius:4px;padding:8px 10px;margin-top:10px;word-break:break-all;max-height:80px;overflow:auto;display:none}
45
+ .card-actions{margin-top:10px;display:flex;gap:7px;flex-wrap:wrap}
46
+ .btn-delete{background:var(--red-darker);color:var(--pink);border:1px solid var(--red-dark);padding:3px 10px;font-size:.75rem;font-weight:600;border-radius:4px;cursor:pointer;transition:background .15s,border-color .15s}
47
+ .btn-delete:hover{background:var(--red-dark);border-color:var(--red-bright)}
48
+ .card-edit-panel{display:none;margin-top:8px;background:var(--bg-dark);border-radius:6px;padding:10px;border:1px solid var(--border-dark)}
49
+ .card-edit-panel .ce-head{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:.72rem;color:var(--amber)}
50
+ .card-edit-panel .ce-hint{font-size:.68rem;color:var(--amber);opacity:.7}
51
+ .ce-row{display:flex;align-items:center;gap:8px;margin-bottom:6px}
52
+ .ce-row label{font-size:.75rem;color:var(--amber);width:88px;flex-shrink:0}
53
+ .ce-input{flex:1;background:var(--bg-darker);border:1px solid var(--border-dark);border-radius:4px;color:var(--text-bright);font-size:.85rem;padding:5px 8px;outline:none;font-family:'Courier New',monospace;transition:border-color .15s}
54
+ .ce-input:focus{border-color:var(--blue)}
55
+ .ce-foot{display:flex;gap:8px;align-items:center;margin-top:4px}
56
+ .ce-msg{font-size:.75rem}
57
+ .set-panel{display:none;margin-top:10px;background:var(--bg-dark);border-radius:6px;padding:10px;border:1px solid var(--border-dark)}
58
+ .set-panel label{font-size:.75rem;color:var(--amber);display:block;margin-bottom:6px}
59
+ .set-input{width:100%;background:var(--bg-darker);border:1px solid var(--border-dark);border-radius:4px;color:var(--text-bright);font-family:'Courier New',monospace;font-size:.85rem;padding:7px 10px;outline:none;resize:vertical;min-height:58px;transition:border-color .2s}
60
+ .set-input:focus{border-color:var(--blue)}
61
+ .set-foot{margin-top:8px;display:flex;gap:8px;align-items:center}
62
+ .set-msg{font-size:.8rem}
63
+ .set-msg.ok{color:var(--green)} .set-msg.fail{color:var(--red)}
64
+ .btn-reveal{background:var(--border-dark);color:var(--blue-light)}.btn-reveal:hover{background:var(--blue-hover-bg)}
65
+ .btn-copy{background:#1a3328;color:var(--green)}.btn-copy:hover{background:#1a4338}
66
+ .btn-set{background:#2d1f4a;color:#a78bfa}.btn-set:hover{background:#3d2f5a}
67
+ .btn-generate{background:#3b2f0d;color:var(--gold);padding:6px 12px}.btn-generate:hover{background:#56420d}
68
+ .btn-enable{background:var(--green-bg);color:var(--green);border:1px solid var(--green-dark)}.btn-enable:hover{background:#1a3d22;border-color:var(--green)}
69
+ .btn-disable{background:#2d1f1f;color:var(--red);border:1px solid var(--red-darker)}.btn-disable:hover{background:#3d2525;border-color:var(--red)}
70
+ .svc-badge{font-size:.7rem;font-weight:600;padding:2px 7px;border-radius:4px;letter-spacing:.4px;text-transform:uppercase}
71
+ .svc-badge.on{background:rgba(74,222,128,.12);color:var(--green);border:1px solid rgba(74,222,128,.25)}
72
+ .svc-badge.off{background:rgba(251,146,60,.12);color:var(--orange-bright);border:1px solid rgba(251,146,60,.25)}
73
+ .status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;opacity:0;transition:opacity .3s;margin-top:4px;cursor:default}
74
+ .status-dot.checking{background:var(--orange);opacity:1;animation:pulse 1s infinite}
75
+ .status-dot.ok{background:var(--green-bright);opacity:1}
76
+ .status-dot.fail{background:var(--red-bright);opacity:1}
77
+ @keyframes sdot-fade{to{opacity:0}} .status-dot.fading{animation:sdot-fade 1.5s forwards}
78
+ .add-panel{display:none;background:var(--bg-modal);border:1px solid var(--border);border-radius:8px;padding:1.25rem;margin-bottom:1.5rem}
79
+ .add-panel h3{font-size:.9rem;font-weight:600;color:var(--text-white);margin-bottom:1rem}
80
+ .add-row{display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;margin-bottom:.75rem}
81
+ .add-field{display:flex;flex-direction:column;gap:4px}
82
+ .add-field label{font-size:.75rem;color:var(--amber)}
83
+ .add-input{background:var(--bg-dark);border:1px solid var(--border);border-radius:6px;color:var(--text-bright);font-family:'Courier New',monospace;font-size:.88rem;padding:7px 12px;outline:none;width:200px;transition:border-color .2s}
84
+ .add-input:focus{border-color:var(--blue)}
85
+ .add-select{background:var(--bg-dark);border:1px solid var(--border);border-radius:6px;color:var(--text-bright);font-size:.88rem;padding:7px 12px;outline:none;width:200px;transition:border-color .2s;cursor:pointer}
86
+ .add-select:focus{border-color:var(--blue)}
87
+ .add-foot{display:flex;gap:8px;align-items:center}
88
+ .add-msg{font-size:.82rem}
89
+ .add-msg.ok{color:var(--green)} .add-msg.fail{color:var(--red)}
90
+ .oauth-fields{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}
91
+ .oauth-field{display:flex;flex-direction:column;gap:3px}
92
+ .oauth-label{font-size:.75rem;color:var(--grey);font-weight:500}
93
+ .oauth-hint{font-size:.71rem;color:var(--amber);font-style:italic}
94
+ .oauth-input{width:100%;background:var(--bg-darker);border:1px solid var(--border-dark);border-radius:4px;color:var(--text-bright);font-family:'Courier New',monospace;font-size:.85rem;padding:7px 10px;outline:none;transition:border-color .2s}
95
+ .oauth-input:focus{border-color:var(--blue)}