@frumu/tandem-panel 0.4.18 → 0.4.21
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/bin/setup.js +38 -1
- package/dist/assets/{index-B_avj5aY.css → index-B3iGh6pG.css} +1 -1
- package/dist/assets/index-BsbwdhBh.js +2910 -0
- package/dist/assets/{react-query-BiFBqyAt.js → react-query-DtPmeC_c.js} +1 -1
- package/dist/assets/{vendor-Q0KoFXrG.js → vendor-UNKt3GY8.js} +49 -49
- package/dist/index.html +4 -4
- package/lib/automations/workflow-list.js +147 -0
- package/lib/setup/control-panel-preferences.js +125 -0
- package/lib/setup/control-panel-principal.js +60 -0
- package/package.json +3 -3
- package/server/routes/control-panel-preferences.js +97 -0
- package/dist/assets/index-7omBpQ9G.js +0 -2900
package/dist/index.html
CHANGED
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Rubik:wght@500;700;800&family=Manrope:wght@400;500;600;700&display=swap"
|
|
12
12
|
rel="stylesheet"
|
|
13
13
|
/>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-BsbwdhBh.js"></script>
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/preact-vendor-CWXGD9A4.js">
|
|
16
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/assets/react-query-
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-UNKt3GY8.js">
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/react-query-DtPmeC_c.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="/assets/motion-m8lxAefi.js">
|
|
19
19
|
<link rel="modulepreload" crossorigin href="/assets/markdown-DMcD1LHz.js">
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B3iGh6pG.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="app"></div>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const WORKFLOW_SORT_MODES = [
|
|
2
|
+
{ value: "created_desc", label: "Created: newest first" },
|
|
3
|
+
{ value: "created_asc", label: "Created: oldest first" },
|
|
4
|
+
{ value: "name_asc", label: "Name: A to Z" },
|
|
5
|
+
{ value: "name_desc", label: "Name: Z to A" },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
const DEFAULT_WORKFLOW_SORT_MODE = WORKFLOW_SORT_MODES[0].value;
|
|
9
|
+
|
|
10
|
+
function normalizeWorkflowSortMode(raw) {
|
|
11
|
+
const value = String(raw || "").trim().toLowerCase();
|
|
12
|
+
if (WORKFLOW_SORT_MODES.some((mode) => mode.value === value)) return value;
|
|
13
|
+
return DEFAULT_WORKFLOW_SORT_MODE;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getAutomationId(row) {
|
|
17
|
+
return String(row?.automation_id || row?.automationId || row?.id || row?.routine_id || "").trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getAutomationName(row) {
|
|
21
|
+
return String(row?.name || row?.title || row?.label || getAutomationId(row) || "Automation").trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toNumber(value) {
|
|
25
|
+
const n = Number(value);
|
|
26
|
+
return Number.isFinite(n) ? n : 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseTimestampValue(value) {
|
|
30
|
+
if (!value) return 0;
|
|
31
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
|
|
32
|
+
if (typeof value === "string") {
|
|
33
|
+
const raw = value.trim();
|
|
34
|
+
if (!raw) return 0;
|
|
35
|
+
const numeric = Number(raw);
|
|
36
|
+
if (Number.isFinite(numeric) && raw === String(numeric)) return numeric;
|
|
37
|
+
const parsed = Date.parse(raw);
|
|
38
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getAutomationCreatedAtMs(row) {
|
|
44
|
+
const candidates = [
|
|
45
|
+
row?.created_at_ms,
|
|
46
|
+
row?.createdAtMs,
|
|
47
|
+
row?.created_at,
|
|
48
|
+
row?.createdAt,
|
|
49
|
+
row?.updated_at_ms,
|
|
50
|
+
row?.updatedAtMs,
|
|
51
|
+
row?.updated_at,
|
|
52
|
+
row?.updatedAt,
|
|
53
|
+
];
|
|
54
|
+
for (const candidate of candidates) {
|
|
55
|
+
const parsed = parseTimestampValue(candidate);
|
|
56
|
+
if (parsed > 0) return parsed;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatAutomationCreatedAtLabel(row) {
|
|
62
|
+
const createdAtMs = getAutomationCreatedAtMs(row);
|
|
63
|
+
if (!createdAtMs) return "";
|
|
64
|
+
const date = new Date(createdAtMs);
|
|
65
|
+
const datePart = new Intl.DateTimeFormat(undefined, {
|
|
66
|
+
month: "short",
|
|
67
|
+
day: "numeric",
|
|
68
|
+
year: "numeric",
|
|
69
|
+
}).format(date);
|
|
70
|
+
const timePart = new Intl.DateTimeFormat(undefined, {
|
|
71
|
+
hour: "numeric",
|
|
72
|
+
minute: "2-digit",
|
|
73
|
+
}).format(date);
|
|
74
|
+
return `${datePart} · ${timePart}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeFavoriteAutomationIds(ids) {
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const raw of Array.isArray(ids) ? ids : []) {
|
|
81
|
+
const id = String(raw || "").trim();
|
|
82
|
+
if (!id || seen.has(id)) continue;
|
|
83
|
+
seen.add(id);
|
|
84
|
+
out.push(id);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toggleFavoriteAutomationId(ids, automationId) {
|
|
90
|
+
const id = String(automationId || "").trim();
|
|
91
|
+
if (!id) return normalizeFavoriteAutomationIds(ids);
|
|
92
|
+
const current = normalizeFavoriteAutomationIds(ids);
|
|
93
|
+
if (current.includes(id)) {
|
|
94
|
+
return current.filter((rowId) => rowId !== id);
|
|
95
|
+
}
|
|
96
|
+
return [...current, id];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function compareAutomationRows(left, right, sortMode, favoriteSet) {
|
|
100
|
+
const leftId = getAutomationId(left);
|
|
101
|
+
const rightId = getAutomationId(right);
|
|
102
|
+
const leftFavorite = !!favoriteSet?.has?.(leftId);
|
|
103
|
+
const rightFavorite = !!favoriteSet?.has?.(rightId);
|
|
104
|
+
if (leftFavorite !== rightFavorite) return leftFavorite ? -1 : 1;
|
|
105
|
+
|
|
106
|
+
const leftName = getAutomationName(left);
|
|
107
|
+
const rightName = getAutomationName(right);
|
|
108
|
+
const leftCreated = getAutomationCreatedAtMs(left);
|
|
109
|
+
const rightCreated = getAutomationCreatedAtMs(right);
|
|
110
|
+
|
|
111
|
+
if (sortMode === "name_asc" || sortMode === "name_desc") {
|
|
112
|
+
const cmp = leftName.localeCompare(rightName, undefined, { sensitivity: "base" });
|
|
113
|
+
if (cmp) return sortMode === "name_desc" ? -cmp : cmp;
|
|
114
|
+
if (leftCreated !== rightCreated) return rightCreated - leftCreated;
|
|
115
|
+
return leftId.localeCompare(rightId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (leftCreated !== rightCreated) {
|
|
119
|
+
return sortMode === "created_asc" ? leftCreated - rightCreated : rightCreated - leftCreated;
|
|
120
|
+
}
|
|
121
|
+
const cmp = leftName.localeCompare(rightName, undefined, { sensitivity: "base" });
|
|
122
|
+
if (cmp) return cmp;
|
|
123
|
+
return leftId.localeCompare(rightId);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sortWorkflowAutomations(rows, options = {}) {
|
|
127
|
+
const sortMode = normalizeWorkflowSortMode(options.sortMode);
|
|
128
|
+
const favoriteSet =
|
|
129
|
+
options.favoriteAutomationIds instanceof Set
|
|
130
|
+
? options.favoriteAutomationIds
|
|
131
|
+
: new Set(normalizeFavoriteAutomationIds(options.favoriteAutomationIds));
|
|
132
|
+
return Array.isArray(rows) ? [...rows].sort((left, right) => compareAutomationRows(left, right, sortMode, favoriteSet)) : [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export {
|
|
136
|
+
DEFAULT_WORKFLOW_SORT_MODE,
|
|
137
|
+
WORKFLOW_SORT_MODES,
|
|
138
|
+
compareAutomationRows,
|
|
139
|
+
getAutomationCreatedAtMs,
|
|
140
|
+
getAutomationId,
|
|
141
|
+
getAutomationName,
|
|
142
|
+
formatAutomationCreatedAtLabel,
|
|
143
|
+
normalizeFavoriteAutomationIds,
|
|
144
|
+
normalizeWorkflowSortMode,
|
|
145
|
+
sortWorkflowAutomations,
|
|
146
|
+
toggleFavoriteAutomationId,
|
|
147
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { mkdir } from "fs/promises";
|
|
3
|
+
import { dirname, resolve } from "path";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_WORKFLOW_SORT_MODE,
|
|
6
|
+
normalizeFavoriteAutomationIds,
|
|
7
|
+
normalizeWorkflowSortMode,
|
|
8
|
+
} from "../automations/workflow-list.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CONTROL_PANEL_PREFERENCES = {
|
|
11
|
+
version: 1,
|
|
12
|
+
principals: {},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function normalizePrincipalPreferences(raw = {}, principalId = "") {
|
|
16
|
+
const input = raw && typeof raw === "object" ? raw : {};
|
|
17
|
+
const createdAtMs = Number(
|
|
18
|
+
input.created_at_ms || input.createdAtMs || input.createdAt || input.created_at || 0
|
|
19
|
+
);
|
|
20
|
+
const updatedAtMs = Number(
|
|
21
|
+
input.updated_at_ms || input.updatedAtMs || input.updatedAt || input.updated_at || 0
|
|
22
|
+
);
|
|
23
|
+
return {
|
|
24
|
+
principal_id: String(input.principal_id || principalId || "").trim(),
|
|
25
|
+
principal_scope: String(input.principal_scope || input.principalScope || "global").trim() || "global",
|
|
26
|
+
scope: input.scope && typeof input.scope === "object" ? { ...input.scope } : { kind: "global" },
|
|
27
|
+
created_at_ms: Number.isFinite(createdAtMs) && createdAtMs > 0 ? createdAtMs : Date.now(),
|
|
28
|
+
updated_at_ms: Number.isFinite(updatedAtMs) && updatedAtMs > 0 ? updatedAtMs : Date.now(),
|
|
29
|
+
favorite_automation_ids: normalizeFavoriteAutomationIds(
|
|
30
|
+
input.favorite_automation_ids || input.favoriteAutomationIds || []
|
|
31
|
+
),
|
|
32
|
+
workflow_sort_mode: normalizeWorkflowSortMode(
|
|
33
|
+
input.workflow_sort_mode || input.workflowSortMode || DEFAULT_WORKFLOW_SORT_MODE
|
|
34
|
+
),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeControlPanelPreferences(raw = {}) {
|
|
39
|
+
const input = raw && typeof raw === "object" ? raw : {};
|
|
40
|
+
const principals = input.principals && typeof input.principals === "object" ? input.principals : {};
|
|
41
|
+
const normalizedPrincipals = {};
|
|
42
|
+
for (const [principalId, prefs] of Object.entries(principals)) {
|
|
43
|
+
const key = String(principalId || "").trim();
|
|
44
|
+
if (!key) continue;
|
|
45
|
+
normalizedPrincipals[key] = normalizePrincipalPreferences(prefs, key);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
version: 1,
|
|
49
|
+
principals: normalizedPrincipals,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveControlPanelPreferencesPath(options = {}) {
|
|
54
|
+
const env = options.env || process.env;
|
|
55
|
+
const explicit = String(
|
|
56
|
+
options.explicitPath ||
|
|
57
|
+
env.TANDEM_CONTROL_PANEL_PREFERENCES_FILE ||
|
|
58
|
+
env.TANDEM_CONTROL_PANEL_PREFERENCES_PATH ||
|
|
59
|
+
""
|
|
60
|
+
).trim();
|
|
61
|
+
if (explicit) return resolve(explicit);
|
|
62
|
+
const stateDir = String(options.stateDir || env.TANDEM_CONTROL_PANEL_STATE_DIR || "").trim();
|
|
63
|
+
const fallbackStateDir = stateDir || resolve(process.cwd(), "tandem-data", "control-panel");
|
|
64
|
+
return resolve(fallbackStateDir, "control-panel-preferences.json");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readControlPanelPreferences(pathname, fallback = DEFAULT_CONTROL_PANEL_PREFERENCES) {
|
|
68
|
+
const target = String(pathname || "").trim();
|
|
69
|
+
if (!target || !existsSync(target)) {
|
|
70
|
+
return normalizeControlPanelPreferences(fallback);
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const raw = JSON.parse(readFileSync(target, "utf8"));
|
|
74
|
+
return normalizeControlPanelPreferences(raw);
|
|
75
|
+
} catch {
|
|
76
|
+
return normalizeControlPanelPreferences(fallback);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function writeControlPanelPreferences(pathname, payload) {
|
|
81
|
+
const target = resolve(String(pathname || "").trim());
|
|
82
|
+
const data = normalizeControlPanelPreferences(payload);
|
|
83
|
+
await mkdir(dirname(target), { recursive: true });
|
|
84
|
+
writeFileSync(target, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
85
|
+
return { path: target, preferences: data };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getPrincipalPreferences(store, principalId) {
|
|
89
|
+
const id = String(principalId || "").trim();
|
|
90
|
+
if (!id) return normalizePrincipalPreferences({}, "");
|
|
91
|
+
return normalizePrincipalPreferences(store?.principals?.[id] || {}, id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function upsertPrincipalPreferences(store, principalId, patch = {}) {
|
|
95
|
+
const normalizedStore = normalizeControlPanelPreferences(store);
|
|
96
|
+
const id = String(principalId || "").trim();
|
|
97
|
+
const current = getPrincipalPreferences(normalizedStore, id);
|
|
98
|
+
const next = normalizePrincipalPreferences(
|
|
99
|
+
{
|
|
100
|
+
...current,
|
|
101
|
+
...patch,
|
|
102
|
+
principal_id: id,
|
|
103
|
+
updated_at_ms: Date.now(),
|
|
104
|
+
},
|
|
105
|
+
id
|
|
106
|
+
);
|
|
107
|
+
return {
|
|
108
|
+
...normalizedStore,
|
|
109
|
+
principals: {
|
|
110
|
+
...normalizedStore.principals,
|
|
111
|
+
[id]: next,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export {
|
|
117
|
+
DEFAULT_CONTROL_PANEL_PREFERENCES,
|
|
118
|
+
getPrincipalPreferences,
|
|
119
|
+
normalizeControlPanelPreferences,
|
|
120
|
+
normalizePrincipalPreferences,
|
|
121
|
+
readControlPanelPreferences,
|
|
122
|
+
resolveControlPanelPreferencesPath,
|
|
123
|
+
upsertPrincipalPreferences,
|
|
124
|
+
writeControlPanelPreferences,
|
|
125
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
|
|
3
|
+
function stablePrincipalHash(raw) {
|
|
4
|
+
return createHash("sha256").update(String(raw || "")).digest("hex").slice(0, 24);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function resolveControlPanelPrincipalIdentity(session = {}) {
|
|
8
|
+
const explicitPrincipalId = String(
|
|
9
|
+
session?.principal_id ||
|
|
10
|
+
session?.principalId ||
|
|
11
|
+
session?.profile_id ||
|
|
12
|
+
session?.profileId ||
|
|
13
|
+
session?.subject_id ||
|
|
14
|
+
session?.subjectId ||
|
|
15
|
+
session?.user_id ||
|
|
16
|
+
session?.userId ||
|
|
17
|
+
""
|
|
18
|
+
).trim();
|
|
19
|
+
if (explicitPrincipalId) {
|
|
20
|
+
return {
|
|
21
|
+
principal_id: explicitPrincipalId,
|
|
22
|
+
principal_source: String(
|
|
23
|
+
session?.principal_source ||
|
|
24
|
+
session?.principalSource ||
|
|
25
|
+
session?.profile_source ||
|
|
26
|
+
session?.profileSource ||
|
|
27
|
+
session?.subject_source ||
|
|
28
|
+
session?.subjectSource ||
|
|
29
|
+
"session"
|
|
30
|
+
).trim(),
|
|
31
|
+
principal_scope: String(session?.principal_scope || session?.principalScope || "global").trim() || "global",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const token = String(session?.token || "").trim();
|
|
36
|
+
if (token) {
|
|
37
|
+
return {
|
|
38
|
+
principal_id: `cp_${stablePrincipalHash(`token:${token}`)}`,
|
|
39
|
+
principal_source: "session_token",
|
|
40
|
+
principal_scope: "global",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sid = String(session?.sid || "").trim();
|
|
45
|
+
if (sid) {
|
|
46
|
+
return {
|
|
47
|
+
principal_id: `cp_${stablePrincipalHash(`sid:${sid}`)}`,
|
|
48
|
+
principal_source: "session_id",
|
|
49
|
+
principal_scope: "global",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
principal_id: "",
|
|
55
|
+
principal_source: "unknown",
|
|
56
|
+
principal_scope: "global",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { resolveControlPanelPrincipalIdentity };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frumu/tandem-panel",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.21",
|
|
4
4
|
"description": "Full web control center for Tandem Engine (chat, routines, swarm, memory, channels, and ops)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"@fullcalendar/interaction": "6.1.20",
|
|
47
47
|
"@fullcalendar/react": "6.1.20",
|
|
48
48
|
"@fullcalendar/timegrid": "6.1.20",
|
|
49
|
-
"@frumu/tandem": "^0.4.
|
|
50
|
-
"@frumu/tandem-client": "^0.4.
|
|
49
|
+
"@frumu/tandem": "^0.4.21",
|
|
50
|
+
"@frumu/tandem-client": "^0.4.21",
|
|
51
51
|
"@tanstack/react-query": "^5.90.21",
|
|
52
52
|
"dompurify": "^3.3.1",
|
|
53
53
|
"lucide": "^0.575.0",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPrincipalPreferences,
|
|
3
|
+
readControlPanelPreferences,
|
|
4
|
+
resolveControlPanelPreferencesPath,
|
|
5
|
+
upsertPrincipalPreferences,
|
|
6
|
+
writeControlPanelPreferences,
|
|
7
|
+
} from "../../lib/setup/control-panel-preferences.js";
|
|
8
|
+
|
|
9
|
+
function resolvePrincipalIdentity(deps, session) {
|
|
10
|
+
const resolver = deps.resolvePrincipalIdentity;
|
|
11
|
+
if (typeof resolver === "function") return resolver(session);
|
|
12
|
+
return { principal_id: "", principal_source: "unknown", principal_scope: "global" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createControlPanelPreferencesHandler(deps) {
|
|
16
|
+
const {
|
|
17
|
+
CONTROL_PANEL_PREFERENCES_FILE,
|
|
18
|
+
TANDEM_CONTROL_PANEL_STATE_DIR,
|
|
19
|
+
sendJson,
|
|
20
|
+
readJsonBody,
|
|
21
|
+
} = deps;
|
|
22
|
+
|
|
23
|
+
function getPreferencesPath() {
|
|
24
|
+
return resolveControlPanelPreferencesPath({
|
|
25
|
+
env: {
|
|
26
|
+
TANDEM_CONTROL_PANEL_PREFERENCES_FILE: CONTROL_PANEL_PREFERENCES_FILE,
|
|
27
|
+
TANDEM_CONTROL_PANEL_STATE_DIR,
|
|
28
|
+
},
|
|
29
|
+
explicitPath: CONTROL_PANEL_PREFERENCES_FILE,
|
|
30
|
+
stateDir: TANDEM_CONTROL_PANEL_STATE_DIR,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadPreferences() {
|
|
35
|
+
return readControlPanelPreferences(getPreferencesPath());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function persistPreferences(preferences) {
|
|
39
|
+
return writeControlPanelPreferences(getPreferencesPath(), preferences);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return async function handleControlPanelPreferences(req, res, session) {
|
|
43
|
+
const url = new URL(req.url, "http://127.0.0.1");
|
|
44
|
+
const principal = resolvePrincipalIdentity(deps, session);
|
|
45
|
+
const principalId = String(principal?.principal_id || "").trim();
|
|
46
|
+
if (!principalId) {
|
|
47
|
+
sendJson(res, 401, { ok: false, error: "Session principal could not be resolved." });
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (url.pathname === "/api/control-panel/preferences" && req.method === "GET") {
|
|
52
|
+
const store = loadPreferences();
|
|
53
|
+
const preferences = getPrincipalPreferences(store, principalId);
|
|
54
|
+
sendJson(res, 200, {
|
|
55
|
+
ok: true,
|
|
56
|
+
principal_id: principalId,
|
|
57
|
+
principal_source: String(principal?.principal_source || "unknown"),
|
|
58
|
+
principal_scope: String(principal?.principal_scope || preferences.principal_scope || "global"),
|
|
59
|
+
preferences,
|
|
60
|
+
});
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (url.pathname === "/api/control-panel/preferences" && req.method === "PATCH") {
|
|
65
|
+
try {
|
|
66
|
+
const payload = await readJsonBody(req);
|
|
67
|
+
const incoming = payload?.preferences && typeof payload.preferences === "object" ? payload.preferences : payload || {};
|
|
68
|
+
const store = loadPreferences();
|
|
69
|
+
const current = getPrincipalPreferences(store, principalId);
|
|
70
|
+
const nextPreferences = upsertPrincipalPreferences(store, principalId, {
|
|
71
|
+
...current,
|
|
72
|
+
...incoming,
|
|
73
|
+
principal_id: principalId,
|
|
74
|
+
principal_scope:
|
|
75
|
+
String(incoming.principal_scope || incoming.principalScope || current.principal_scope || "global")
|
|
76
|
+
.trim() || "global",
|
|
77
|
+
});
|
|
78
|
+
const saved = await persistPreferences(nextPreferences);
|
|
79
|
+
sendJson(res, 200, {
|
|
80
|
+
ok: true,
|
|
81
|
+
principal_id: principalId,
|
|
82
|
+
principal_source: String(principal?.principal_source || "unknown"),
|
|
83
|
+
principal_scope: String(principal?.principal_scope || "global"),
|
|
84
|
+
preferences: getPrincipalPreferences(saved.preferences, principalId),
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
sendJson(res, 400, {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: error instanceof Error ? error.message : String(error),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
96
|
+
};
|
|
97
|
+
}
|