paneltir 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.10.0
22
+ # npm install github:daifukus/paneltir#v0.10.1
23
23
  ```
24
24
 
25
25
  (Use the tag you were given; never install without pinning a version.)
@@ -237,7 +237,7 @@ identity** — never another project's.
237
237
 
238
238
  ```bash
239
239
  npx paneltir version # version, fingerprint, where it came from
240
- npx paneltir check 0.10.0 # exits non-zero if that is not what is installed
240
+ npx paneltir check 0.10.1 # exits non-zero if that is not what is installed
241
241
  npx paneltir board # read the board and say what is wrong with it
242
242
  ```
243
243
 
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.10.0
57
+ # npm install github:daifukus/paneltir#v0.10.1
58
58
  ```
59
59
 
60
60
  Installing from Git runs the `prepare` script, which builds `dist/`, so the
package/bin/paneltir.mjs CHANGED
@@ -130,10 +130,35 @@ function writeStamp(target, templateFiles, written) {
130
130
  const files = { ...(previous?.files ?? {}) }
131
131
  for (const file of written) files[file] = sha256(join(target, file))
132
132
 
133
+ /*
134
+ * The headline only advances when this run actually rewrote the whole gate.
135
+ *
136
+ * It used to advance on every run, which quietly defeated the entire point.
137
+ * A project on v0.9.0, a template changed in v0.10.0, `npm update`, then a
138
+ * re-run of `init` — which writes nothing, because the files are already
139
+ * there — and the stamp said the gate came from v0.10.0 while the files on
140
+ * disk were still v0.9.0's. `doctor` then printed "The template has not
141
+ * changed since" directly above a row reading BEHIND, and the panel, which
142
+ * has only this hash to go on, said the gate was current. Reproduced before
143
+ * fixing.
144
+ *
145
+ * The board is excluded because it is never rewritten: requiring it would
146
+ * mean the headline never advanced at all after the first install.
147
+ *
148
+ * When the gate is mixed — some files written, some left — the previous
149
+ * answer stands. That can over-report staleness for a partly updated gate,
150
+ * and it is the safe direction: `doctor`'s per-file rows carry the truth,
151
+ * and over-reporting sends somebody to read a diff while under-reporting
152
+ * sends them nowhere at all.
153
+ */
154
+ const gateFiles = templateFiles.filter((file) => file !== BOARD_SEED)
155
+ const wholeGate = gateFiles.length > 0 && gateFiles.every((file) => written.includes(file))
156
+
133
157
  const stamp = {
134
- version: pkg.version,
135
- templateHash: fingerprint.templateHash ?? '',
136
- copiedAt: new Date().toISOString(),
158
+ version: wholeGate ? pkg.version : (previous?.version ?? pkg.version),
159
+ templateHash: wholeGate ? (fingerprint.templateHash ?? '') : (previous?.templateHash ?? ''),
160
+ // When this gate was last written, not when this command last ran.
161
+ copiedAt: written.length ? new Date().toISOString() : (previous?.copiedAt ?? new Date().toISOString()),
137
162
  files,
138
163
  }
139
164
  try {
@@ -513,9 +538,33 @@ function doctor() {
513
538
  * behalf would be the second thing in this project writing to their
514
539
  * repository.
515
540
  */
541
+ /**
542
+ * One flag, in either form, because the usage line prints the space one.
543
+ *
544
+ * It only read `--cards=a,b`, while its own error message printed
545
+ * `[--cards a,b]` — so the documented form took `a,b` as the board path and
546
+ * failed with "no board at a,b", and `--by you` was dropped silently, which is
547
+ * worse: the run was attributed to Claude when somebody said it was theirs.
548
+ */
549
+ function flag(args, name) {
550
+ const joined = args.find((a) => a.startsWith(`--${name}=`))
551
+ if (joined) return joined.slice(name.length + 3)
552
+ const at = args.indexOf(`--${name}`)
553
+ if (at !== -1 && typeof args[at + 1] === 'string' && !args[at + 1].startsWith('--')) return args[at + 1]
554
+ return undefined
555
+ }
556
+
516
557
  async function run(args) {
517
- const flags = args.filter((a) => a.startsWith('--'))
518
- const plain = args.filter((a) => !a.startsWith('--'))
558
+ // A flag's value is not a positional argument, whichever form it came in.
559
+ const taken = new Set()
560
+ for (const name of ['cards', 'by']) {
561
+ const at = args.indexOf(`--${name}`)
562
+ if (at !== -1) {
563
+ taken.add(at)
564
+ if (typeof args[at + 1] === 'string' && !args[at + 1].startsWith('--')) taken.add(at + 1)
565
+ }
566
+ }
567
+ const plain = args.filter((a, i) => !a.startsWith('--') && !taken.has(i))
519
568
  const summary = plain[0]
520
569
  const file = plain[1] ?? BOARD_SEED
521
570
 
@@ -543,7 +592,7 @@ async function run(args) {
543
592
  }
544
593
 
545
594
  const board = check.board
546
- const cards = (flags.find((f) => f.startsWith('--cards='))?.slice(8) ?? '')
595
+ const cards = (flag(args, 'cards') ?? '')
547
596
  .split(',')
548
597
  .map((id) => id.trim())
549
598
  .filter(Boolean)
@@ -557,10 +606,15 @@ async function run(args) {
557
606
  process.exit(1)
558
607
  }
559
608
 
560
- const by = flags.find((f) => f.startsWith('--by='))?.slice(5) === 'you' ? 'you' : 'claude'
609
+ const by = flag(args, 'by') === 'you' ? 'you' : 'claude'
561
610
  const date = new Date().toISOString().slice(0, 10)
562
611
  board.runs = Array.isArray(board.runs) ? board.runs : []
563
- board.runs.unshift({
612
+ // Appended, not prepended: `runs` is stored oldest-first and every reader
613
+ // reverses it. `unshift` put a new pass at the *oldest* position, so it
614
+ // rendered at the bottom of the revisions log and fell outside the bell's
615
+ // newest-first window — a record of work that read as the first thing ever
616
+ // done here.
617
+ board.runs.push({
564
618
  id: `run-${date}-${String(board.runs.length + 1).padStart(3, '0')}`,
565
619
  date,
566
620
  by,
@@ -569,8 +623,27 @@ async function run(args) {
569
623
  fingerprint: shortHash,
570
624
  })
571
625
 
626
+ /*
627
+ * Bounded here, because here is where it grows.
628
+ *
629
+ * The validators used to refuse a board with more than 200 runs while
630
+ * nothing anywhere trimmed them, so the only reachable outcome was a Save
631
+ * that broke permanently on the two-hundredth pass and said "Too many runs".
632
+ * A bound belongs where the list is appended, not where it is read: trimming
633
+ * on the write path would commit a board shorter than the one the panel is
634
+ * holding, and the panel would read as saved while the repository disagreed.
635
+ *
636
+ * The oldest go, since `runs` is oldest-first. Three hundred is about a
637
+ * decade of daily passes, and the diff of a list that only grows grows with
638
+ * it.
639
+ */
640
+ const KEEP_RUNS = 300
641
+ const dropped = Math.max(0, board.runs.length - KEEP_RUNS)
642
+ if (dropped) board.runs = board.runs.slice(dropped)
643
+
572
644
  writeFileSync(path, JSON.stringify(board, null, 2) + '\n')
573
645
  console.log(`Appended to ${file}: ${summary}`)
646
+ if (dropped) console.log(` dropped the ${dropped} oldest run(s), keeping the last ${KEEP_RUNS}`)
574
647
  console.log(` ${cards.length ? `cards ${cards.join(', ')}` : 'no cards named'} · by ${by} · ${shortHash}`)
575
648
  console.log('\nThe file is changed and not committed. The panel commits it on Save,')
576
649
  console.log('or commit it yourself — this writes nothing to your repository.')
package/dist/index.d.ts CHANGED
@@ -521,6 +521,18 @@ interface SetupRequirement {
521
521
  enables?: React.ReactNode;
522
522
  /** Exactly what to do, shown only when it is missing. */
523
523
  fix?: React.ReactNode;
524
+ /**
525
+ * Information rather than a gate: drawn in the list, never walked as a step.
526
+ *
527
+ * Every other requirement here is an environment variable, and the
528
+ * walkthrough's one action — re-read the environment from the server —
529
+ * is what clears it. A requirement that action cannot clear parks the
530
+ * walkthrough on it for ever, and because `unknown` is judged across the
531
+ * whole list, one advisory row that nobody wired made every step answer a
532
+ * successful check with "the panel could not ask its own server". That is
533
+ * what the copied gate did the day it was added.
534
+ */
535
+ advisory?: boolean;
524
536
  }
525
537
  interface SetupGuideProps {
526
538
  requirements: SetupRequirement[];
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // src/version.ts
2
- var PANELTIR_VERSION = "0.10.0";
3
- var PANELTIR_FINGERPRINT = "sha256:4de7ef7076563e4c3118ac16b645737a44c4c0675115d9a9d7bbff562d5ecb74";
2
+ var PANELTIR_VERSION = "0.10.1";
3
+ var PANELTIR_FINGERPRINT = "sha256:b2d1daf120972ce3bfc54f3a9f09bccaddd6ceb306a65a1682b810012d85f33d";
4
4
  var PANELTIR_FILE_COUNT = 163;
5
- var PANELTIR_TEMPLATE_HASH = "sha256:7f9accd11920a0c189dd683bdb96635a606960afeca71e02c2a26a0257acefd8";
5
+ var PANELTIR_TEMPLATE_HASH = "sha256:034ba27266408af4f2ba5793d9cefbe448b168570d6a0b87dfe698dfe639feb1";
6
6
  var paneltirBuild = {
7
7
  version: PANELTIR_VERSION,
8
8
  fingerprint: PANELTIR_FINGERPRINT,
@@ -918,15 +918,16 @@ import { useState as useState5 } from "react";
918
918
 
919
919
  // src/components/Setup/wizard.ts
920
920
  function wizardStep(requirements) {
921
- const total = requirements.length;
922
- const done = requirements.filter((requirement) => requirement.status === "ok");
923
- const unknown = requirements.some((requirement) => requirement.status === "unknown");
924
- const at = requirements.findIndex((requirement) => requirement.status !== "ok");
921
+ const steps = requirements.filter((requirement) => !requirement.advisory);
922
+ const total = steps.length;
923
+ const done = steps.filter((requirement) => requirement.status === "ok");
924
+ const unknown = steps.some((requirement) => requirement.status === "unknown");
925
+ const at = steps.findIndex((requirement) => requirement.status !== "ok");
925
926
  if (at === -1) {
926
927
  return { current: null, index: 0, total, done, ready: total > 0, unknown: false };
927
928
  }
928
929
  return {
929
- current: requirements[at],
930
+ current: steps[at],
930
931
  index: at + 1,
931
932
  total,
932
933
  done,
@@ -2939,7 +2940,7 @@ function applyMove(cards, move, visible) {
2939
2940
  function resolveTheme(saved, stored, themes, hasImported, fallback) {
2940
2941
  for (const name of [saved, stored]) {
2941
2942
  if (name === "imported" && hasImported) return "imported";
2942
- if (name && name in themes) return name;
2943
+ if (name && Object.prototype.hasOwnProperty.call(themes, name)) return name;
2943
2944
  }
2944
2945
  return fallback ?? Object.keys(themes)[0];
2945
2946
  }
@@ -3108,6 +3109,9 @@ function PanelApp({
3108
3109
  // `SetupWizard` already refuses to make.
3109
3110
  ...gate === void 0 ? [] : [{
3110
3111
  id: "gate",
3112
+ // Information, not a gate: the walkthrough's only action re-reads the
3113
+ // environment, and this is files on disk. See SetupRequirement.
3114
+ advisory: true,
3111
3115
  label: ui.needGate,
3112
3116
  status: gateReading.verdict === "current" ? "ok" : gateReading.verdict === "behind" ? "missing" : "unknown",
3113
3117
  enables: ui.needGateEnables,
package/fingerprint.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "paneltir",
3
- "version": "0.10.0",
4
- "hash": "sha256:4de7ef7076563e4c3118ac16b645737a44c4c0675115d9a9d7bbff562d5ecb74",
3
+ "version": "0.10.1",
4
+ "hash": "sha256:b2d1daf120972ce3bfc54f3a9f09bccaddd6ceb306a65a1682b810012d85f33d",
5
5
  "fileCount": 163,
6
- "templateHash": "sha256:7f9accd11920a0c189dd683bdb96635a606960afeca71e02c2a26a0257acefd8",
7
- "generatedAt": "2026-09-11T02:49:51.562Z"
6
+ "templateHash": "sha256:034ba27266408af4f2ba5793d9cefbe448b168570d6a0b87dfe698dfe639feb1",
7
+ "generatedAt": "2026-09-11T07:08:17.308Z"
8
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paneltir",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
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)",
@@ -99,42 +99,56 @@
99
99
  "node": ">=20"
100
100
  },
101
101
  "paneltirRelease": {
102
- "version": "0.10.0",
102
+ "version": "0.10.1",
103
103
  "date": "2026-09-11",
104
104
  "entries": [
105
105
  {
106
- "kind": "feature",
106
+ "kind": "fix",
107
107
  "text": {
108
- "en": "`paneltir doctor` says whether the files this project copied are still the ones the installed kit ships and separates \"you edited it\" from \"we changed it underneath you\", which a diff cannot.",
109
- "es": "`paneltir doctor` dice si los archivos que copió este proyecto siguen siendo los del kit instalado y separa «lo editaste tú» de «lo cambiamos nosotros debajo», que un diff no puede."
108
+ "en": "Re-running `paneltir init` no longer tells the panel a stale gate is current. It stamped the new template over files it had not written, so a project that updated and re-ran init was told its gate was up to date when it was not.",
109
+ "es": "Re-ejecutar `paneltir init` ya no le dice al panel que una puerta vieja está al día. Sellaba la plantilla nueva sobre archivos que no había escrito, así que un proyecto que actualizaba y re-ejecutaba init recibía un «todo correcto» que no era cierto."
110
110
  }
111
111
  },
112
112
  {
113
- "kind": "feature",
113
+ "kind": "fix",
114
+ "text": {
115
+ "en": "The save endpoint's own board check had drifted to a shallow one that accepted a board with no areas, an unknown intent, an unknown area and any version at all. All three copies of that check are now held to the same verdict.",
116
+ "es": "La comprobación del tablero en el endpoint de guardado había derivado a una superficial que aceptaba un tablero sin áreas, un intent desconocido, un área desconocida y cualquier versión. Las tres copias de esa comprobación se sujetan ahora al mismo veredicto."
117
+ }
118
+ },
119
+ {
120
+ "kind": "fix",
114
121
  "text": {
115
- "en": "The setup list can carry a row for the copied gate. Import `.paneltir/gate.json` and pass it as `gate`; without it the panel says nothing about those files rather than guessing.",
116
- "es": "La lista de configuración puede llevar una fila para la puerta copiada. Importa `.paneltir/gate.json` y pásalo como `gate`; sin él el panel no dice nada de esos archivos en vez de suponer."
122
+ "en": "`paneltir run` appends to the end of the revisions log instead of the front, so a new pass reads as the newest and not the oldest and accepts `--cards a,b` and `--by you`, the spaced form its own usage line prints.",
123
+ "es": "`paneltir run` añade al final del registro de revisiones en vez de al principio, así una pasada nueva se lee como la más reciente y no como la más antigua y acepta `--cards a,b` y `--by you`, la forma con espacio que imprime su propio uso."
117
124
  }
118
125
  },
119
126
  {
120
- "kind": "feature",
127
+ "kind": "fix",
128
+ "text": {
129
+ "en": "A board naming a theme that only exists on Object's prototype — `constructor`, `toString` — no longer resolves as a theme and draws the panel in the browser's default colours.",
130
+ "es": "Un tablero que nombra un tema que sólo existe en el prototipo de Object — `constructor`, `toString` — ya no se resuelve como tema ni dibuja el panel con los colores por defecto del navegador."
131
+ }
132
+ },
133
+ {
134
+ "kind": "fix",
121
135
  "text": {
122
- "en": "`paneltir run \"what this pass did\"` appends to the board's revisions, so the record does not depend on anyone remembering at the end of a session.",
123
- "es": "`paneltir run \"qué hizo esta pasada\"` añade una entrada a las revisiones del tablero, así el registro no depende de que alguien se acuerde al final de la sesión."
136
+ "en": "Saving no longer breaks permanently once a board reaches two hundred revisions. The limit was refused on the write path and applied nowhere; it is bounded where the log is appended instead.",
137
+ "es": "Guardar ya no se rompe para siempre cuando un tablero llega a doscientas revisiones. El límite se rechazaba al escribir y no se aplicaba en ningún sitio; ahora se acota donde se añade al registro."
124
138
  }
125
139
  },
126
140
  {
127
141
  "kind": "fix",
128
142
  "text": {
129
- "en": "`paneltir init` and `paneltir doctor` no longer need React installed. They loaded the component bundle at startup, so they failed in a project that had not installed it yet which is exactly when `init` is run.",
130
- "es": "`paneltir init` y `paneltir doctor` ya no necesitan React instalado. Cargaban el bundle de componentes al arrancar, así que fallaban en un proyecto que aún no lo tenía que es justo cuando se ejecuta `init`."
143
+ "en": "The copied gate's row in the setup list no longer parks the walkthrough on a step its one action cannot clear, nor makes every other step report that the panel could not reach its own server.",
144
+ "es": "La fila de la puerta copiada en la lista de configuración ya no deja el asistente atascado en un paso que su única acción no puede resolver, ni hace que el resto de pasos digan que el panel no pudo alcanzar su propio servidor."
131
145
  }
132
146
  },
133
147
  {
134
148
  "kind": "chore",
135
149
  "text": {
136
- "en": "The rules a project needs are checked to actually reach it: the documents a project keeps must name `extraThemes` and `paneltir doctor`, or the build fails saying why.",
137
- "es": "Se comprueba que las reglas que un proyecto necesita le lleguen de verdad: los documentos que el proyecto conserva tienen que nombrar `extraThemes` y `paneltir doctor`, o la build falla diciendo por qué."
150
+ "en": "Two checks that could stop checking were closed: a travelling document that cannot be read is now a failure rather than a pass, and no gate file may read the environment at module scope — where no error can be caught.",
151
+ "es": "Se cerraron dos comprobaciones que podían dejar de comprobar: un documento que viaja y no se puede leer ahora falla en vez de pasar, y ningún archivo de la puerta puede leer el entorno a nivel de módulo, donde ningún error se puede capturar."
138
152
  }
139
153
  }
140
154
  ]
@@ -121,8 +121,22 @@ function readCookie(header: string | null | undefined, name: string): string | u
121
121
  * Where the board lives in this repository. Set PANEL_FILE to override; the
122
122
  * default is only a guess at a sensible place, and a wrong path here fails
123
123
  * loudly on the first save rather than quietly writing somewhere else.
124
+ *
125
+ * A function, and not a constant, for the reason `env()` above is wrapped: a
126
+ * bare `process.env.X` is a ReferenceError wherever `process` is undefined,
127
+ * and at module scope it runs *before* the handler's `try` — so it cannot be
128
+ * caught and the platform answers `FUNCTION_INVOCATION_FAILED`, a code that
129
+ * names the shape of the fault and never the fault. That is the exact failure
130
+ * this file has already had twice, from an import and then from a throw ahead
131
+ * of the wrapper. This was the last module-scope read left in any gate file.
124
132
  */
125
- const FILE_PATH = process.env.PANEL_FILE || 'src/data/panel-state.json'
133
+ function filePath(): string {
134
+ try {
135
+ return process.env.PANEL_FILE || 'src/data/panel-state.json'
136
+ } catch {
137
+ return 'src/data/panel-state.json'
138
+ }
139
+ }
126
140
  const MAX_BODY_BYTES = 512 * 1024
127
141
 
128
142
  interface VercelRequest {
@@ -277,6 +291,12 @@ function invalidState(state: unknown): string | null {
277
291
  }
278
292
 
279
293
  if (!Array.isArray(state.runs)) say('runs', 'must be an array')
294
+ // Deliberately no cap. The shape is checked here; how long a log may get is
295
+ // not a shape, and refusing a board for having too much history is a panel
296
+ // that stops saving one day and never says why. This copy used to refuse
297
+ // over 200 runs while nothing anywhere trimmed them, so the only outcome was
298
+ // a Save that broke permanently on the two-hundredth pass. Bounded on write
299
+ // instead, where a bound can be applied rather than only enforced.
280
300
 
281
301
  return problems.length ? problems.join('; ') : null
282
302
  }
@@ -302,7 +322,7 @@ function configuration() {
302
322
  // is how a misconfigured target gets noticed before it is written to.
303
323
  repo: repo ?? null,
304
324
  branch: process.env.PANEL_BRANCH || 'main',
305
- file: FILE_PATH,
325
+ file: filePath(),
306
326
  }
307
327
  }
308
328
 
@@ -373,7 +393,7 @@ async function handleRequest(request: VercelRequest, response: VercelResponse) {
373
393
  ? payload.message.trim().slice(0, 120)
374
394
  : 'chore(panel): update the board from the panel'
375
395
 
376
- const api = `https://api.github.com/repos/${repo}/contents/${FILE_PATH}`
396
+ const api = `https://api.github.com/repos/${repo}/contents/${filePath()}`
377
397
  const githubHeaders = {
378
398
  authorization: `Bearer ${token}`,
379
399
  accept: 'application/vnd.github+json',