paneltir 0.9.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/CLAUDE_MD_SNIPPET.md +14 -0
- package/INSTALL.md +35 -2
- package/README.md +1 -1
- package/bin/paneltir.mjs +354 -5
- package/dist/index.d.ts +132 -2
- package/dist/index.js +70 -11
- package/fingerprint.json +5 -4
- package/package.json +36 -14
- package/template/.claude/skills/panel/SKILL.md +29 -0
- package/template/api/panel-state.ts +23 -3
package/CLAUDE_MD_SNIPPET.md
CHANGED
|
@@ -31,6 +31,13 @@ library shared with other projects. Rules:
|
|
|
31
31
|
src/dashboard-theme.ts -->. Colour changes go there, never in the kit. The
|
|
32
32
|
dashboard's **shape** is a `DashboardThemeForm` passed to the same provider
|
|
33
33
|
(`panel` or `ledger`); it too is this project's choice, not the kit's.
|
|
34
|
+
- **This project's theme goes to `PanelApp` as `extraThemes`, never as
|
|
35
|
+
`themes`.** `extraThemes` offers it alongside the four the kit ships;
|
|
36
|
+
`themes` means "exactly these and nothing else", so passing one theme there
|
|
37
|
+
deletes all four and the settings sheet ends up with a single row. That is
|
|
38
|
+
not hypothetical — it happened to two projects before the distinction
|
|
39
|
+
existed, and it looks like the kit having no themes rather than like a prop
|
|
40
|
+
being wrong.
|
|
34
41
|
- **The panel's gate is this project's, not the library's.** `middleware.ts`,
|
|
35
42
|
`lib/session.ts` and `api/login.ts` / `api/logout.ts` / `api/panel-state.ts`
|
|
36
43
|
were copied here by `npx paneltir init`. They hold this project's password
|
|
@@ -38,6 +45,13 @@ library shared with other projects. Rules:
|
|
|
38
45
|
change when the kit updates, and `paneltir init` will not overwrite them.
|
|
39
46
|
The password, the GitHub token and the target repository are environment
|
|
40
47
|
variables where this project is hosted — never in this repository.
|
|
48
|
+
- **Those copied files do not update, so they have to be asked.** Run
|
|
49
|
+
`npx paneltir doctor` when the panel misbehaves for no visible reason: it
|
|
50
|
+
compares each copied file against the version installed now and separates
|
|
51
|
+
the three answers that need different actions — you edited it, the template
|
|
52
|
+
moved underneath you, or both. The second is the one no `git diff` can find,
|
|
53
|
+
because there is nothing in this project's history to diff against, and it is
|
|
54
|
+
how a fix released upstream stayed out of a project for two releases.
|
|
41
55
|
- **The panel explains itself once.** New readers get a short guided tour the
|
|
42
56
|
first time they open the board; each note is dismissed and stays dismissed.
|
|
43
57
|
**Help** in the header reports what the panel needs — the password, the
|
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.10.1
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
(Use the tag you were given; never install without pinning a version.)
|
|
@@ -165,6 +165,7 @@ identity** — never another project's.
|
|
|
165
165
|
| `extraThemes` | This project's own identities, offered **as well as** the kit's presets. Theirs come first, so the panel opens in one of them. This is the one you want. |
|
|
166
166
|
| `themes` | The exact set: these and nothing else, presets included. Only for a caller deliberately curating the list — it replaces rather than adds. |
|
|
167
167
|
| `defaultTheme` | Which one to draw before the board has said. Defaults to the first key. |
|
|
168
|
+
| `gate` | `.paneltir/gate.json` from this project, imported. With it the setup list gains a row saying whether the files `init` copied are still the ones this version ships; without it the panel says nothing about them rather than guessing. |
|
|
168
169
|
| `brand` | What sits where a name goes in the header. Defaults to the board's `project`. |
|
|
169
170
|
| `capabilities`, `extras` | Sections of the caller's own. Not drawn when absent. |
|
|
170
171
|
| `links`, `footer`, `decoration`, `className` | Where else to go, what sits under the panel, and anything the caller draws behind it. |
|
|
@@ -192,6 +193,38 @@ identity** — never another project's.
|
|
|
192
193
|
there. On a board already in use the same walkthrough is behind **Help**,
|
|
193
194
|
offered only while something is actually missing.
|
|
194
195
|
|
|
196
|
+
**The files `init` copied do not update, so ask them.** Everything under
|
|
197
|
+
`api/`, `lib/` and `middleware.ts` is this project's from the moment it is
|
|
198
|
+
copied — that is the point, since it holds this project's password rule —
|
|
199
|
+
and the cost is that a fix released upstream can be missing here while every
|
|
200
|
+
version number on screen looks current. It has happened: the `HTTP 500` on
|
|
201
|
+
saving was fixed in `template/api/panel-state.ts`, and projects installed
|
|
202
|
+
before that still had it.
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
npx paneltir doctor
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Commit `.paneltir/gate.json`.** `init` writes it to record what it copied
|
|
209
|
+
and what it copied it from, and it is provenance rather than a build
|
|
210
|
+
artifact: ignored by git it disappears on the next clone and every answer
|
|
211
|
+
below degrades to "differs", which is the one that cannot say who moved.
|
|
212
|
+
|
|
213
|
+
It separates the three answers that need different actions — *yours* (you
|
|
214
|
+
edited it), *BEHIND* (you did not touch it and the template moved), *BOTH* —
|
|
215
|
+
and the middle one is invisible to `git diff`, because this project's history
|
|
216
|
+
has nothing to compare against. `init --force` replaces the gate and never
|
|
217
|
+
the board; read the diff first where a file is marked BOTH, or the edit is
|
|
218
|
+
lost.
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
npx paneltir run "what this pass did" --cards=a,b
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
appends an entry to the board's `runs` so the record does not depend on
|
|
225
|
+
anyone remembering at the end of a session. It writes the file and commits
|
|
226
|
+
nothing.
|
|
227
|
+
|
|
195
228
|
Two things to check before trusting it: `/admin` signed out must land on
|
|
196
229
|
`/login`, and **the panel's own JavaScript bundle must answer 401 signed
|
|
197
230
|
out**. If it does not, point the last entry of the matcher in
|
|
@@ -204,7 +237,7 @@ identity** — never another project's.
|
|
|
204
237
|
|
|
205
238
|
```bash
|
|
206
239
|
npx paneltir version # version, fingerprint, where it came from
|
|
207
|
-
npx paneltir check 0.
|
|
240
|
+
npx paneltir check 0.10.1 # exits non-zero if that is not what is installed
|
|
208
241
|
npx paneltir board # read the board and say what is wrong with it
|
|
209
242
|
```
|
|
210
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.
|
|
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
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* to the project: readable, editable, in its own git history. Re-running never
|
|
15
15
|
* overwrites what is already there.
|
|
16
16
|
*/
|
|
17
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
17
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { createHash } from 'node:crypto'
|
|
18
19
|
import { dirname, join, relative, resolve } from 'node:path'
|
|
19
20
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
20
21
|
|
|
@@ -110,6 +111,68 @@ function survey(target) {
|
|
|
110
111
|
return found
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Records the provenance of the gate now in the project.
|
|
116
|
+
*
|
|
117
|
+
* Merged rather than replaced: re-running `init` writes only the files that
|
|
118
|
+
* were missing, so a stamp that dropped the rest would forget where the
|
|
119
|
+
* majority of the gate came from. Only files this run actually wrote are
|
|
120
|
+
* re-stamped — for one it left alone we did not write it and cannot claim to
|
|
121
|
+
* know its origin, so the previous answer stands or there is none.
|
|
122
|
+
*
|
|
123
|
+
* The board is stamped like the rest and then deliberately ignored by
|
|
124
|
+
* `doctor`: it is meant to diverge from the seed on the first card, and a
|
|
125
|
+
* report that called that drift would be noise on every project that is
|
|
126
|
+
* working.
|
|
127
|
+
*/
|
|
128
|
+
function writeStamp(target, templateFiles, written) {
|
|
129
|
+
const previous = readStamp(target)
|
|
130
|
+
const files = { ...(previous?.files ?? {}) }
|
|
131
|
+
for (const file of written) files[file] = sha256(join(target, file))
|
|
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
|
+
|
|
157
|
+
const stamp = {
|
|
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()),
|
|
162
|
+
files,
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
mkdirSync(dirname(join(target, STAMP)), { recursive: true })
|
|
166
|
+
writeFileSync(join(target, STAMP), JSON.stringify(stamp, null, 2) + '\n')
|
|
167
|
+
} catch (error) {
|
|
168
|
+
// Never fatal. The gate is copied and working; the stamp only makes a
|
|
169
|
+
// later report sharper, and failing the install over bookkeeping would
|
|
170
|
+
// trade something that matters for something that does not.
|
|
171
|
+
console.error(`\npaneltir: could not write ${STAMP} (${error.message}).`)
|
|
172
|
+
console.error('The gate is in place; `paneltir doctor` will have less to go on.')
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
113
176
|
function reportSurvey(found) {
|
|
114
177
|
const lines = []
|
|
115
178
|
|
|
@@ -149,6 +212,36 @@ Remove it once the board is moved over, or the project ships both.`)
|
|
|
149
212
|
/** The board. Never replaced once it exists — see the loop below. */
|
|
150
213
|
const BOARD_SEED = 'src/data/panel-state.json'
|
|
151
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Where `init` records what it copied and what it copied it from.
|
|
217
|
+
*
|
|
218
|
+
* The gate is the one part of this package that leaves as files a project
|
|
219
|
+
* keeps, and a kept file can never be told it is old. That has been paid for
|
|
220
|
+
* twice: the `HTTP 500` fix landed in `template/api/panel-state.ts`, so every
|
|
221
|
+
* project installed before it still has a save that cannot work, and the skill
|
|
222
|
+
* file is copied the same way, so a rule learned upstream never reaches the
|
|
223
|
+
* Claude that reads it.
|
|
224
|
+
*
|
|
225
|
+
* A plain diff cannot close that, because it cannot tell "you edited this"
|
|
226
|
+
* from "we changed this underneath you" — and those need opposite actions.
|
|
227
|
+
* Hashing each file as written is what separates them.
|
|
228
|
+
*/
|
|
229
|
+
const STAMP = '.paneltir/gate.json'
|
|
230
|
+
|
|
231
|
+
function sha256(path) {
|
|
232
|
+
return createHash('sha256').update(readFileSync(path)).digest('hex')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function readStamp(target) {
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(readFileSync(join(target, STAMP), 'utf8'))
|
|
238
|
+
} catch {
|
|
239
|
+
// Absent or unreadable is not a fault: every project installed before
|
|
240
|
+
// stamping existed is here, and `doctor` still compares the files.
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
152
245
|
function init(args) {
|
|
153
246
|
const force = args.includes('--force')
|
|
154
247
|
const target = process.cwd()
|
|
@@ -180,6 +273,8 @@ function init(args) {
|
|
|
180
273
|
written.push(file)
|
|
181
274
|
}
|
|
182
275
|
|
|
276
|
+
writeStamp(target, files, written);
|
|
277
|
+
|
|
183
278
|
console.log(`paneltir ${pkg.version} — the panel's gate, into ${target}\n`)
|
|
184
279
|
|
|
185
280
|
const notes = reportSurvey(survey(target))
|
|
@@ -299,7 +394,7 @@ function check(args) {
|
|
|
299
394
|
* neither helps the person who has just edited the file by hand and wants to
|
|
300
395
|
* know before they push. This is that answer, from the same check.
|
|
301
396
|
*/
|
|
302
|
-
function board(args) {
|
|
397
|
+
async function board(args) {
|
|
303
398
|
const file = args.find((a) => !a.startsWith('-')) || process.env.PANEL_FILE || 'src/data/panel-state.json'
|
|
304
399
|
const path = resolve(process.cwd(), file)
|
|
305
400
|
|
|
@@ -309,7 +404,7 @@ function board(args) {
|
|
|
309
404
|
process.exit(1)
|
|
310
405
|
}
|
|
311
406
|
|
|
312
|
-
const check =
|
|
407
|
+
const check = (await boardReader())(readFileSync(path, 'utf8'))
|
|
313
408
|
if (check.ok) {
|
|
314
409
|
const cards = check.board.cards.length
|
|
315
410
|
console.log(`ok ${file} — ${cards} card${cards === 1 ? '' : 's'}, ${check.board.columns.length} columns`)
|
|
@@ -328,7 +423,253 @@ function board(args) {
|
|
|
328
423
|
process.exit(1)
|
|
329
424
|
}
|
|
330
425
|
|
|
331
|
-
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Says whether the files this project copied are still the files it shipped.
|
|
429
|
+
*
|
|
430
|
+
* The three answers it exists to tell apart:
|
|
431
|
+
*
|
|
432
|
+
* - **yours** — you edited it. Nothing to do; the gate is meant to be edited,
|
|
433
|
+
* and this is only here so the next line is believable.
|
|
434
|
+
* - **behind** — you did not touch it and the template moved. This is the one
|
|
435
|
+
* that has cost real money twice, and the one a bare `git diff` cannot see
|
|
436
|
+
* at all, because there is nothing in the project's history to diff
|
|
437
|
+
* against.
|
|
438
|
+
* - **both** — you edited it *and* the template moved. The only case that
|
|
439
|
+
* needs a person: `--force` would throw your edit away.
|
|
440
|
+
*
|
|
441
|
+
* It reports and never repairs, for the same reason the panel records a request
|
|
442
|
+
* instead of running it: replacing somebody's front door is a decision, and
|
|
443
|
+
* `init --force` is where a person makes it having read what changed.
|
|
444
|
+
*/
|
|
445
|
+
function doctor() {
|
|
446
|
+
const target = process.cwd()
|
|
447
|
+
const templateDir = join(packageRoot, 'template')
|
|
448
|
+
if (!existsSync(templateDir)) {
|
|
449
|
+
console.error('paneltir: this install has no template/ directory.')
|
|
450
|
+
process.exit(1)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const stamp = readStamp(target)
|
|
454
|
+
const files = walk(templateDir).sort()
|
|
455
|
+
|
|
456
|
+
console.log(`paneltir ${pkg.version} — the gate in ${target}\n`)
|
|
457
|
+
|
|
458
|
+
if (!stamp) {
|
|
459
|
+
console.log(`No ${STAMP}, so this gate was copied before stamping existed.`)
|
|
460
|
+
console.log('Files can still be compared; "you edited it" and "we changed it"')
|
|
461
|
+
console.log('cannot be told apart until the next `init` writes one.\n')
|
|
462
|
+
} else {
|
|
463
|
+
const behind = stamp.templateHash !== (fingerprint.templateHash ?? '')
|
|
464
|
+
console.log(`Copied from v${stamp.version} on ${String(stamp.copiedAt).slice(0, 10)}`)
|
|
465
|
+
console.log(behind ? 'The template has changed since.\n' : 'The template has not changed since.\n')
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const rows = []
|
|
469
|
+
for (const file of files) {
|
|
470
|
+
// The board is meant to diverge: it stops being a template on the first
|
|
471
|
+
// card. Calling that drift would put a warning on every working project.
|
|
472
|
+
if (file === BOARD_SEED) {
|
|
473
|
+
rows.push([file, existsSync(join(target, file)) ? 'yours' : 'missing', 'the board — never compared'])
|
|
474
|
+
continue
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const here = join(target, file)
|
|
478
|
+
if (!existsSync(here)) {
|
|
479
|
+
rows.push([file, 'missing', 'never copied; `paneltir init` writes it'])
|
|
480
|
+
continue
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const mine = sha256(here)
|
|
484
|
+
const theirs = sha256(join(templateDir, file))
|
|
485
|
+
const stamped = stamp?.files?.[file]
|
|
486
|
+
|
|
487
|
+
if (mine === theirs) {
|
|
488
|
+
rows.push([file, 'current', ''])
|
|
489
|
+
} else if (!stamped) {
|
|
490
|
+
rows.push([file, 'differs', 'no stamp, so who moved cannot be said'])
|
|
491
|
+
} else if (mine === stamped) {
|
|
492
|
+
rows.push([file, 'BEHIND', 'you did not touch it; the template moved'])
|
|
493
|
+
} else if (stamped === theirs) {
|
|
494
|
+
rows.push([file, 'yours', 'you edited it; the template has not moved'])
|
|
495
|
+
} else {
|
|
496
|
+
rows.push([file, 'BOTH', 'you edited it AND the template moved — read both'])
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const width = Math.max(...rows.map(([file]) => file.length))
|
|
501
|
+
for (const [file, state, note] of rows) {
|
|
502
|
+
console.log(` ${state.padEnd(8)} ${file.padEnd(width)}${note ? ` ${note}` : ''}`)
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const stale = rows.filter(([, state]) => state === 'BEHIND' || state === 'BOTH')
|
|
506
|
+
const missing = rows.filter(([, state]) => state === 'missing')
|
|
507
|
+
|
|
508
|
+
if (!stale.length && !missing.length) {
|
|
509
|
+
console.log('\nNothing to do.')
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (stale.length) {
|
|
514
|
+
console.log(`\n${stale.length} file(s) are older than the package they came from.`)
|
|
515
|
+
console.log('See what changed, then decide:')
|
|
516
|
+
console.log(` diff -u <file> node_modules/paneltir/template/<file>`)
|
|
517
|
+
console.log('`paneltir init --force` replaces the gate and never the board.')
|
|
518
|
+
console.log('Anything marked BOTH would lose your edit — read that diff first.')
|
|
519
|
+
}
|
|
520
|
+
if (missing.length) {
|
|
521
|
+
console.log(`\n${missing.length} file(s) were never copied. \`paneltir init\` writes them.`)
|
|
522
|
+
}
|
|
523
|
+
// A gate that cannot work is a failure, not a remark: this runs in CI for
|
|
524
|
+
// some projects, and an exit code is the only part of it a script reads.
|
|
525
|
+
process.exit(1)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Appends a run to the board, so the record does not depend on remembering.
|
|
530
|
+
*
|
|
531
|
+
* `runs` is what each pass actually did, and until now it was written by hand
|
|
532
|
+
* — by Claude, into JSON, at the end of a session, from memory. That is the
|
|
533
|
+
* shape of record that is complete right up until the session that mattered.
|
|
534
|
+
* A command can be called the moment the work lands.
|
|
535
|
+
*
|
|
536
|
+
* It writes the file and nothing else: the board is committed by the panel's
|
|
537
|
+
* Save or by the project's own commit, and a CLI that pushed on somebody's
|
|
538
|
+
* behalf would be the second thing in this project writing to their
|
|
539
|
+
* repository.
|
|
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
|
+
|
|
557
|
+
async function run(args) {
|
|
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))
|
|
568
|
+
const summary = plain[0]
|
|
569
|
+
const file = plain[1] ?? BOARD_SEED
|
|
570
|
+
|
|
571
|
+
if (!summary) {
|
|
572
|
+
console.error('paneltir run "what this pass did" [--cards a,b] [--by you] [board.json]')
|
|
573
|
+
process.exit(1)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const path = resolve(process.cwd(), file)
|
|
577
|
+
if (!existsSync(path)) {
|
|
578
|
+
console.error(`paneltir: no board at ${file}`)
|
|
579
|
+
process.exit(1)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const text = readFileSync(path, 'utf8')
|
|
583
|
+
const check = (await boardReader())(text)
|
|
584
|
+
if (!check.ok) {
|
|
585
|
+
// Refused before writing, not after: appending to a board that is already
|
|
586
|
+
// broken buries the fault under a change nobody asked for.
|
|
587
|
+
console.error(`paneltir: ${file} is not a board this kit can read, so nothing was appended.\n`)
|
|
588
|
+
for (const problem of check.problems) {
|
|
589
|
+
console.error(problem.at ? ` ${problem.at}\n ${problem.says}` : ` ${problem.says}`)
|
|
590
|
+
}
|
|
591
|
+
process.exit(1)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const board = check.board
|
|
595
|
+
const cards = (flag(args, 'cards') ?? '')
|
|
596
|
+
.split(',')
|
|
597
|
+
.map((id) => id.trim())
|
|
598
|
+
.filter(Boolean)
|
|
599
|
+
|
|
600
|
+
// A run naming a card the board does not have is a claim that cannot be
|
|
601
|
+
// checked, which is the only thing `cards` was for.
|
|
602
|
+
const known = new Set(board.cards.map((card) => card.id))
|
|
603
|
+
const unknown = cards.filter((id) => !known.has(id))
|
|
604
|
+
if (unknown.length) {
|
|
605
|
+
console.error(`paneltir: ${file} has no card called ${unknown.join(', ')}.`)
|
|
606
|
+
process.exit(1)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const by = flag(args, 'by') === 'you' ? 'you' : 'claude'
|
|
610
|
+
const date = new Date().toISOString().slice(0, 10)
|
|
611
|
+
board.runs = Array.isArray(board.runs) ? board.runs : []
|
|
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({
|
|
618
|
+
id: `run-${date}-${String(board.runs.length + 1).padStart(3, '0')}`,
|
|
619
|
+
date,
|
|
620
|
+
by,
|
|
621
|
+
summary,
|
|
622
|
+
cards,
|
|
623
|
+
fingerprint: shortHash,
|
|
624
|
+
})
|
|
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
|
+
|
|
644
|
+
writeFileSync(path, JSON.stringify(board, null, 2) + '\n')
|
|
645
|
+
console.log(`Appended to ${file}: ${summary}`)
|
|
646
|
+
if (dropped) console.log(` dropped the ${dropped} oldest run(s), keeping the last ${KEEP_RUNS}`)
|
|
647
|
+
console.log(` ${cards.length ? `cards ${cards.join(', ')}` : 'no cards named'} · by ${by} · ${shortHash}`)
|
|
648
|
+
console.log('\nThe file is changed and not committed. The panel commits it on Save,')
|
|
649
|
+
console.log('or commit it yourself — this writes nothing to your repository.')
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* The board reader, loaded only by the commands that read a board.
|
|
654
|
+
*
|
|
655
|
+
* It used to be imported at module scope, which meant every command pulled in
|
|
656
|
+
* `dist/index.js` — and that bundle imports React, because it is a component
|
|
657
|
+
* library. So `paneltir init`, whose entire job is to copy seven files, and
|
|
658
|
+
* `paneltir doctor`, whose job is to hash them, both failed outright in a
|
|
659
|
+
* project that had not installed React yet. Which is exactly when somebody runs
|
|
660
|
+
* `init`.
|
|
661
|
+
*
|
|
662
|
+
* It is the same rule as the gate's, one layer up: a thing whose job is files
|
|
663
|
+
* does not carry a component library to do it. Here the fix is smaller than
|
|
664
|
+
* duplicating anything — the import just moves to where it is used.
|
|
665
|
+
*/
|
|
666
|
+
let readBoard
|
|
667
|
+
async function boardReader() {
|
|
668
|
+
if (!readBoard) {
|
|
669
|
+
;({ readBoard } = await import(pathToFileURL(join(here, '../dist/index.js')).href))
|
|
670
|
+
}
|
|
671
|
+
return readBoard
|
|
672
|
+
}
|
|
332
673
|
|
|
333
674
|
const [command, ...rest] = process.argv.slice(2)
|
|
334
675
|
|
|
@@ -343,7 +684,13 @@ switch (command) {
|
|
|
343
684
|
check(rest)
|
|
344
685
|
break
|
|
345
686
|
case 'board':
|
|
346
|
-
board(rest)
|
|
687
|
+
await board(rest)
|
|
688
|
+
break
|
|
689
|
+
case 'doctor':
|
|
690
|
+
doctor()
|
|
691
|
+
break
|
|
692
|
+
case 'run':
|
|
693
|
+
await run(rest)
|
|
347
694
|
break
|
|
348
695
|
default:
|
|
349
696
|
console.log(`paneltir v${pkg.version} · ${shortHash}
|
|
@@ -352,6 +699,8 @@ switch (command) {
|
|
|
352
699
|
paneltir version what is installed, and where it came from
|
|
353
700
|
paneltir check [version] fail if the installed version is not the one meant
|
|
354
701
|
paneltir board [file] read this project's board and say what is wrong
|
|
702
|
+
paneltir doctor is the gate you copied still the gate we ship?
|
|
703
|
+
paneltir run "summary" append what this pass did to the board's runs
|
|
355
704
|
|
|
356
705
|
The components install as a library and are never edited. The gate — password,
|
|
357
706
|
session, write-back — is copied in by init and belongs to the project.`)
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,17 @@ declare const PANELTIR_VERSION: string;
|
|
|
15
15
|
declare const PANELTIR_FINGERPRINT: string;
|
|
16
16
|
/** How many files that fingerprint covers. */
|
|
17
17
|
declare const PANELTIR_FILE_COUNT: number;
|
|
18
|
+
/**
|
|
19
|
+
* SHA-256 over `template/` alone — the gate, the seed board and the skill file.
|
|
20
|
+
*
|
|
21
|
+
* Separate from the fingerprint above because it answers a different question.
|
|
22
|
+
* The fingerprint says which build this is; this says whether the files a
|
|
23
|
+
* project *copied out of* the package are the ones it ships now. Most releases
|
|
24
|
+
* do not touch `template/`, so those two move at different rates, and a gate
|
|
25
|
+
* checked against the version number would be reported stale on every release
|
|
26
|
+
* and believed on none.
|
|
27
|
+
*/
|
|
28
|
+
declare const PANELTIR_TEMPLATE_HASH: string;
|
|
18
29
|
/**
|
|
19
30
|
* Everything needed to report the kit in a panel's own footer or settings
|
|
20
31
|
* sheet, and to check it against the released version without opening
|
|
@@ -24,6 +35,8 @@ declare const paneltirBuild: {
|
|
|
24
35
|
readonly version: string;
|
|
25
36
|
readonly fingerprint: string;
|
|
26
37
|
readonly fileCount: number;
|
|
38
|
+
/** What `init` stamps, and what a gate is compared against. */
|
|
39
|
+
readonly templateHash: string;
|
|
27
40
|
/** The tag this build came from, ready to compare or to link. */
|
|
28
41
|
readonly tag: `v${string}`;
|
|
29
42
|
/** Short form, for a footer that has one line to spare. */
|
|
@@ -508,6 +521,18 @@ interface SetupRequirement {
|
|
|
508
521
|
enables?: React.ReactNode;
|
|
509
522
|
/** Exactly what to do, shown only when it is missing. */
|
|
510
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;
|
|
511
536
|
}
|
|
512
537
|
interface SetupGuideProps {
|
|
513
538
|
requirements: SetupRequirement[];
|
|
@@ -1361,6 +1386,9 @@ interface UiStrings {
|
|
|
1361
1386
|
needTokenEnables: string;
|
|
1362
1387
|
needRepo: string;
|
|
1363
1388
|
needRepoEnables: string;
|
|
1389
|
+
needGate: string;
|
|
1390
|
+
needGateEnables: string;
|
|
1391
|
+
fixGate: string;
|
|
1364
1392
|
fixTokenCreate: string;
|
|
1365
1393
|
fixTokenScope: string;
|
|
1366
1394
|
fixTokenPaste: string;
|
|
@@ -1591,6 +1619,21 @@ interface PanelAppProps {
|
|
|
1591
1619
|
* collides with a preset replaces it, because the caller named it.
|
|
1592
1620
|
*/
|
|
1593
1621
|
extraThemes?: PanelThemes;
|
|
1622
|
+
/**
|
|
1623
|
+
* `.paneltir/gate.json` from this project, if the caller imports it.
|
|
1624
|
+
*
|
|
1625
|
+
* The gate is copied once and then belongs to the project, which means it is
|
|
1626
|
+
* the one part of this package that can never be told it is old — and that
|
|
1627
|
+
* has cost two releases already. Handing the stamp in lets the panel say so,
|
|
1628
|
+
* with no filesystem and no network, because both halves of the comparison
|
|
1629
|
+
* are already data: the stamp from the project and `templateHash` from the
|
|
1630
|
+
* installed package.
|
|
1631
|
+
*
|
|
1632
|
+
* Omitted, the panel says nothing about the gate rather than guessing. This
|
|
1633
|
+
* is a prop and not a fetch for the reason the Tools screen is a prop in the
|
|
1634
|
+
* demo: the kit does not decide what a caller's server can be asked.
|
|
1635
|
+
*/
|
|
1636
|
+
gate?: unknown;
|
|
1594
1637
|
/** Which one to draw before the board has said. Defaults to the first. */
|
|
1595
1638
|
defaultTheme?: string;
|
|
1596
1639
|
/**
|
|
@@ -1657,7 +1700,94 @@ interface PanelAppProps {
|
|
|
1657
1700
|
fileCount: number;
|
|
1658
1701
|
};
|
|
1659
1702
|
}
|
|
1660
|
-
declare function PanelApp({ state: initialState, themes: exactThemes, extraThemes, defaultTheme, brand, decoration, className, footer, links, capabilities, extras, showLanguage, showHistory, checkForUpdates, saveEndpoint, marketplaceEndpoint, marketplaceReport, fingerprint, }: PanelAppProps): React.JSX.Element;
|
|
1703
|
+
declare function PanelApp({ state: initialState, themes: exactThemes, extraThemes, gate, defaultTheme, brand, decoration, className, footer, links, capabilities, extras, showLanguage, showHistory, checkForUpdates, saveEndpoint, marketplaceEndpoint, marketplaceReport, fingerprint, }: PanelAppProps): React.JSX.Element;
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* Whether the gate a project is running is the gate this version ships.
|
|
1707
|
+
*
|
|
1708
|
+
* `template/` is the one thing here that leaves as *files a project keeps*.
|
|
1709
|
+
* The components arrive in `node_modules` and are replaced on every update;
|
|
1710
|
+
* the gate is copied once by `init` and is then that project's own — which is
|
|
1711
|
+
* the point, since it holds their password rule and points at their
|
|
1712
|
+
* repository, and a front door that moves when a library updates is not a
|
|
1713
|
+
* front door.
|
|
1714
|
+
*
|
|
1715
|
+
* The cost of that is a copy that can never be told it is old. It has already
|
|
1716
|
+
* been paid twice. The `HTTP 500` fix landed in `template/api/panel-state.ts`,
|
|
1717
|
+
* so every project installed before it still has a save that cannot work, and
|
|
1718
|
+
* nothing anywhere says so; `.claude/skills/panel/SKILL.md` is copied the same
|
|
1719
|
+
* way, so a rule learned upstream never reaches the Claude that reads it.
|
|
1720
|
+
* Upstream the copy is held honest by `test:auth` and `test:board`. Downstream
|
|
1721
|
+
* there was nothing at all.
|
|
1722
|
+
*
|
|
1723
|
+
* So `init` records what it copied and what it copied it from, and this says
|
|
1724
|
+
* what that record means. Two rules shape it:
|
|
1725
|
+
*
|
|
1726
|
+
* - **It compares hashes, not version numbers.** Most releases do not touch
|
|
1727
|
+
* `template/`, so "you are on 0.6.0 and 0.9.0 exists" would cry wolf for
|
|
1728
|
+
* every release and be ignored by the one that mattered. `templateHash`
|
|
1729
|
+
* ships inside `fingerprint.json`, so the comparison is exact.
|
|
1730
|
+
* - **It needs no filesystem and no network**, because the panel runs in a
|
|
1731
|
+
* browser and has neither. Everything it judges is data the caller already
|
|
1732
|
+
* holds: the stamp from the project, the hash from the installed package.
|
|
1733
|
+
*
|
|
1734
|
+
* It reports and never repairs. Replacing somebody's gate is `init --force`,
|
|
1735
|
+
* typed by a person who has read what changed.
|
|
1736
|
+
*/
|
|
1737
|
+
/**
|
|
1738
|
+
* What `init` wrote, and what it wrote it from.
|
|
1739
|
+
*
|
|
1740
|
+
* Lives at `.paneltir/gate.json` in the project. It is not a lockfile and
|
|
1741
|
+
* nothing resolves against it — it exists so a later session can tell "you
|
|
1742
|
+
* edited this" from "we changed this underneath you", which is the distinction
|
|
1743
|
+
* a bare diff cannot make.
|
|
1744
|
+
*/
|
|
1745
|
+
interface GateStamp {
|
|
1746
|
+
/** The kit version that copied these files. */
|
|
1747
|
+
version: string;
|
|
1748
|
+
/** `templateHash` from that version's `fingerprint.json`. */
|
|
1749
|
+
templateHash: string;
|
|
1750
|
+
/** When, so a reader can date it against their own git history. */
|
|
1751
|
+
copiedAt: string;
|
|
1752
|
+
/**
|
|
1753
|
+
* SHA-256 of each file as written, keyed by its path in the project.
|
|
1754
|
+
*
|
|
1755
|
+
* This is what separates a file the project edited from one the template
|
|
1756
|
+
* moved under. Absent for a file `init` left alone: we did not write it, so
|
|
1757
|
+
* we cannot claim to know where it came from.
|
|
1758
|
+
*/
|
|
1759
|
+
files: Record<string, string>;
|
|
1760
|
+
}
|
|
1761
|
+
type GateVerdict =
|
|
1762
|
+
/** The gate is the one this version ships. */
|
|
1763
|
+
'current'
|
|
1764
|
+
/** `template/` has changed since this gate was copied. */
|
|
1765
|
+
| 'behind'
|
|
1766
|
+
/**
|
|
1767
|
+
* No stamp. Every project installed before stamping existed is here, and it
|
|
1768
|
+
* is not a fault — it is the absence of an answer, which is why it is not
|
|
1769
|
+
* called one. `paneltir doctor` can still compare the files themselves.
|
|
1770
|
+
*/
|
|
1771
|
+
| 'unstamped';
|
|
1772
|
+
interface GateReading {
|
|
1773
|
+
verdict: GateVerdict;
|
|
1774
|
+
/** The version that copied the gate, when a stamp says so. */
|
|
1775
|
+
copiedFrom?: string;
|
|
1776
|
+
/** The version running now. */
|
|
1777
|
+
installed: string;
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Reads a stamp against the installed package.
|
|
1781
|
+
*
|
|
1782
|
+
* Deliberately total: a malformed or absent stamp answers `unstamped` rather
|
|
1783
|
+
* than throwing. This is called while a panel is drawing, and a panel that
|
|
1784
|
+
* fails to open because it could not classify its own gate would be a worse
|
|
1785
|
+
* outcome than the drift it was looking for.
|
|
1786
|
+
*/
|
|
1787
|
+
declare function gateStatus(stamp: unknown, installed: {
|
|
1788
|
+
version: string;
|
|
1789
|
+
templateHash?: string;
|
|
1790
|
+
}): GateReading;
|
|
1661
1791
|
|
|
1662
1792
|
/**
|
|
1663
1793
|
* Applies a board move to the flat card list.
|
|
@@ -1717,4 +1847,4 @@ interface CardSheetProps {
|
|
|
1717
1847
|
}
|
|
1718
1848
|
declare function CardSheet({ card, state, lang, ui, onChange, onDelete, onClose }: CardSheetProps): React.JSX.Element | null;
|
|
1719
1849
|
|
|
1720
|
-
export { ANALYSIS_SECTIONS, AlertBanner, type AlertBannerProps, type Analysis, type AnalysisSection, AnalysisView, BOARD_VERSION, Board, type BoardCardData, type BoardCheck, type BoardColumnData, type BoardFacts, type BoardMoveResult, type BoardProblem, type BoardProps, type BoardTag, CAPABILITIES, type Capability, Card$1 as Card, type CardProps, CardSheet, type Check, type Choice, type ChoiceOption, type ColumnDef, type Competitor, type Constraint, CopyBlock, type CopyBlockLabels, type CopyBlockProps, type Counts, DashboardHeader, type DashboardHeaderProps, type DashboardThemeForm, DashboardThemeProvider, type DashboardThemeTokens, DetailSection, type DetailSectionProps, DetailSheet, type DetailSheetProps, FilterChip, type FilterChipProps, FilterChipRow, type Goal, GuideNote, type GuideNoteProps, GuideTour, type GuideTourProps, type HealthFact, HealthPill, type HealthPillProps, HealthPillRow, type HealthStatus, INTENTS, INTENT_COPY, type ImportedTheme, type InstallState, type Intent, type IntentCopy, LANGS, LANG_LABELS, LEVELS, type Lang, type Level, MARKETPLACE_NAME, MARKETPLACE_URL, type Market, type MarketplaceReport, type MarketplaceTool, MarketplaceView, type Metric, type Note, NotificationBell, type NotificationBellProps, type NotificationLabels, type NotificationTone, OWNERS, type Owner, PANELTIR_FILE_COUNT, PANELTIR_FINGERPRINT, PANELTIR_VERSION, PRESET_PANEL_THEMES, PanelApp, type PanelAppProps, type Card as PanelCard, type PanelNotification, type PanelState, type PanelTheme, type PanelThemes, type Preferences, type Reading, type Run, SetupGuide, type SetupGuideProps, type SetupRequirement, type SetupStatus, SetupWizard, type SetupWizardLabels, type SetupWizardProps, type Severity, StatTile, StatTileGrid, type StatTileProps, type Strategy, type Suggestion, type SuggestionStatus, THEME_FORMS, THEME_PRESETS, type Text, type ThemeFormName, type ThemePresetName, type Trend, UI, type UiStrings, type Weight, type WizardStep, applyMove, boardFacts, checkProgress, claimedWithoutStarting, claudeTheme, commandSnippet, countCards, cyberpunkTheme, decided, emptyCard, explainBoard, isCard, isFirstRun, ledgerForm, midnightTheme, moveCardInColumns, newId, offeredThemes, oldMoneyTheme, panelForm, paneltirBuild, paperForm, readBoard, readStoredLang, repositorySnippet, resetGuide, sparkPoints, stampForColumn, storeLang, text, today, trendOf, undecided, untranslated, useDashboardForm, useDashboardTheme, useDelegatedClick, useGuideNote, useMediaQuery, validateBoard, wizardStep, writeText };
|
|
1850
|
+
export { ANALYSIS_SECTIONS, AlertBanner, type AlertBannerProps, type Analysis, type AnalysisSection, AnalysisView, BOARD_VERSION, Board, type BoardCardData, type BoardCheck, type BoardColumnData, type BoardFacts, type BoardMoveResult, type BoardProblem, type BoardProps, type BoardTag, CAPABILITIES, type Capability, Card$1 as Card, type CardProps, CardSheet, type Check, type Choice, type ChoiceOption, type ColumnDef, type Competitor, type Constraint, CopyBlock, type CopyBlockLabels, type CopyBlockProps, type Counts, DashboardHeader, type DashboardHeaderProps, type DashboardThemeForm, DashboardThemeProvider, type DashboardThemeTokens, DetailSection, type DetailSectionProps, DetailSheet, type DetailSheetProps, FilterChip, type FilterChipProps, FilterChipRow, type GateReading, type GateStamp, type GateVerdict, type Goal, GuideNote, type GuideNoteProps, GuideTour, type GuideTourProps, type HealthFact, HealthPill, type HealthPillProps, HealthPillRow, type HealthStatus, INTENTS, INTENT_COPY, type ImportedTheme, type InstallState, type Intent, type IntentCopy, LANGS, LANG_LABELS, LEVELS, type Lang, type Level, MARKETPLACE_NAME, MARKETPLACE_URL, type Market, type MarketplaceReport, type MarketplaceTool, MarketplaceView, type Metric, type Note, NotificationBell, type NotificationBellProps, type NotificationLabels, type NotificationTone, OWNERS, type Owner, PANELTIR_FILE_COUNT, PANELTIR_FINGERPRINT, PANELTIR_TEMPLATE_HASH, PANELTIR_VERSION, PRESET_PANEL_THEMES, PanelApp, type PanelAppProps, type Card as PanelCard, type PanelNotification, type PanelState, type PanelTheme, type PanelThemes, type Preferences, type Reading, type Run, SetupGuide, type SetupGuideProps, type SetupRequirement, type SetupStatus, SetupWizard, type SetupWizardLabels, type SetupWizardProps, type Severity, StatTile, StatTileGrid, type StatTileProps, type Strategy, type Suggestion, type SuggestionStatus, THEME_FORMS, THEME_PRESETS, type Text, type ThemeFormName, type ThemePresetName, type Trend, UI, type UiStrings, type Weight, type WizardStep, applyMove, boardFacts, checkProgress, claimedWithoutStarting, claudeTheme, commandSnippet, countCards, cyberpunkTheme, decided, emptyCard, explainBoard, gateStatus, isCard, isFirstRun, ledgerForm, midnightTheme, moveCardInColumns, newId, offeredThemes, oldMoneyTheme, panelForm, paneltirBuild, paperForm, readBoard, readStoredLang, repositorySnippet, resetGuide, sparkPoints, stampForColumn, storeLang, text, today, trendOf, undecided, untranslated, useDashboardForm, useDashboardTheme, useDelegatedClick, useGuideNote, useMediaQuery, validateBoard, wizardStep, writeText };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
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.10.1";
|
|
3
|
+
var PANELTIR_FINGERPRINT = "sha256:b2d1daf120972ce3bfc54f3a9f09bccaddd6ceb306a65a1682b810012d85f33d";
|
|
4
|
+
var PANELTIR_FILE_COUNT = 163;
|
|
5
|
+
var PANELTIR_TEMPLATE_HASH = "sha256:034ba27266408af4f2ba5793d9cefbe448b168570d6a0b87dfe698dfe639feb1";
|
|
5
6
|
var paneltirBuild = {
|
|
6
7
|
version: PANELTIR_VERSION,
|
|
7
8
|
fingerprint: PANELTIR_FINGERPRINT,
|
|
8
9
|
fileCount: PANELTIR_FILE_COUNT,
|
|
10
|
+
/** What `init` stamps, and what a gate is compared against. */
|
|
11
|
+
templateHash: PANELTIR_TEMPLATE_HASH,
|
|
9
12
|
/** The tag this build came from, ready to compare or to link. */
|
|
10
13
|
tag: `v${PANELTIR_VERSION}`,
|
|
11
14
|
/** Short form, for a footer that has one line to spare. */
|
|
@@ -915,15 +918,16 @@ import { useState as useState5 } from "react";
|
|
|
915
918
|
|
|
916
919
|
// src/components/Setup/wizard.ts
|
|
917
920
|
function wizardStep(requirements) {
|
|
918
|
-
const
|
|
919
|
-
const
|
|
920
|
-
const
|
|
921
|
-
const
|
|
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");
|
|
922
926
|
if (at === -1) {
|
|
923
927
|
return { current: null, index: 0, total, done, ready: total > 0, unknown: false };
|
|
924
928
|
}
|
|
925
929
|
return {
|
|
926
|
-
current:
|
|
930
|
+
current: steps[at],
|
|
927
931
|
index: at + 1,
|
|
928
932
|
total,
|
|
929
933
|
done,
|
|
@@ -1472,6 +1476,18 @@ function offeredThemes(themes, extraThemes) {
|
|
|
1472
1476
|
return offered;
|
|
1473
1477
|
}
|
|
1474
1478
|
|
|
1479
|
+
// src/gate/stamp.ts
|
|
1480
|
+
function gateStatus(stamp, installed) {
|
|
1481
|
+
const reading = { verdict: "unstamped", installed: installed.version };
|
|
1482
|
+
if (typeof stamp !== "object" || stamp === null) return reading;
|
|
1483
|
+
const candidate = stamp;
|
|
1484
|
+
if (typeof candidate.templateHash !== "string" || candidate.templateHash === "") return reading;
|
|
1485
|
+
if (typeof candidate.version === "string" && candidate.version) reading.copiedFrom = candidate.version;
|
|
1486
|
+
if (typeof installed.templateHash !== "string" || installed.templateHash === "") return reading;
|
|
1487
|
+
reading.verdict = candidate.templateHash === installed.templateHash ? "current" : "behind";
|
|
1488
|
+
return reading;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1475
1491
|
// src/panel/AnalysisView.tsx
|
|
1476
1492
|
import { useMemo as useMemo3, useState as useState7 } from "react";
|
|
1477
1493
|
|
|
@@ -1599,6 +1615,9 @@ var UI = {
|
|
|
1599
1615
|
needTokenEnables: "Lets Save commit the board back to the repository. Without it everything else works and Save fails.",
|
|
1600
1616
|
needRepo: "The repository to write to",
|
|
1601
1617
|
needRepoEnables: "Which repository the board is committed to. There is no default on purpose: writing to the wrong one is worse than not writing.",
|
|
1618
|
+
needGate: "The gate this project copied",
|
|
1619
|
+
needGateEnables: "The sign-in, the session and the write-back are files this project owns, copied once and never updated with the library. When one of them is older than the package, the panel does not look misconfigured \u2014 it looks broken.",
|
|
1620
|
+
fixGate: "The gate is older than the installed kit. See what changed before replacing anything:",
|
|
1602
1621
|
fixTokenCreate: "On GitHub, make a fine-grained personal access token.",
|
|
1603
1622
|
fixTokenScope: "Give it Contents: read and write, on this repository only \u2014 nothing else, and no other repository.",
|
|
1604
1623
|
fixTokenPaste: "Paste it into the hosting project as GH_TOKEN. Never into the repository: a token in a commit is a token to revoke.",
|
|
@@ -1920,6 +1939,9 @@ var UI = {
|
|
|
1920
1939
|
needTokenEnables: "Permite que Guardar confirme el tablero en el repositorio. Sin \xE9l todo lo dem\xE1s funciona y Guardar falla.",
|
|
1921
1940
|
needRepo: "El repositorio donde escribir",
|
|
1922
1941
|
needRepoEnables: "En qu\xE9 repositorio se confirma el tablero. No hay valor por defecto a prop\xF3sito: escribir en el equivocado es peor que no escribir.",
|
|
1942
|
+
needGate: "La puerta que copi\xF3 este proyecto",
|
|
1943
|
+
needGateEnables: "El acceso, la sesi\xF3n y la escritura de vuelta son archivos de este proyecto, copiados una vez y que no se actualizan con la librer\xEDa. Cuando uno se queda m\xE1s viejo que el paquete, el panel no parece mal configurado: parece roto.",
|
|
1944
|
+
fixGate: "La puerta es m\xE1s vieja que el kit instalado. Mira qu\xE9 cambi\xF3 antes de reemplazar nada:",
|
|
1923
1945
|
fixTokenCreate: "En GitHub, crea un token de acceso personal de tipo fine-grained.",
|
|
1924
1946
|
fixTokenScope: "Dale Contents: read and write, solo sobre este repositorio \u2014 nada m\xE1s, y ning\xFAn otro repositorio.",
|
|
1925
1947
|
fixTokenPaste: "P\xE9galo en el proyecto de hosting como GH_TOKEN. Nunca en el repositorio: un token en un commit es un token que hay que revocar.",
|
|
@@ -2918,7 +2940,7 @@ function applyMove(cards, move, visible) {
|
|
|
2918
2940
|
function resolveTheme(saved, stored, themes, hasImported, fallback) {
|
|
2919
2941
|
for (const name of [saved, stored]) {
|
|
2920
2942
|
if (name === "imported" && hasImported) return "imported";
|
|
2921
|
-
if (name && name
|
|
2943
|
+
if (name && Object.prototype.hasOwnProperty.call(themes, name)) return name;
|
|
2922
2944
|
}
|
|
2923
2945
|
return fallback ?? Object.keys(themes)[0];
|
|
2924
2946
|
}
|
|
@@ -2977,6 +2999,7 @@ function PanelApp({
|
|
|
2977
2999
|
state: initialState,
|
|
2978
3000
|
themes: exactThemes,
|
|
2979
3001
|
extraThemes,
|
|
3002
|
+
gate,
|
|
2980
3003
|
defaultTheme,
|
|
2981
3004
|
brand,
|
|
2982
3005
|
decoration,
|
|
@@ -2994,6 +3017,7 @@ function PanelApp({
|
|
|
2994
3017
|
fingerprint
|
|
2995
3018
|
}) {
|
|
2996
3019
|
const themes = useMemo5(() => offeredThemes(exactThemes, extraThemes), [exactThemes, extraThemes]);
|
|
3020
|
+
const gateReading = useMemo5(() => gateStatus(gate, paneltirBuild), [gate]);
|
|
2997
3021
|
const [themeName, setThemeName] = useState10(
|
|
2998
3022
|
() => resolveTheme(
|
|
2999
3023
|
initialState.preferences?.theme,
|
|
@@ -3065,9 +3089,42 @@ function PanelApp({
|
|
|
3065
3089
|
variable: "PANEL_REPO",
|
|
3066
3090
|
enables: ui.needRepoEnables,
|
|
3067
3091
|
fix: /* @__PURE__ */ jsx20("p", { children: ui.fixRepo })
|
|
3068
|
-
}
|
|
3092
|
+
},
|
|
3093
|
+
// The gate is the only requirement here that is not an environment
|
|
3094
|
+
// variable: it is files this project owns, and the only one whose
|
|
3095
|
+
// failure is silent. A stale `api/panel-state.ts` does not read as
|
|
3096
|
+
// missing configuration — it reads as the panel being broken, which is
|
|
3097
|
+
// how the HTTP 500 cost a week.
|
|
3098
|
+
//
|
|
3099
|
+
// Drawn only when a stamp was passed. The kit does not invent a
|
|
3100
|
+
// requirement out of the absence of a prop: this project's own panel has
|
|
3101
|
+
// no copied gate — `template/` is the original, held honest by
|
|
3102
|
+
// `test:auth` — so a row here would invite wiring something that cannot
|
|
3103
|
+
// exist. A caller who passes nothing still has `paneltir doctor`, which
|
|
3104
|
+
// is what the skill file sends their Claude to.
|
|
3105
|
+
//
|
|
3106
|
+
// Passed but unreadable is a different thing and *is* reported, as
|
|
3107
|
+
// `unknown` rather than `missing`: telling somebody their gate is old on
|
|
3108
|
+
// the strength of a file we failed to parse is the mistake
|
|
3109
|
+
// `SetupWizard` already refuses to make.
|
|
3110
|
+
...gate === void 0 ? [] : [{
|
|
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,
|
|
3115
|
+
label: ui.needGate,
|
|
3116
|
+
status: gateReading.verdict === "current" ? "ok" : gateReading.verdict === "behind" ? "missing" : "unknown",
|
|
3117
|
+
enables: ui.needGateEnables,
|
|
3118
|
+
fix: /* @__PURE__ */ jsxs18(Fragment5, { children: [
|
|
3119
|
+
/* @__PURE__ */ jsxs18("p", { children: [
|
|
3120
|
+
ui.fixGate,
|
|
3121
|
+
gateReading.copiedFrom ? ` (v${gateReading.copiedFrom} \u2192 v${gateReading.installed})` : ""
|
|
3122
|
+
] }),
|
|
3123
|
+
/* @__PURE__ */ jsx20("pre", { className: "pt-copy__pre", children: "npx paneltir doctor" })
|
|
3124
|
+
] })
|
|
3125
|
+
}]
|
|
3069
3126
|
];
|
|
3070
|
-
}, [setup, ui]);
|
|
3127
|
+
}, [setup, ui, gate, gateReading]);
|
|
3071
3128
|
useEffect9(() => {
|
|
3072
3129
|
document.documentElement.lang = lang;
|
|
3073
3130
|
if (showLanguage) storeLang(lang);
|
|
@@ -4236,6 +4293,7 @@ export {
|
|
|
4236
4293
|
OWNERS,
|
|
4237
4294
|
PANELTIR_FILE_COUNT,
|
|
4238
4295
|
PANELTIR_FINGERPRINT,
|
|
4296
|
+
PANELTIR_TEMPLATE_HASH,
|
|
4239
4297
|
PANELTIR_VERSION,
|
|
4240
4298
|
PRESET_PANEL_THEMES,
|
|
4241
4299
|
PanelApp,
|
|
@@ -4257,6 +4315,7 @@ export {
|
|
|
4257
4315
|
decided,
|
|
4258
4316
|
emptyCard,
|
|
4259
4317
|
explainBoard,
|
|
4318
|
+
gateStatus,
|
|
4260
4319
|
isCard,
|
|
4261
4320
|
isFirstRun,
|
|
4262
4321
|
ledgerForm,
|
package/fingerprint.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paneltir",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"hash": "sha256:
|
|
5
|
-
"fileCount":
|
|
6
|
-
"
|
|
3
|
+
"version": "0.10.1",
|
|
4
|
+
"hash": "sha256:b2d1daf120972ce3bfc54f3a9f09bccaddd6ceb306a65a1682b810012d85f33d",
|
|
5
|
+
"fileCount": 163,
|
|
6
|
+
"templateHash": "sha256:034ba27266408af4f2ba5793d9cefbe448b168570d6a0b87dfe698dfe639feb1",
|
|
7
|
+
"generatedAt": "2026-09-11T07:08:17.308Z"
|
|
7
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "paneltir",
|
|
3
|
-
"version": "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)",
|
|
@@ -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 test:move && npm run test:theme && npm run test:setup && npm run test:updates && npm run release-notes:check && 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:gate && npm run test:move && npm run test:theme && npm run test:setup && npm run test:updates && npm run release-notes:check && 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",
|
|
@@ -78,7 +78,8 @@
|
|
|
78
78
|
"test:board": "node scripts/test-board.mjs",
|
|
79
79
|
"test:move": "node scripts/test-move.mjs",
|
|
80
80
|
"test:theme": "node scripts/test-theme.mjs",
|
|
81
|
-
"test:setup": "node scripts/test-setup.mjs"
|
|
81
|
+
"test:setup": "node scripts/test-setup.mjs",
|
|
82
|
+
"test:gate": "node scripts/test-gate.mjs"
|
|
82
83
|
},
|
|
83
84
|
"peerDependencies": {
|
|
84
85
|
"react": ">=18",
|
|
@@ -98,35 +99,56 @@
|
|
|
98
99
|
"node": ">=20"
|
|
99
100
|
},
|
|
100
101
|
"paneltirRelease": {
|
|
101
|
-
"version": "0.
|
|
102
|
-
"date": "2026-09-
|
|
102
|
+
"version": "0.10.1",
|
|
103
|
+
"date": "2026-09-11",
|
|
103
104
|
"entries": [
|
|
104
105
|
{
|
|
105
|
-
"kind": "
|
|
106
|
+
"kind": "fix",
|
|
107
|
+
"text": {
|
|
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
|
+
}
|
|
111
|
+
},
|
|
112
|
+
{
|
|
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",
|
|
121
|
+
"text": {
|
|
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."
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"kind": "fix",
|
|
106
128
|
"text": {
|
|
107
|
-
"en": "A
|
|
108
|
-
"es": "
|
|
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."
|
|
109
131
|
}
|
|
110
132
|
},
|
|
111
133
|
{
|
|
112
134
|
"kind": "fix",
|
|
113
135
|
"text": {
|
|
114
|
-
"en": "
|
|
115
|
-
"es": "
|
|
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."
|
|
116
138
|
}
|
|
117
139
|
},
|
|
118
140
|
{
|
|
119
141
|
"kind": "fix",
|
|
120
142
|
"text": {
|
|
121
|
-
"en": "
|
|
122
|
-
"es": "
|
|
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."
|
|
123
145
|
}
|
|
124
146
|
},
|
|
125
147
|
{
|
|
126
148
|
"kind": "chore",
|
|
127
149
|
"text": {
|
|
128
|
-
"en": "
|
|
129
|
-
"es": "
|
|
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."
|
|
130
152
|
}
|
|
131
153
|
}
|
|
132
154
|
]
|
|
@@ -132,6 +132,35 @@ Never invent a palette. If the project has no colours of its own, say so and
|
|
|
132
132
|
leave the theme alone — the kit ships presets for exactly that case, and one
|
|
133
133
|
of them is a light theme.
|
|
134
134
|
|
|
135
|
+
And when you hand this project's own theme to `PanelApp`, it goes in
|
|
136
|
+
**`extraThemes`** and never in `themes`. `extraThemes` offers it as well as the
|
|
137
|
+
kit's four; `themes` means "exactly these", so one theme passed there deletes
|
|
138
|
+
every preset and the settings sheet shows a single row. Two projects were
|
|
139
|
+
installed that way before the distinction existed, and the symptom reads as the
|
|
140
|
+
kit having no themes rather than as a prop being wrong.
|
|
141
|
+
|
|
142
|
+
## When the panel behaves oddly for no visible reason
|
|
143
|
+
|
|
144
|
+
The gate — `middleware.ts`, `lib/session.ts`, `api/*.ts` — was copied into this
|
|
145
|
+
project once and does **not** update when the kit does. So a fix released
|
|
146
|
+
upstream can be missing here while every version number on the screen looks
|
|
147
|
+
current, and nothing says so: the `HTTP 500` on saving lived in
|
|
148
|
+
`api/panel-state.ts` and stayed in projects installed before the fix.
|
|
149
|
+
|
|
150
|
+
npx paneltir doctor
|
|
151
|
+
|
|
152
|
+
`.paneltir/gate.json` is what makes that answer sharp, and it belongs in git:
|
|
153
|
+
it records what `init` copied and from where. Ignored, it vanishes on the next
|
|
154
|
+
clone and every file degrades to "differs" — the one answer that cannot say
|
|
155
|
+
whether you moved it or we did.
|
|
156
|
+
|
|
157
|
+
It separates the three answers that need different actions: *yours* (you edited
|
|
158
|
+
it), *BEHIND* (you did not touch it and the template moved), and *BOTH*. The
|
|
159
|
+
middle one is invisible to `git diff`, because there is nothing in this
|
|
160
|
+
project's history to compare against. `paneltir init --force` replaces the gate
|
|
161
|
+
and never the board — read the diff first when a file is marked BOTH, or the
|
|
162
|
+
edit is lost.
|
|
163
|
+
|
|
135
164
|
## Setting the panel up in the first place
|
|
136
165
|
|
|
137
166
|
The panel is asked for this by name: its walkthrough carries a line the owner
|
|
@@ -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
|
-
|
|
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:
|
|
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/${
|
|
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',
|