shadok-ai 0.17.35 → 0.17.36
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/package.json +1 -1
- package/public/index.html +106 -24
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -1086,8 +1086,28 @@
|
|
|
1086
1086
|
.sec-meta { flex-basis: 100%; font-size: 11px; color: var(--text-dim); margin-top: 2px; }
|
|
1087
1087
|
#secretAttach[data-grouped="1"] { display: none; }
|
|
1088
1088
|
.sec-from { margin-left: 8px; font-size: 11px; color: var(--text-dim); font-family: var(--sans, inherit); }
|
|
1089
|
-
|
|
1090
|
-
|
|
1089
|
+
/* Grouped mode: each own secret is a card — name + actions, one meta line, then
|
|
1090
|
+
the "Shared with" / "Injected into" chip rows and ONE Save. */
|
|
1091
|
+
#secretsList li.sec-row { border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; margin-bottom: 8px; background: none; }
|
|
1092
|
+
#secretsList li.sec-row:nth-child(odd) { background: none; }
|
|
1093
|
+
.sec-ctl { flex-basis: 100%; display: flex; flex-direction: column; gap: 8px; font-size: 12px; margin-top: 10px; }
|
|
1094
|
+
.sec-field { display: grid; grid-template-columns: 96px 1fr; gap: 8px; align-items: start; }
|
|
1095
|
+
.sec-field-label { font-size: 11px; color: var(--text-dim); text-transform: uppercase; letter-spacing: .05em; padding-top: 5px; }
|
|
1096
|
+
.sec-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1097
|
+
.sec-chip { position: relative; display: inline-flex; align-items: center; gap: 5px; font-size: 12px; padding: 3px 10px; border: 1px solid var(--line); border-radius: 14px; cursor: pointer; user-select: none; }
|
|
1098
|
+
.sec-chip:hover { border-color: var(--amber); }
|
|
1099
|
+
.sec-chip input { position: absolute; opacity: 0; pointer-events: none; }
|
|
1100
|
+
.sec-chip:has(input:checked) { border-color: var(--amber); background: color-mix(in srgb, var(--amber) 16%, transparent); color: var(--text); }
|
|
1101
|
+
.sec-chip:has(input:checked)::before { content: "✓"; font-size: 11px; color: var(--amber); }
|
|
1102
|
+
.sec-chip:has(input:focus-visible) { outline: 2px solid var(--amber); outline-offset: 1px; }
|
|
1103
|
+
.sec-foot { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
|
|
1104
|
+
.sec-status { flex: 1; font-size: 12px; color: var(--text-dim); }
|
|
1105
|
+
.sec-status.err { color: #e2564a; }
|
|
1106
|
+
#secretsList button.sec-save { font-size: 12px; padding: 4px 14px; border-radius: 6px; border: 1px solid var(--amber); color: var(--amber); }
|
|
1107
|
+
#secretsList button.sec-save:hover:not(:disabled) { background: color-mix(in srgb, var(--amber) 15%, transparent); }
|
|
1108
|
+
#secretsList button.sec-save:disabled { opacity: .35; cursor: default; border-color: var(--line); color: var(--text-dim); }
|
|
1109
|
+
.sec-err { margin: 0 0 10px; padding: 8px 11px; border: 1px solid #e2564a; border-radius: 8px; color: #e2564a; background: color-mix(in srgb, #e2564a 8%, transparent); font-size: 12.5px; }
|
|
1110
|
+
@media (max-width: 520px) { .sec-field { grid-template-columns: 1fr; gap: 4px; } .sec-field-label { padding-top: 0; } }
|
|
1091
1111
|
.group-head .gkey { flex: none; font-size: 12px; padding: 0 4px; cursor: pointer; visibility: hidden; }
|
|
1092
1112
|
.group-head:hover .gkey { visibility: visible; }
|
|
1093
1113
|
/* A touch screen has no hover, so the group-head ⚙/🔑 (the single-column /
|
|
@@ -3017,6 +3037,7 @@
|
|
|
3017
3037
|
</div>
|
|
3018
3038
|
<div class="pin-body">
|
|
3019
3039
|
<div id="secretsScope" class="sec-scope" hidden><span>Group</span><select id="secretsGroup" aria-label="Group whose secrets are shown"></select></div>
|
|
3040
|
+
<p id="secretsErr" class="sec-err" role="alert" hidden></p>
|
|
3020
3041
|
<ul id="secretsList"></ul>
|
|
3021
3042
|
<form id="secretsAdd">
|
|
3022
3043
|
<div class="sec-add-row">
|
|
@@ -10504,8 +10525,11 @@
|
|
|
10504
10525
|
actions.append(
|
|
10505
10526
|
actionBtn("s-edit", "✎", "Update this secret's value / the profiles it feeds", () => startEditSecret(k)),
|
|
10506
10527
|
actionBtn("s-del", "✕", "Remove from vault", async () => {
|
|
10507
|
-
|
|
10508
|
-
|
|
10528
|
+
// The row's OWNER group, not the panel's write target (see secretControls).
|
|
10529
|
+
const g = grouped && sec.ownerGroup != null ? String(sec.ownerGroup) : secretWriteGroup();
|
|
10530
|
+
const r = await fetch("/secrets?name=" + encodeURIComponent(k) + (g ? "&group=" + encodeURIComponent(g) : ""), { method: "DELETE" });
|
|
10531
|
+
const e = await vaultError(r);
|
|
10532
|
+
if (e) { showVaultError("Could not remove " + k + ": " + e); return; }
|
|
10509
10533
|
if (editingSecret === k) resetSecretForm();
|
|
10510
10534
|
await loadVault(); renderSecrets();
|
|
10511
10535
|
}),
|
|
@@ -10519,36 +10543,89 @@
|
|
|
10519
10543
|
}
|
|
10520
10544
|
return li;
|
|
10521
10545
|
}
|
|
10546
|
+
/** Read a refused vault write's message: the server answers `{error}` JSON;
|
|
10547
|
+
* a proxy may answer HTML, so fall back to the status line. */
|
|
10548
|
+
async function vaultError(r) {
|
|
10549
|
+
if (r.ok) return null;
|
|
10550
|
+
try { const j = await r.json(); if (j && j.error) return String(j.error); } catch {}
|
|
10551
|
+
return "refused (HTTP " + r.status + ")";
|
|
10552
|
+
}
|
|
10522
10553
|
/** Own secret, grouped mode: who it is lent to (PUT /secrets/share) and which
|
|
10523
|
-
* profiles inject it (PUT /secrets/profiles)
|
|
10524
|
-
*
|
|
10554
|
+
* profiles inject it (PUT /secrets/profiles), edited as chips and saved by ONE
|
|
10555
|
+
* button that only lights up once something changed. Both calls target the
|
|
10556
|
+
* secret's OWNER group, never the panel's selection: in the "Public groups"
|
|
10557
|
+
* view the panel's write target is the first public group, which sent every
|
|
10558
|
+
* row there (a 403 or a 404 for a secret living in another public group). A
|
|
10559
|
+
* refusal is shown in the card, not swallowed. */
|
|
10525
10560
|
function secretControls(sec) {
|
|
10526
10561
|
const box = document.createElement("div"); box.className = "sec-ctl";
|
|
10527
10562
|
const targets = window.shareTargets ? window.shareTargets(vaultGroups, sec.ownerGroup) : [];
|
|
10528
10563
|
const profs = profileCache.map((p) => p.name).filter((n) => !(window.isManagedProfile && window.isManagedProfile(n))).sort();
|
|
10529
|
-
const
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10564
|
+
const same = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
|
|
10565
|
+
const fields = [];
|
|
10566
|
+
const field = (label, hint, options, current) => {
|
|
10567
|
+
if (!options.length) return null;
|
|
10568
|
+
const row = document.createElement("div"); row.className = "sec-field";
|
|
10569
|
+
const l = document.createElement("span"); l.className = "sec-field-label"; l.textContent = label; l.title = hint;
|
|
10570
|
+
const chips = document.createElement("div"); chips.className = "sec-chips";
|
|
10571
|
+
const initial = (current || []).map(String);
|
|
10533
10572
|
const cbs = options.map((o) => {
|
|
10534
|
-
const
|
|
10535
|
-
const cb = document.createElement("input"); cb.type = "checkbox"; cb.value = o.value; cb.checked =
|
|
10573
|
+
const lab = document.createElement("label"); lab.className = "sec-chip";
|
|
10574
|
+
const cb = document.createElement("input"); cb.type = "checkbox"; cb.value = o.value; cb.checked = initial.includes(o.value);
|
|
10536
10575
|
const t = document.createElement("span"); t.textContent = o.label;
|
|
10537
|
-
|
|
10576
|
+
lab.append(cb, t); chips.appendChild(lab); return cb;
|
|
10538
10577
|
});
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
box.appendChild(wrap);
|
|
10578
|
+
row.append(l, chips); box.appendChild(row);
|
|
10579
|
+
const f = { cbs, initial: initial.filter((v) => options.some((o) => o.value === v)), value: () => cbs.filter((c) => c.checked).map((c) => c.value) };
|
|
10580
|
+
fields.push(f);
|
|
10581
|
+
return f;
|
|
10544
10582
|
};
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
|
|
10549
|
-
|
|
10583
|
+
const share = field("Shared with", "Groups that may READ this secret (it stays owned by its group)",
|
|
10584
|
+
targets.map((g) => ({ value: String(g.id), label: g.name })), sec.sharedWith);
|
|
10585
|
+
const inject = field("Injected into", "Profiles whose agents receive it as an environment variable at spawn",
|
|
10586
|
+
profs.map((n) => ({ value: n, label: n })), sec.profiles);
|
|
10587
|
+
if (!fields.length) return box;
|
|
10588
|
+
const foot = document.createElement("div"); foot.className = "sec-foot";
|
|
10589
|
+
const status = document.createElement("span"); status.className = "sec-status"; status.setAttribute("role", "status");
|
|
10590
|
+
const save = document.createElement("button"); save.type = "button"; save.className = "sec-save"; save.textContent = "Save"; save.disabled = true;
|
|
10591
|
+
const dirty = () => fields.some((f) => !same(f.value(), f.initial));
|
|
10592
|
+
box.addEventListener("change", () => {
|
|
10593
|
+
save.disabled = !dirty();
|
|
10594
|
+
if (status.classList.contains("ok")) { status.textContent = ""; status.className = "sec-status"; }
|
|
10595
|
+
});
|
|
10596
|
+
save.addEventListener("click", async () => {
|
|
10597
|
+
save.disabled = true; status.className = "sec-status"; status.textContent = "Saving…";
|
|
10598
|
+
const put = (url, body) => fetch(url, { method: "PUT", headers: { "Content-Type": "application/json" },
|
|
10599
|
+
body: JSON.stringify({ name: sec.name, group: String(sec.ownerGroup), ...body }) });
|
|
10600
|
+
const errors = [];
|
|
10601
|
+
try {
|
|
10602
|
+
if (share && !same(share.value(), share.initial)) {
|
|
10603
|
+
const e = await vaultError(await put("/secrets/share", { sharedWith: share.value() }));
|
|
10604
|
+
if (e) errors.push("Sharing not saved: " + e);
|
|
10605
|
+
}
|
|
10606
|
+
if (inject && !same(inject.value(), inject.initial)) {
|
|
10607
|
+
const e = await vaultError(await put("/secrets/profiles", { profiles: inject.value() }));
|
|
10608
|
+
if (e) errors.push("Profiles not saved: " + e);
|
|
10609
|
+
}
|
|
10610
|
+
} catch (err) { errors.push("Network error: " + (err && err.message || err)); }
|
|
10611
|
+
if (errors.length) {
|
|
10612
|
+
// Keep the user's ticks on screen so they can retry or undo — a reload
|
|
10613
|
+
// here would silently reset them to the server's state.
|
|
10614
|
+
status.className = "sec-status err"; status.textContent = errors.join(" · ");
|
|
10615
|
+
save.disabled = false;
|
|
10616
|
+
return;
|
|
10617
|
+
}
|
|
10618
|
+
await loadVault(); renderSecrets();
|
|
10619
|
+
});
|
|
10620
|
+
foot.append(status, save); box.appendChild(foot);
|
|
10550
10621
|
return box;
|
|
10551
10622
|
}
|
|
10623
|
+
/** Panel-level refusal (add / update / delete): shown above the list until the
|
|
10624
|
+
* next successful write or the panel is reopened. */
|
|
10625
|
+
function showVaultError(msg) {
|
|
10626
|
+
const el = $("secretsErr");
|
|
10627
|
+
el.textContent = msg || ""; el.hidden = !msg;
|
|
10628
|
+
}
|
|
10552
10629
|
function renderSecrets() {
|
|
10553
10630
|
const ul = $("secretsList");
|
|
10554
10631
|
ul.innerHTML = "";
|
|
@@ -10689,6 +10766,7 @@
|
|
|
10689
10766
|
await Promise.all([loadVault(), loadProfilesInto()]);
|
|
10690
10767
|
renderSecrets();
|
|
10691
10768
|
resetSecretForm();
|
|
10769
|
+
showVaultError("");
|
|
10692
10770
|
$("secretsReload").hidden = true;
|
|
10693
10771
|
$("secretsOverlay").hidden = false;
|
|
10694
10772
|
$("secretKey").focus();
|
|
@@ -12024,8 +12102,12 @@
|
|
|
12024
12102
|
// AGENT replacing a credential silently).
|
|
12025
12103
|
const keepValue = editingSecret && value === "";
|
|
12026
12104
|
if (!keepValue) {
|
|
12027
|
-
await fetch("/secrets", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(withSecretGroup({ name, value, overwrite: true })) });
|
|
12105
|
+
const r = await fetch("/secrets", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(withSecretGroup({ name, value, overwrite: true })) });
|
|
12106
|
+
const err = await vaultError(r);
|
|
12107
|
+
// Refused: say so and keep the form filled, rather than clearing it as if saved.
|
|
12108
|
+
if (err) { showVaultError("Could not save " + name + ": " + err); return; }
|
|
12028
12109
|
}
|
|
12110
|
+
showVaultError("");
|
|
12029
12111
|
// Grouped mode binds profiles per row (PUT /secrets/profiles) and hides the
|
|
12030
12112
|
// ticks: syncing here would read an empty selection and detach everything.
|
|
12031
12113
|
if (window.secretsPanelMode(vaultGroups) === "flat") await syncSecretToProfiles(name, wanted); // refreshes profileCache
|