paneltir 0.6.0 → 0.8.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.
- package/INSTALL.md +2 -2
- package/README.md +1 -1
- package/bin/paneltir.mjs +6 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +172 -33
- package/fingerprint.json +4 -4
- package/package.json +3 -2
- package/template/api/panel-state.ts +139 -3
package/INSTALL.md
CHANGED
|
@@ -19,7 +19,7 @@ identity** — never another project's.
|
|
|
19
19
|
npm install paneltir
|
|
20
20
|
|
|
21
21
|
# or, without npm access, straight from the repository:
|
|
22
|
-
# npm install github:daifukus/paneltir#v0.
|
|
22
|
+
# npm install github:daifukus/paneltir#v0.8.0
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
(Use the tag you were given; never install without pinning a version.)
|
|
@@ -181,7 +181,7 @@ identity** — never another project's.
|
|
|
181
181
|
|
|
182
182
|
```bash
|
|
183
183
|
npx paneltir version # version, fingerprint, where it came from
|
|
184
|
-
npx paneltir check 0.
|
|
184
|
+
npx paneltir check 0.8.0 # exits non-zero if that is not what is installed
|
|
185
185
|
npx paneltir board # read the board and say what is wrong with it
|
|
186
186
|
```
|
|
187
187
|
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ tag rather than a branch, so an unfinished push cannot reach a project:
|
|
|
54
54
|
npm install paneltir
|
|
55
55
|
|
|
56
56
|
# or, without npm access, straight from the repository:
|
|
57
|
-
# npm install github:daifukus/paneltir#v0.
|
|
57
|
+
# npm install github:daifukus/paneltir#v0.8.0
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
Installing from Git runs the `prepare` script, which builds `dist/`, so the
|
package/bin/paneltir.mjs
CHANGED
|
@@ -239,7 +239,12 @@ function version() {
|
|
|
239
239
|
console.log(`paneltir v${pkg.version}`)
|
|
240
240
|
console.log(`fingerprint ${fingerprint.hash}`)
|
|
241
241
|
console.log(`${fingerprint.fileCount} tracked files`)
|
|
242
|
-
|
|
242
|
+
// It used to print "installed from github:daifukus/paneltir#vX.Y.Z", which
|
|
243
|
+
// was true while that was the only way in and became a guess the moment the
|
|
244
|
+
// package went to npm. A copy on disk does not carry where it was fetched
|
|
245
|
+
// from — npm records that in the consumer's lockfile, not here — so the
|
|
246
|
+
// line is gone rather than wrong. The version and the fingerprint are facts;
|
|
247
|
+
// that was not one.
|
|
243
248
|
}
|
|
244
249
|
|
|
245
250
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -872,6 +872,14 @@ interface Preferences {
|
|
|
872
872
|
seen?: string[];
|
|
873
873
|
/** Ids of guide notes already dismissed. */
|
|
874
874
|
guideDismissed?: string[];
|
|
875
|
+
/**
|
|
876
|
+
* Whether an update card is written as an order rather than a suggestion.
|
|
877
|
+
*
|
|
878
|
+
* It does not make anything automatic — the panel cannot run npm, and this
|
|
879
|
+
* changes `order` on the card it offers to write, nothing else. Off means
|
|
880
|
+
* the card asks; on means it tells.
|
|
881
|
+
*/
|
|
882
|
+
updateAsOrder?: boolean;
|
|
875
883
|
}
|
|
876
884
|
interface PanelState {
|
|
877
885
|
v: 2;
|
|
@@ -1115,6 +1123,21 @@ interface UiStrings {
|
|
|
1115
1123
|
all: string;
|
|
1116
1124
|
board: string;
|
|
1117
1125
|
analysis: string;
|
|
1126
|
+
/** The Revisions tab. Short, because it sits beside Board and Analysis. */
|
|
1127
|
+
history: string;
|
|
1128
|
+
updateTitle: string;
|
|
1129
|
+
updateSubtitle: string;
|
|
1130
|
+
updateCurrent: (version: string) => string;
|
|
1131
|
+
updateAvailable: (version: string) => string;
|
|
1132
|
+
updateUpToDate: string;
|
|
1133
|
+
updateUnknown: string;
|
|
1134
|
+
updateCreateCard: string;
|
|
1135
|
+
updateCardExists: string;
|
|
1136
|
+
updateAsOrder: string;
|
|
1137
|
+
updateAsOrderHow: string;
|
|
1138
|
+
notifUpdate: string;
|
|
1139
|
+
updateCardTitle: (version: string) => string;
|
|
1140
|
+
updateCardBody: (version: string) => string;
|
|
1118
1141
|
cardDetail: string;
|
|
1119
1142
|
panelSettings: string;
|
|
1120
1143
|
title: string;
|
|
@@ -1417,6 +1440,14 @@ interface PanelAppProps {
|
|
|
1417
1440
|
showLanguage?: boolean;
|
|
1418
1441
|
/** The revision log: one entry per session, newest first. */
|
|
1419
1442
|
showHistory?: boolean;
|
|
1443
|
+
/**
|
|
1444
|
+
* Ask the public npm registry whether a newer kit has been published.
|
|
1445
|
+
*
|
|
1446
|
+
* Opt-in, because it is a request leaving the reader's browser and that is
|
|
1447
|
+
* the caller's decision rather than the kit's. Nothing is installed either
|
|
1448
|
+
* way: the panel offers to write a card, and a session acts on it.
|
|
1449
|
+
*/
|
|
1450
|
+
checkForUpdates?: boolean;
|
|
1420
1451
|
/** Endpoint that commits the board back to the repository, when there is one. */
|
|
1421
1452
|
saveEndpoint?: string;
|
|
1422
1453
|
/**
|
|
@@ -1434,7 +1465,7 @@ interface PanelAppProps {
|
|
|
1434
1465
|
fileCount: number;
|
|
1435
1466
|
};
|
|
1436
1467
|
}
|
|
1437
|
-
declare function PanelApp({ state: initialState, themes, defaultTheme, brand, decoration, className, footer, links, capabilities, extras, showLanguage, showHistory, saveEndpoint, marketplaceEndpoint, marketplaceReport, fingerprint, }: PanelAppProps): React.JSX.Element;
|
|
1468
|
+
declare function PanelApp({ state: initialState, themes, defaultTheme, brand, decoration, className, footer, links, capabilities, extras, showLanguage, showHistory, checkForUpdates, saveEndpoint, marketplaceEndpoint, marketplaceReport, fingerprint, }: PanelAppProps): React.JSX.Element;
|
|
1438
1469
|
|
|
1439
1470
|
/**
|
|
1440
1471
|
* Applies a board move to the flat card list.
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var PANELTIR_VERSION = "0.
|
|
3
|
-
var PANELTIR_FINGERPRINT = "sha256:
|
|
4
|
-
var PANELTIR_FILE_COUNT =
|
|
2
|
+
var PANELTIR_VERSION = "0.8.0";
|
|
3
|
+
var PANELTIR_FINGERPRINT = "sha256:a252ef2a015240e0e8463c0ebb0ef7a8af80ad7ade66f2c48e71657500d506d5";
|
|
4
|
+
var PANELTIR_FILE_COUNT = 153;
|
|
5
5
|
var paneltirBuild = {
|
|
6
6
|
version: PANELTIR_VERSION,
|
|
7
7
|
fingerprint: PANELTIR_FINGERPRINT,
|
|
@@ -1302,6 +1302,37 @@ function isCard(value) {
|
|
|
1302
1302
|
// src/panel/PanelApp.tsx
|
|
1303
1303
|
import React12, { useCallback as useCallback5, useEffect as useEffect9, useMemo as useMemo5, useState as useState9 } from "react";
|
|
1304
1304
|
|
|
1305
|
+
// src/panel/updates.ts
|
|
1306
|
+
var REGISTRY_URL = "https://registry.npmjs.org/paneltir/latest";
|
|
1307
|
+
function isNewer(candidate, current) {
|
|
1308
|
+
const parts = (value) => value.trim().replace(/^v/, "").split(/[-+]/)[0].split(".").map((piece) => Number.parseInt(piece, 10));
|
|
1309
|
+
const left = parts(candidate);
|
|
1310
|
+
const right = parts(current);
|
|
1311
|
+
if (left.some(Number.isNaN) || right.some(Number.isNaN)) return false;
|
|
1312
|
+
for (let i = 0; i < 3; i += 1) {
|
|
1313
|
+
const a = left[i] ?? 0;
|
|
1314
|
+
const b = right[i] ?? 0;
|
|
1315
|
+
if (a !== b) return a > b;
|
|
1316
|
+
}
|
|
1317
|
+
return false;
|
|
1318
|
+
}
|
|
1319
|
+
async function checkForUpdate(current, fetchImpl = fetch) {
|
|
1320
|
+
try {
|
|
1321
|
+
const response = await fetchImpl(REGISTRY_URL, { headers: { accept: "application/json" } });
|
|
1322
|
+
if (!response.ok) return null;
|
|
1323
|
+
const body = await response.json();
|
|
1324
|
+
const latest = typeof body.version === "string" ? body.version : null;
|
|
1325
|
+
if (!latest) return null;
|
|
1326
|
+
return { current, latest, behind: isNewer(latest, current) };
|
|
1327
|
+
} catch {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function updateCardId(version) {
|
|
1332
|
+
const found = /\d+\.\d+\.\d+/.exec(version);
|
|
1333
|
+
return `paneltir-update-${found ? found[0] : "unknown"}`;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1305
1336
|
// src/panel/themes.ts
|
|
1306
1337
|
var PRESET_FORM = {
|
|
1307
1338
|
midnight: THEME_FORMS.panel,
|
|
@@ -1355,6 +1386,20 @@ var UI = {
|
|
|
1355
1386
|
all: "All",
|
|
1356
1387
|
board: "Board",
|
|
1357
1388
|
analysis: "Analysis",
|
|
1389
|
+
history: "Revisions",
|
|
1390
|
+
updateTitle: "The kit this panel runs on",
|
|
1391
|
+
updateSubtitle: "What is installed, and what has been published since.",
|
|
1392
|
+
updateCurrent: (version) => `Installed: v${version}`,
|
|
1393
|
+
updateAvailable: (version) => `v${version} has been published.`,
|
|
1394
|
+
updateUpToDate: "This is the newest published version.",
|
|
1395
|
+
updateUnknown: "The registry could not be reached, so there is nothing to report either way.",
|
|
1396
|
+
updateCreateCard: "Add a card asking for it",
|
|
1397
|
+
updateCardExists: "The board already carries a card for this version.",
|
|
1398
|
+
updateAsOrder: "Write it as an order, not a suggestion",
|
|
1399
|
+
updateAsOrderHow: "This does not update anything on its own \u2014 the panel cannot run npm. It decides whether the card it writes asks for the update or instructs it.",
|
|
1400
|
+
notifUpdate: "A newer Paneltir has been published",
|
|
1401
|
+
updateCardTitle: (version) => `Update Paneltir to v${version}`,
|
|
1402
|
+
updateCardBody: (version) => `Run \`npm install paneltir@${version}\` in this project, then check the panel still builds and renders. The version in use is reported by \`paneltirBuild\`, so it can be confirmed rather than assumed.`,
|
|
1358
1403
|
cardDetail: "Card",
|
|
1359
1404
|
panelSettings: "Panel settings",
|
|
1360
1405
|
title: "Title",
|
|
@@ -1615,6 +1660,20 @@ var UI = {
|
|
|
1615
1660
|
all: "Todo",
|
|
1616
1661
|
board: "Tablero",
|
|
1617
1662
|
analysis: "An\xE1lisis",
|
|
1663
|
+
history: "Revisiones",
|
|
1664
|
+
updateTitle: "El kit sobre el que corre este panel",
|
|
1665
|
+
updateSubtitle: "Qu\xE9 hay instalado, y qu\xE9 se ha publicado desde entonces.",
|
|
1666
|
+
updateCurrent: (version) => `Instalada: v${version}`,
|
|
1667
|
+
updateAvailable: (version) => `Se ha publicado la v${version}.`,
|
|
1668
|
+
updateUpToDate: "Esta es la versi\xF3n publicada m\xE1s reciente.",
|
|
1669
|
+
updateUnknown: "No se pudo consultar el registro, as\xED que no hay nada que decir en ning\xFAn sentido.",
|
|
1670
|
+
updateCreateCard: "A\xF1adir una tarjeta pidi\xE9ndolo",
|
|
1671
|
+
updateCardExists: "El tablero ya lleva una tarjeta para esta versi\xF3n.",
|
|
1672
|
+
updateAsOrder: "Escribirla como orden, no como sugerencia",
|
|
1673
|
+
updateAsOrderHow: "Esto no actualiza nada por su cuenta \u2014 el panel no puede ejecutar npm. Decide si la tarjeta que escribe pide la actualizaci\xF3n o la manda.",
|
|
1674
|
+
notifUpdate: "Se ha publicado un Paneltir m\xE1s nuevo",
|
|
1675
|
+
updateCardTitle: (version) => `Actualizar Paneltir a la v${version}`,
|
|
1676
|
+
updateCardBody: (version) => `Ejecuta \`npm install paneltir@${version}\` en este proyecto y comprueba que el panel sigue construy\xE9ndose y dibuj\xE1ndose. La versi\xF3n en uso la reporta \`paneltirBuild\`, as\xED que se puede confirmar en vez de darla por hecha.`,
|
|
1618
1677
|
cardDetail: "Tarjeta",
|
|
1619
1678
|
panelSettings: "Ajustes del panel",
|
|
1620
1679
|
title: "T\xEDtulo",
|
|
@@ -2651,6 +2710,20 @@ function applyMove(cards, move, visible) {
|
|
|
2651
2710
|
// src/panel/PanelApp.tsx
|
|
2652
2711
|
import { Fragment as Fragment5, jsx as jsx19, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
2653
2712
|
var IMPORTED = "imported";
|
|
2713
|
+
function parseJson(body) {
|
|
2714
|
+
try {
|
|
2715
|
+
const parsed = JSON.parse(body);
|
|
2716
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
2717
|
+
} catch {
|
|
2718
|
+
return {};
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
function saveError(status, data, body) {
|
|
2722
|
+
if (typeof data.error === "string" && data.error) return data.error;
|
|
2723
|
+
const code = body.match(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/)?.[0];
|
|
2724
|
+
const id = body.match(/\bID:\s*([\w:.-]+)/i)?.[1];
|
|
2725
|
+
return [`HTTP ${status}`, code, id && `id ${id}`].filter(Boolean).join(" \u2014 ");
|
|
2726
|
+
}
|
|
2654
2727
|
var PRIORITY_TONE = {
|
|
2655
2728
|
high: "danger",
|
|
2656
2729
|
medium: "warning",
|
|
@@ -2669,6 +2742,7 @@ function PanelApp({
|
|
|
2669
2742
|
extras,
|
|
2670
2743
|
showLanguage,
|
|
2671
2744
|
showHistory,
|
|
2745
|
+
checkForUpdates,
|
|
2672
2746
|
saveEndpoint,
|
|
2673
2747
|
marketplaceEndpoint,
|
|
2674
2748
|
marketplaceReport,
|
|
@@ -2686,6 +2760,7 @@ function PanelApp({
|
|
|
2686
2760
|
const [filter, setFilter] = useState9("all");
|
|
2687
2761
|
const [openCardId, setOpenCardId] = useState9(null);
|
|
2688
2762
|
const [settingsOpen, setSettingsOpen] = useState9(false);
|
|
2763
|
+
const [update, setUpdate] = useState9(null);
|
|
2689
2764
|
const [helpOpen, setHelpOpen] = useState9(false);
|
|
2690
2765
|
const [setup, setSetup] = useState9(null);
|
|
2691
2766
|
const [signOut, setSignOut] = useState9("idle");
|
|
@@ -2794,6 +2869,15 @@ function PanelApp({
|
|
|
2794
2869
|
onOpen: () => setOpenCardId(card.id)
|
|
2795
2870
|
});
|
|
2796
2871
|
}
|
|
2872
|
+
if (update?.behind && !state.cards.some((card) => card.id === updateCardId(update.latest))) {
|
|
2873
|
+
items.push({
|
|
2874
|
+
id: `paneltir-update:${update.latest}`,
|
|
2875
|
+
tone: "info",
|
|
2876
|
+
title: ui.notifUpdate,
|
|
2877
|
+
detail: ui.updateAvailable(update.latest),
|
|
2878
|
+
onOpen: () => setSettingsOpen(true)
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2797
2881
|
if (state.importedTheme?.refreshRequested) {
|
|
2798
2882
|
items.push({
|
|
2799
2883
|
id: `theme-refresh:${state.importedTheme.refreshRequested}`,
|
|
@@ -2873,6 +2957,23 @@ function PanelApp({
|
|
|
2873
2957
|
},
|
|
2874
2958
|
[state.areas]
|
|
2875
2959
|
);
|
|
2960
|
+
const addUpdateCard = useCallback5(() => {
|
|
2961
|
+
if (!update?.behind) return;
|
|
2962
|
+
const id = updateCardId(update.latest);
|
|
2963
|
+
if (state.cards.some((card2) => card2.id === id)) return;
|
|
2964
|
+
const card = {
|
|
2965
|
+
...emptyCard("next", state.areas[0]?.id ?? "general"),
|
|
2966
|
+
id,
|
|
2967
|
+
title: { en: UI.en.updateCardTitle(update.latest), es: UI.es.updateCardTitle(update.latest) },
|
|
2968
|
+
body: { en: UI.en.updateCardBody(update.latest), es: UI.es.updateCardBody(update.latest) },
|
|
2969
|
+
priority: "medium",
|
|
2970
|
+
risk: "low",
|
|
2971
|
+
owner: "claude",
|
|
2972
|
+
order: state.preferences?.updateAsOrder === true
|
|
2973
|
+
};
|
|
2974
|
+
setState((current) => ({ ...current, updatedAt: today(), cards: [...current.cards, card] }));
|
|
2975
|
+
setOpenCardId(id);
|
|
2976
|
+
}, [update, state.cards, state.areas, state.preferences?.updateAsOrder]);
|
|
2876
2977
|
const moveCard = useCallback5(
|
|
2877
2978
|
(move) => {
|
|
2878
2979
|
setState((current) => {
|
|
@@ -2992,6 +3093,16 @@ function PanelApp({
|
|
|
2992
3093
|
setMarketAsked(true);
|
|
2993
3094
|
loadMarketplace(false);
|
|
2994
3095
|
}, [view, marketAsked, marketplaceEndpoint, loadMarketplace]);
|
|
3096
|
+
useEffect9(() => {
|
|
3097
|
+
if (!checkForUpdates || !fingerprint?.version) return;
|
|
3098
|
+
let cancelled = false;
|
|
3099
|
+
void checkForUpdate(fingerprint.version).then((status2) => {
|
|
3100
|
+
if (!cancelled) setUpdate(status2);
|
|
3101
|
+
});
|
|
3102
|
+
return () => {
|
|
3103
|
+
cancelled = true;
|
|
3104
|
+
};
|
|
3105
|
+
}, [checkForUpdates, fingerprint?.version]);
|
|
2995
3106
|
useEffect9(() => {
|
|
2996
3107
|
if (!helpOpen || !saveEndpoint || setup) return;
|
|
2997
3108
|
let cancelled = false;
|
|
@@ -3025,10 +3136,11 @@ function PanelApp({
|
|
|
3025
3136
|
credentials: "same-origin",
|
|
3026
3137
|
body: JSON.stringify({ state: next, message: "chore(panel): update the board from the panel" })
|
|
3027
3138
|
});
|
|
3028
|
-
const
|
|
3139
|
+
const body = await response.text().catch(() => "");
|
|
3140
|
+
const data = parseJson(body);
|
|
3029
3141
|
if (!response.ok) {
|
|
3030
3142
|
const conflict = response.status === 409;
|
|
3031
|
-
throw Object.assign(new Error(
|
|
3143
|
+
throw Object.assign(new Error(saveError(response.status, data, body)), { conflict });
|
|
3032
3144
|
}
|
|
3033
3145
|
setSavedSnapshot(JSON.stringify(state));
|
|
3034
3146
|
setSave({
|
|
@@ -3078,7 +3190,7 @@ function PanelApp({
|
|
|
3078
3190
|
),
|
|
3079
3191
|
status,
|
|
3080
3192
|
controls: /* @__PURE__ */ jsxs17(Fragment5, { children: [
|
|
3081
|
-
(initialState.analysis || hasMarketplace) && /* @__PURE__ */ jsxs17("span", { className: "pt-views", children: [
|
|
3193
|
+
(initialState.analysis || hasMarketplace || showHistory) && /* @__PURE__ */ jsxs17("span", { className: "pt-views", children: [
|
|
3082
3194
|
/* @__PURE__ */ jsx19(
|
|
3083
3195
|
"button",
|
|
3084
3196
|
{
|
|
@@ -3108,6 +3220,16 @@ function PanelApp({
|
|
|
3108
3220
|
onClick: () => setView("marketplace"),
|
|
3109
3221
|
children: ui.marketplace
|
|
3110
3222
|
}
|
|
3223
|
+
),
|
|
3224
|
+
showHistory && /* @__PURE__ */ jsx19(
|
|
3225
|
+
"button",
|
|
3226
|
+
{
|
|
3227
|
+
type: "button",
|
|
3228
|
+
className: `pt-lang${view === "history" ? " is-on" : ""}`,
|
|
3229
|
+
"aria-pressed": view === "history",
|
|
3230
|
+
onClick: () => setView("history"),
|
|
3231
|
+
children: ui.history
|
|
3232
|
+
}
|
|
3111
3233
|
)
|
|
3112
3234
|
] }),
|
|
3113
3235
|
showLanguage && LANGS.map((code) => /* @__PURE__ */ jsx19(
|
|
@@ -3335,33 +3457,6 @@ function PanelApp({
|
|
|
3335
3457
|
)
|
|
3336
3458
|
}
|
|
3337
3459
|
),
|
|
3338
|
-
showHistory && /* @__PURE__ */ jsxs17("section", { className: "pt-panel-page pt-panel-section", id: "revisions", children: [
|
|
3339
|
-
/* @__PURE__ */ jsxs17("div", { children: [
|
|
3340
|
-
/* @__PURE__ */ jsx19("p", { className: "pt-eyebrow", children: ui.historyEyebrow }),
|
|
3341
|
-
/* @__PURE__ */ jsx19("h2", { className: "pt-h2", children: ui.historyTitle }),
|
|
3342
|
-
/* @__PURE__ */ jsx19("p", { className: "pt-sub", children: ui.historyIntro })
|
|
3343
|
-
] }),
|
|
3344
|
-
state.runs.length === 0 ? /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: ui.historyEmpty }) : /* @__PURE__ */ jsx19("ol", { className: "pt-runs", children: [...state.runs].reverse().map((run) => /* @__PURE__ */ jsxs17("li", { className: "pt-run pt-hud", children: [
|
|
3345
|
-
/* @__PURE__ */ jsxs17("p", { className: "pt-run__meta", children: [
|
|
3346
|
-
run.date,
|
|
3347
|
-
" \xB7 ",
|
|
3348
|
-
run.by === "claude" ? "Claude" : ui.ownerYou,
|
|
3349
|
-
run.fingerprint ? ` \xB7 ${run.fingerprint.replace("sha256:", "").slice(0, 12)}` : ""
|
|
3350
|
-
] }),
|
|
3351
|
-
/* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: t(run.summary) }),
|
|
3352
|
-
run.cards.length > 0 && /* @__PURE__ */ jsxs17("p", { className: "pt-run__cards", children: [
|
|
3353
|
-
/* @__PURE__ */ jsxs17("span", { children: [
|
|
3354
|
-
ui.historyTouched,
|
|
3355
|
-
":"
|
|
3356
|
-
] }),
|
|
3357
|
-
" ",
|
|
3358
|
-
run.cards.map((id) => {
|
|
3359
|
-
const card = state.cards.find((candidate) => candidate.id === id);
|
|
3360
|
-
return card ? t(card.title) || ui.untitled : id;
|
|
3361
|
-
}).join(" \xB7 ")
|
|
3362
|
-
] })
|
|
3363
|
-
] }, run.id)) })
|
|
3364
|
-
] }),
|
|
3365
3460
|
extras,
|
|
3366
3461
|
capabilities && capabilities.length > 0 && /* @__PURE__ */ jsxs17("section", { className: "pt-panel-page pt-panel-section", id: "functions", children: [
|
|
3367
3462
|
/* @__PURE__ */ jsxs17("div", { children: [
|
|
@@ -3380,6 +3475,33 @@ function PanelApp({
|
|
|
3380
3475
|
] }, capability.id)) })
|
|
3381
3476
|
] })
|
|
3382
3477
|
] }),
|
|
3478
|
+
view === "history" && showHistory && /* @__PURE__ */ jsxs17("section", { className: "pt-panel-page pt-panel-section", id: "revisions", children: [
|
|
3479
|
+
/* @__PURE__ */ jsxs17("div", { children: [
|
|
3480
|
+
/* @__PURE__ */ jsx19("p", { className: "pt-eyebrow", children: ui.historyEyebrow }),
|
|
3481
|
+
/* @__PURE__ */ jsx19("h2", { className: "pt-h2", children: ui.historyTitle }),
|
|
3482
|
+
/* @__PURE__ */ jsx19("p", { className: "pt-sub", children: ui.historyIntro })
|
|
3483
|
+
] }),
|
|
3484
|
+
state.runs.length === 0 ? /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: ui.historyEmpty }) : /* @__PURE__ */ jsx19("ol", { className: "pt-runs", children: [...state.runs].reverse().map((run) => /* @__PURE__ */ jsxs17("li", { className: "pt-run pt-hud", children: [
|
|
3485
|
+
/* @__PURE__ */ jsxs17("p", { className: "pt-run__meta", children: [
|
|
3486
|
+
run.date,
|
|
3487
|
+
" \xB7 ",
|
|
3488
|
+
run.by === "claude" ? "Claude" : ui.ownerYou,
|
|
3489
|
+
run.fingerprint ? ` \xB7 ${run.fingerprint.replace("sha256:", "").slice(0, 12)}` : ""
|
|
3490
|
+
] }),
|
|
3491
|
+
/* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: t(run.summary) }),
|
|
3492
|
+
run.cards.length > 0 && /* @__PURE__ */ jsxs17("p", { className: "pt-run__cards", children: [
|
|
3493
|
+
/* @__PURE__ */ jsxs17("span", { children: [
|
|
3494
|
+
ui.historyTouched,
|
|
3495
|
+
":"
|
|
3496
|
+
] }),
|
|
3497
|
+
" ",
|
|
3498
|
+
run.cards.map((id) => {
|
|
3499
|
+
const card = state.cards.find((candidate) => candidate.id === id);
|
|
3500
|
+
return card ? t(card.title) || ui.untitled : id;
|
|
3501
|
+
}).join(" \xB7 ")
|
|
3502
|
+
] })
|
|
3503
|
+
] }, run.id)) })
|
|
3504
|
+
] }),
|
|
3383
3505
|
/* @__PURE__ */ jsx19(
|
|
3384
3506
|
CardSheet,
|
|
3385
3507
|
{
|
|
@@ -3485,6 +3607,23 @@ function PanelApp({
|
|
|
3485
3607
|
/* @__PURE__ */ jsx19("p", { className: "pt-marketplace__detail", children: ui.importedRefreshHow })
|
|
3486
3608
|
] })
|
|
3487
3609
|
] }),
|
|
3610
|
+
checkForUpdates && fingerprint?.version && /* @__PURE__ */ jsxs17(DetailSection, { title: ui.updateTitle, subtitle: ui.updateSubtitle, children: [
|
|
3611
|
+
/* @__PURE__ */ jsx19("p", { className: "pt-panel-text", children: ui.updateCurrent(fingerprint.version) }),
|
|
3612
|
+
/* @__PURE__ */ jsx19("p", { className: "pt-panel-text", style: { marginTop: 8 }, children: !update ? ui.updateUnknown : update.behind ? ui.updateAvailable(update.latest) : ui.updateUpToDate }),
|
|
3613
|
+
update?.behind && (state.cards.some((card) => card.id === updateCardId(update.latest)) ? /* @__PURE__ */ jsx19("p", { className: "pt-panel-text", style: { marginTop: 8 }, children: ui.updateCardExists }) : /* @__PURE__ */ jsx19("div", { className: "pt-panel-row", style: { marginTop: 12 }, children: /* @__PURE__ */ jsx19("button", { type: "button", className: "pt-btn", onClick: addUpdateCard, children: ui.updateCreateCard }) })),
|
|
3614
|
+
/* @__PURE__ */ jsxs17("label", { className: "pt-panel-row", style: { marginTop: 12 }, children: [
|
|
3615
|
+
/* @__PURE__ */ jsx19(
|
|
3616
|
+
"input",
|
|
3617
|
+
{
|
|
3618
|
+
type: "checkbox",
|
|
3619
|
+
checked: state.preferences?.updateAsOrder === true,
|
|
3620
|
+
onChange: (event) => setPreference("updateAsOrder", event.target.checked)
|
|
3621
|
+
}
|
|
3622
|
+
),
|
|
3623
|
+
/* @__PURE__ */ jsx19("span", { className: "pt-panel-text", children: ui.updateAsOrder })
|
|
3624
|
+
] }),
|
|
3625
|
+
/* @__PURE__ */ jsx19("p", { className: "pt-marketplace__detail", children: ui.updateAsOrderHow })
|
|
3626
|
+
] }),
|
|
3488
3627
|
fingerprint && /* @__PURE__ */ jsxs17(DetailSection, { title: ui.fingerprint, subtitle: ui.fingerprintSubtitle, children: [
|
|
3489
3628
|
/* @__PURE__ */ jsx19("p", { className: "pt-panel-mono", children: fingerprint.hash }),
|
|
3490
3629
|
/* @__PURE__ */ jsx19("p", { className: "pt-panel-text", style: { marginTop: 8 }, children: ui.trackedFiles(fingerprint.fileCount, fingerprint.version) })
|
package/fingerprint.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paneltir",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"hash": "sha256:
|
|
5
|
-
"fileCount":
|
|
6
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"hash": "sha256:a252ef2a015240e0e8463c0ebb0ef7a8af80ad7ade66f2c48e71657500d506d5",
|
|
5
|
+
"fileCount": 153,
|
|
6
|
+
"generatedAt": "2026-09-07T12:59:14.850Z"
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paneltir",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A React UI kit for admin panels: page frame, a board with touch-ready drag and drop, detail sheets, and a whole panel in one component. Structure and behaviour are fixed; colour and brand belong to each project.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "DFKlabs (https://DFKlabs.com)",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"check:boards": "node scripts/check-boards.mjs",
|
|
65
65
|
"demo:install": "npm --prefix examples/demo ci",
|
|
66
66
|
"demo:build": "npm --prefix examples/demo run build",
|
|
67
|
-
"verify": "npm run typecheck && npm run build && npm run icons:check && npm run seo:check && npm run test:auth && npm run test:marketplace && npm run test:init && npm run test:board && npm run check:boards && npm run check:docs && npm run check:package && npm run fingerprint:check",
|
|
67
|
+
"verify": "npm run typecheck && npm run build && npm run icons:check && npm run seo:check && npm run test:auth && npm run test:marketplace && npm run test:init && npm run test:board && npm run test:updates && npm run check:boards && npm run check:docs && npm run check:package && npm run fingerprint:check",
|
|
68
68
|
"site:install": "npm --prefix site ci",
|
|
69
69
|
"site:dev": "npm --prefix site run dev",
|
|
70
70
|
"site:build": "npm --prefix site run build",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"check:docs": "node scripts/check-docs.mjs",
|
|
73
73
|
"check:package": "node scripts/check-package.mjs",
|
|
74
74
|
"test:init": "node scripts/test-init.mjs",
|
|
75
|
+
"test:updates": "node scripts/test-updates.mjs",
|
|
75
76
|
"test:board": "node scripts/test-board.mjs"
|
|
76
77
|
},
|
|
77
78
|
"peerDependencies": {
|
|
@@ -24,7 +24,98 @@
|
|
|
24
24
|
* PANEL_FILE optional path to the board within it.
|
|
25
25
|
* PANEL_BRANCH optional, defaults to main.
|
|
26
26
|
*/
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The session check, carried here rather than imported.
|
|
29
|
+
*
|
|
30
|
+
* This is the rule `middleware.ts` lives by, and it reached the functions the
|
|
31
|
+
* hard way: every function under `api/` that imported anything answered
|
|
32
|
+
* FUNCTION_INVOCATION_FAILED on every request, while the middleware, which
|
|
33
|
+
* imports nothing, never failed once. A wrapper added to catch and name the
|
|
34
|
+
* fault changed nothing, which is the answer — the module never runs, so there
|
|
35
|
+
* is no handler for a `try` to protect.
|
|
36
|
+
*
|
|
37
|
+
* It is a copy of `lib/session.ts`, and `npm run test:auth` holds every copy to
|
|
38
|
+
* the same answer over the same tokens. Edit this file freely; if you change
|
|
39
|
+
* the check, change it everywhere the test names.
|
|
40
|
+
*/
|
|
41
|
+
const SESSION_COOKIE = 'pt_session'
|
|
42
|
+
const VERSION = 'v1'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads one variable, surviving a runtime with no `process` at all.
|
|
46
|
+
*
|
|
47
|
+
* Written out as literal `process.env.X` accesses because that is the form
|
|
48
|
+
* build tools substitute at build time; a computed key would leave nothing to
|
|
49
|
+
* substitute. A bare access is a ReferenceError where `process` is undefined,
|
|
50
|
+
* which in middleware reads as the whole route crashing rather than as a
|
|
51
|
+
* missing variable — hence the catch.
|
|
52
|
+
*/
|
|
53
|
+
function env(name: 'ADMIN_PASSWORD' | 'SESSION_SECRET'): string | undefined {
|
|
54
|
+
try {
|
|
55
|
+
if (name === 'ADMIN_PASSWORD') return process.env.ADMIN_PASSWORD || undefined
|
|
56
|
+
return process.env.SESSION_SECRET || undefined
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Kept identical to lib/session.ts; test:auth asserts they agree. */
|
|
63
|
+
function signingSecret(): string | null {
|
|
64
|
+
const password = env('ADMIN_PASSWORD')
|
|
65
|
+
if (!password) return null
|
|
66
|
+
return env('SESSION_SECRET') || password
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function safeEqual(a: string, b: string): boolean {
|
|
70
|
+
const encoder = new TextEncoder()
|
|
71
|
+
const left = encoder.encode(a)
|
|
72
|
+
const right = encoder.encode(b)
|
|
73
|
+
let diff = left.length ^ right.length
|
|
74
|
+
const max = Math.max(left.length, right.length)
|
|
75
|
+
for (let i = 0; i < max; i += 1) diff |= (left[i] ?? 0) ^ (right[i] ?? 0)
|
|
76
|
+
return diff === 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function hmac(secret: string, message: string): Promise<string> {
|
|
80
|
+
const encoder = new TextEncoder()
|
|
81
|
+
const key = await crypto.subtle.importKey(
|
|
82
|
+
'raw',
|
|
83
|
+
encoder.encode(secret),
|
|
84
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
85
|
+
false,
|
|
86
|
+
['sign']
|
|
87
|
+
)
|
|
88
|
+
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(message))
|
|
89
|
+
let binary = ''
|
|
90
|
+
for (const byte of new Uint8Array(signature)) binary += String.fromCharCode(byte)
|
|
91
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function verifyToken(token: string | undefined, now = Date.now()): Promise<boolean> {
|
|
95
|
+
const secret = signingSecret()
|
|
96
|
+
if (!secret || !token) return false
|
|
97
|
+
|
|
98
|
+
const parts = token.split('.')
|
|
99
|
+
if (parts.length !== 3) return false
|
|
100
|
+
const [version, expires, signature] = parts
|
|
101
|
+
if (version !== VERSION) return false
|
|
102
|
+
|
|
103
|
+
const expiry = Number(expires)
|
|
104
|
+
if (!Number.isSafeInteger(expiry) || expiry * 1000 <= now) return false
|
|
105
|
+
|
|
106
|
+
return safeEqual(signature, await hmac(secret, `${version}.${expires}`))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readCookie(header: string | null | undefined, name: string): string | undefined {
|
|
110
|
+
if (!header) return undefined
|
|
111
|
+
for (const pair of header.split(';')) {
|
|
112
|
+
const eq = pair.indexOf('=')
|
|
113
|
+
if (eq === -1) continue
|
|
114
|
+
if (pair.slice(0, eq).trim() === name) return pair.slice(eq + 1).trim()
|
|
115
|
+
}
|
|
116
|
+
return undefined
|
|
117
|
+
}
|
|
118
|
+
|
|
28
119
|
|
|
29
120
|
/**
|
|
30
121
|
* Where the board lives in this repository. Set PANEL_FILE to override; the
|
|
@@ -215,7 +306,31 @@ function configuration() {
|
|
|
215
306
|
}
|
|
216
307
|
}
|
|
217
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Nothing here may reach the platform as an uncaught throw.
|
|
311
|
+
*
|
|
312
|
+
* `authorized()` runs before the try that wraps the network calls, so anything
|
|
313
|
+
* it raised left the platform to answer with its own generic 500 and the
|
|
314
|
+
* SHOUTING_CODE FUNCTION_INVOCATION_FAILED — a code that names the shape of
|
|
315
|
+
* the fault and never the fault, which is how three separate fixes were aimed
|
|
316
|
+
* at a cause nobody had seen. It is the rule `middleware.ts` already lives by:
|
|
317
|
+
* it never throws, because a throw is answered by a page that names nothing.
|
|
318
|
+
*
|
|
319
|
+
* The detail is safe to return: this endpoint is behind the session, and the
|
|
320
|
+
* GitHub branch below already answers with `detail` for the same reason.
|
|
321
|
+
*/
|
|
218
322
|
export default async function handler(request: VercelRequest, response: VercelResponse) {
|
|
323
|
+
try {
|
|
324
|
+
return await handleRequest(request, response)
|
|
325
|
+
} catch (error) {
|
|
326
|
+
return response.status(500).json({
|
|
327
|
+
error: 'The panel API failed before it could answer.',
|
|
328
|
+
detail: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function handleRequest(request: VercelRequest, response: VercelResponse) {
|
|
219
334
|
response.setHeader('Cache-Control', 'no-store')
|
|
220
335
|
|
|
221
336
|
if (request.method !== 'POST' && request.method !== 'GET') {
|
|
@@ -248,7 +363,8 @@ export default async function handler(request: VercelRequest, response: VercelRe
|
|
|
248
363
|
if (problem) return response.status(400).json({ error: problem })
|
|
249
364
|
|
|
250
365
|
const content = `${JSON.stringify(payload.state, null, 2)}\n`
|
|
251
|
-
|
|
366
|
+
const bytes = new TextEncoder().encode(content)
|
|
367
|
+
if (bytes.length > MAX_BODY_BYTES) {
|
|
252
368
|
return response.status(413).json({ error: 'The state is too large.' })
|
|
253
369
|
}
|
|
254
370
|
|
|
@@ -280,7 +396,7 @@ export default async function handler(request: VercelRequest, response: VercelRe
|
|
|
280
396
|
headers: { ...githubHeaders, 'content-type': 'application/json' },
|
|
281
397
|
body: JSON.stringify({
|
|
282
398
|
message,
|
|
283
|
-
content:
|
|
399
|
+
content: base64(bytes),
|
|
284
400
|
branch,
|
|
285
401
|
...(sha ? { sha } : {}),
|
|
286
402
|
}),
|
|
@@ -308,6 +424,26 @@ export default async function handler(request: VercelRequest, response: VercelRe
|
|
|
308
424
|
}
|
|
309
425
|
}
|
|
310
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Base64 without `Buffer`.
|
|
429
|
+
*
|
|
430
|
+
* `Buffer` was the only runtime-specific global left on this path, and it sat
|
|
431
|
+
* on the POST side alone — so a function running anywhere but Node answered a
|
|
432
|
+
* save with a platform 500 while every GET on the same file kept working, and
|
|
433
|
+
* the panel could only report the number. `TextEncoder` and `btoa` exist in
|
|
434
|
+
* every runtime this can land in.
|
|
435
|
+
*
|
|
436
|
+
* Chunked because `String.fromCharCode(...bytes)` spreads one argument per
|
|
437
|
+
* byte: a board is far past the call-argument limit and it would throw.
|
|
438
|
+
*/
|
|
439
|
+
function base64(bytes: Uint8Array): string {
|
|
440
|
+
let binary = ''
|
|
441
|
+
for (let i = 0; i < bytes.length; i += 0x8000) {
|
|
442
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000))
|
|
443
|
+
}
|
|
444
|
+
return btoa(binary)
|
|
445
|
+
}
|
|
446
|
+
|
|
311
447
|
function safeParse(value: string): unknown {
|
|
312
448
|
try {
|
|
313
449
|
return JSON.parse(value)
|