@quolu/lattice 0.55.1 → 0.57.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/bin/lattice-bridge.mjs +3 -2
- package/package.json +1 -1
- package/sensor/dist/index.d.ts.map +1 -1
- package/sensor/dist/index.js +11 -1
- package/sensor/dist/index.js.map +1 -1
- package/src/bridge-cli.mjs +14 -2
- package/src/bridge-daemon.mjs +5 -2
- package/src/bridge-hub-heartbeat.mjs +53 -7
- package/src/bridge-hub-landing.mjs +223 -0
- package/src/bridge-hub-protocol.mjs +47 -25
- package/src/bridge-hub-server.mjs +90 -45
- package/src/bridge-server.mjs +8 -2
- package/src/cli-help.mjs +2 -0
- package/src/runtime-cli.mjs +10 -0
- package/src/runtime-pull-intake.mjs +96 -3
- package/src/todo-cli.mjs +54 -16
- package/src/todo-gantt-live.mjs +3 -3
package/src/runtime-cli.mjs
CHANGED
|
@@ -100,6 +100,7 @@ import {
|
|
|
100
100
|
acceptPullTask,
|
|
101
101
|
attachPullWorker,
|
|
102
102
|
closePullRun,
|
|
103
|
+
detachPullWorker,
|
|
103
104
|
inspectRunMode as inspectPullRunMode,
|
|
104
105
|
intakePullTask,
|
|
105
106
|
interventionPullTask,
|
|
@@ -4411,6 +4412,15 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
|
|
|
4411
4412
|
const output = await releasePullTask({ runDir, taskId: argv[6] });
|
|
4412
4413
|
stdout.write(`${JSON.stringify(output)}\n`); return 0;
|
|
4413
4414
|
};
|
|
4415
|
+
} else if (argv.length === 7
|
|
4416
|
+
&& argv[0] === 'run' && argv[1] === 'intake' && argv[2] === 'detach'
|
|
4417
|
+
&& argv[3] === '--run' && typeof argv[4] === 'string' && argv[4].length > 0
|
|
4418
|
+
&& argv[5] === '--task' && typeof argv[6] === 'string' && argv[6].length > 0) {
|
|
4419
|
+
action = async () => {
|
|
4420
|
+
const { runDir } = await resolveRunStore(cwd, argv[4]);
|
|
4421
|
+
const output = await detachPullWorker({ runDir, taskId: argv[6] });
|
|
4422
|
+
stdout.write(`${JSON.stringify(output)}\n`); return 0;
|
|
4423
|
+
};
|
|
4414
4424
|
} else if (argv.length === 9
|
|
4415
4425
|
&& argv[0] === 'run' && argv[1] === 'intake' && argv[2] === 'attach'
|
|
4416
4426
|
&& argv[3] === '--run' && typeof argv[4] === 'string' && argv[4].length > 0
|
|
@@ -216,6 +216,7 @@ function project(events, meta) {
|
|
|
216
216
|
sequence: event.sequence,
|
|
217
217
|
...structuredClone(event.payload),
|
|
218
218
|
worker: null,
|
|
219
|
+
worker_detached: false,
|
|
219
220
|
accepted: null,
|
|
220
221
|
});
|
|
221
222
|
} else if (event.kind === 'intake_refreshed') {
|
|
@@ -235,6 +236,13 @@ function project(events, meta) {
|
|
|
235
236
|
} else if (event.kind === 'worker_stopped' || event.kind === 'worker_resumed') {
|
|
236
237
|
const intake = intakes.get(event.task_id);
|
|
237
238
|
if (intake?.worker) intake.worker.stopped = event.kind === 'worker_stopped';
|
|
239
|
+
} else if (event.kind === 'worker_detached') {
|
|
240
|
+
const intake = intakes.get(event.task_id);
|
|
241
|
+
// `worker_detached` is remembered separately from `worker === null`: an
|
|
242
|
+
// intake that never had a worker and one whose worker was deliberately
|
|
243
|
+
// unbound look identical otherwise, and only the latter may be released
|
|
244
|
+
// by an operator who is not the original seat.
|
|
245
|
+
if (intake) { intake.worker = null; intake.worker_detached = true; }
|
|
238
246
|
} else if (event.kind === 'task_accepted') {
|
|
239
247
|
const intake = intakes.get(event.task_id);
|
|
240
248
|
if (intake) intake.accepted = structuredClone(event.payload);
|
|
@@ -287,6 +295,20 @@ function sameActor(left, right) {
|
|
|
287
295
|
return left?.host === right.host && left?.session === right.session && left?.agent === right.agent;
|
|
288
296
|
}
|
|
289
297
|
|
|
298
|
+
/**
|
|
299
|
+
* The acting actor when the caller has one, `null` when it does not.
|
|
300
|
+
*
|
|
301
|
+
* Recovery is performed by an operator who is by definition NOT the seat being
|
|
302
|
+
* recovered — the seat may be SIGSTOP'd, dead, or from a session that no longer
|
|
303
|
+
* exists. Demanding the seat's own env-derived identity on a recovery command
|
|
304
|
+
* would mean only the frozen seat could unfreeze itself, which is the trap this
|
|
305
|
+
* whole path exists to remove. The actor is recorded when present so the trail
|
|
306
|
+
* survives, but it is not an authorization gate here.
|
|
307
|
+
*/
|
|
308
|
+
function optionalActorFromEnvironment(environment = process.env) {
|
|
309
|
+
try { return actorFromEnvironment(environment); } catch { return null; }
|
|
310
|
+
}
|
|
311
|
+
|
|
290
312
|
function activeMember(store, planKey) {
|
|
291
313
|
const member = store.members?.find((entry) => entry.plan?.plan_key === planKey);
|
|
292
314
|
if (!member) fail('PLAN_NOT_ACTIVE', `active planが見つからない: ${planKey}`);
|
|
@@ -805,10 +827,15 @@ export async function releasePullTask({ runDir, taskId, environment = process.en
|
|
|
805
827
|
if (intake.worker !== null) {
|
|
806
828
|
fail('INTAKE_WORKER_ATTACHED', 'workerがattach済みのintakeはreleaseできない', {
|
|
807
829
|
task_id: taskId,
|
|
808
|
-
next_action: '
|
|
830
|
+
next_action: 'lattice run intake detach --run <run> --task <task> --json',
|
|
809
831
|
});
|
|
810
832
|
}
|
|
811
|
-
|
|
833
|
+
// The actor gate protects a seat's own in-progress work from another seat.
|
|
834
|
+
// Once the worker is detached there is no seat left to protect, and demanding
|
|
835
|
+
// the original session's identity would block the operator performing the
|
|
836
|
+
// recovery — the seat that owned this intake is precisely the one that is
|
|
837
|
+
// gone. The releasing actor is recorded either way.
|
|
838
|
+
if (intake.worker_detached !== true && !sameActor(intake.actor, actor)) {
|
|
812
839
|
fail('INTAKE_BINDING_CONFLICT', 'intakeを作成したactorだけがreleaseできる', {
|
|
813
840
|
task_id: taskId,
|
|
814
841
|
});
|
|
@@ -930,6 +957,66 @@ export async function attachPullWorker({ runDir, taskId, input, environment = pr
|
|
|
930
957
|
} finally { await lock.release(); }
|
|
931
958
|
}
|
|
932
959
|
|
|
960
|
+
/**
|
|
961
|
+
* Unbind a worker from its intake, releasing it first if the hold stopped it.
|
|
962
|
+
*
|
|
963
|
+
* This is the door `releasePullTask`'s own `next_action` already names. Without
|
|
964
|
+
* it a hold that can never clear leaves the seat SIGSTOP'd with no legitimate
|
|
965
|
+
* exit: `run close` refuses while the intake is unaccepted, `run intake release`
|
|
966
|
+
* refuses while a worker is attached, and `run abandon` is legacy-only. A shared
|
|
967
|
+
* pull run over a repo with no indexable code reached exactly that state
|
|
968
|
+
* (2026-08-10) — the sensor could not stamp an empty index, so the boundary was
|
|
969
|
+
* never verifiable and the hold was permanent.
|
|
970
|
+
*
|
|
971
|
+
* Authorization is process identity, not the actor: `signalAttachedWorker`
|
|
972
|
+
* re-verifies lstart/argv/pgid against the recorded binding before signalling,
|
|
973
|
+
* which is strictly stronger than an env-derived actor claim (env vars are
|
|
974
|
+
* trivially settable; a process's start identity is not). See
|
|
975
|
+
* `optionalActorFromEnvironment` for why the seat's own identity cannot be the
|
|
976
|
+
* gate on a recovery command.
|
|
977
|
+
*/
|
|
978
|
+
export async function detachPullWorker({ runDir, taskId, environment = process.env }) {
|
|
979
|
+
if (!identifier(taskId)) fail('INVALID_TASK_ID', 'task idが不正');
|
|
980
|
+
const actor = optionalActorFromEnvironment(environment);
|
|
981
|
+
const lock = await acquirePullLock(runDir, 'detach', `${taskId}-${randomUUID()}`);
|
|
982
|
+
try {
|
|
983
|
+
let current = await readPullStore(runDir);
|
|
984
|
+
const state = project(current.events, current.meta);
|
|
985
|
+
if (state.closed) fail('RUN_CLOSED', 'closed pull runのworkerをdetachできない');
|
|
986
|
+
const intake = state.intakes.find((entry) => entry.task_id === taskId);
|
|
987
|
+
if (intake === undefined) fail('INTAKE_NOT_FOUND', 'active intakeが存在しない', { task_id: taskId });
|
|
988
|
+
if (intake.accepted !== null) {
|
|
989
|
+
fail('INTAKE_ALREADY_ACCEPTED', 'accepted intakeのworkerはdetachできない', { task_id: taskId });
|
|
990
|
+
}
|
|
991
|
+
if (intake.worker === null) {
|
|
992
|
+
fail('INTAKE_WORKER_ABSENT', 'attach済みworkerが無い', { task_id: taskId });
|
|
993
|
+
}
|
|
994
|
+
// Resume before unbinding. Once the binding is gone nothing in this store can
|
|
995
|
+
// name that pid again, so a worker left stopped here would be unreachable by
|
|
996
|
+
// any future command — the same one-way door, one step further along.
|
|
997
|
+
const resumed = intake.worker.stopped;
|
|
998
|
+
if (resumed) {
|
|
999
|
+
await signalAttachedWorker(intake, 'SIGCONT');
|
|
1000
|
+
current = await appendEvent(runDir, current, buildEvent({
|
|
1001
|
+
events: current.events, meta: current.meta, kind: 'worker_resumed', taskId,
|
|
1002
|
+
payload: { released_by: 'worker_detach' },
|
|
1003
|
+
}));
|
|
1004
|
+
}
|
|
1005
|
+
current = await appendEvent(runDir, current, buildEvent({
|
|
1006
|
+
events: current.events, meta: current.meta, kind: 'worker_detached', taskId,
|
|
1007
|
+
payload: { detached_by: actor, pid: intake.worker.pid, resumed },
|
|
1008
|
+
}));
|
|
1009
|
+
const output = {
|
|
1010
|
+
schema: 'lattice.pull_worker_detach_result.v1', outcome: 'detached',
|
|
1011
|
+
run_id: state.run_id, task_id: taskId, pid: intake.worker.pid, resumed,
|
|
1012
|
+
next_action: 'lattice run intake release --run <run> --task <task> --json',
|
|
1013
|
+
result_digest: '',
|
|
1014
|
+
};
|
|
1015
|
+
output.result_digest = digestArtifact(output);
|
|
1016
|
+
return output;
|
|
1017
|
+
} finally { await lock.release(); }
|
|
1018
|
+
}
|
|
1019
|
+
|
|
933
1020
|
function observationModel(state) {
|
|
934
1021
|
const intakes = state.intakes.filter((entry) => (
|
|
935
1022
|
entry.accepted === null && entry.intervention.state !== 'hold'
|
|
@@ -1105,7 +1192,13 @@ export async function interventionPullTask(runDir, taskId) {
|
|
|
1105
1192
|
if (!intake) fail('TASK_NOT_INTAKED', `taskがintakeされていない: ${taskId}`);
|
|
1106
1193
|
const output = { schema: 'lattice.pull_intervention.v1', run_id: stored.meta.run_id,
|
|
1107
1194
|
task_id: taskId, ...structuredClone(intake.intervention),
|
|
1108
|
-
worker_attached: intake.worker !== null, worker_stopped: intake.worker?.stopped ?? false
|
|
1195
|
+
worker_attached: intake.worker !== null, worker_stopped: intake.worker?.stopped ?? false,
|
|
1196
|
+
// `next_action` says how to clear the hold. When the seat is already frozen
|
|
1197
|
+
// the operator also needs to know how to get out if it never clears — the
|
|
1198
|
+
// absence of that answer is what turned one unverifiable boundary into an
|
|
1199
|
+
// unrecoverable run (2026-08-10).
|
|
1200
|
+
recovery: intake.worker?.stopped === true
|
|
1201
|
+
? 'lattice run intake detach --run <run> --task <task> --json' : null };
|
|
1109
1202
|
output.result_digest = digestArtifact(output); return output;
|
|
1110
1203
|
}
|
|
1111
1204
|
|
package/src/todo-cli.mjs
CHANGED
|
@@ -1205,8 +1205,11 @@ async function migrate({ repoRoot, inputRef, serializationReviewed = false }) {
|
|
|
1205
1205
|
function extractionFreshnessViolations(repoRoot, extraction) {
|
|
1206
1206
|
let reachable;
|
|
1207
1207
|
try {
|
|
1208
|
+
// rev-listの出力はobject数に比例して伸びる(本repo実測で既定maxBuffer 1MiB超過済み)。
|
|
1209
|
+
// 隣のgit showと同様にmaxBufferを明示しないと、ENOBUFSが握られて
|
|
1210
|
+
// source_reachability_unreadableへ誤変換される。
|
|
1208
1211
|
reachable = new Set(gitSync(['rev-list', '--objects', '--all'], {
|
|
1209
|
-
cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
1212
|
+
cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 134_217_728,
|
|
1210
1213
|
}).split('\n').filter(Boolean).map((line) => line.split(' ')[0]));
|
|
1211
1214
|
} catch {
|
|
1212
1215
|
return [{ code: 'source_reachability_unreadable', path: '/tasks', task_ids: [],
|
|
@@ -2673,6 +2676,50 @@ async function serveGantt({ repoRoot, port, stdout, env, scope = DEFAULT_GANTT_S
|
|
|
2673
2676
|
return null;
|
|
2674
2677
|
}
|
|
2675
2678
|
|
|
2679
|
+
/**
|
|
2680
|
+
* Whether this command should mark the repo as an actively-worked project.
|
|
2681
|
+
*
|
|
2682
|
+
* Being "active" is not a local convenience: the bridge heartbeat sends every
|
|
2683
|
+
* active project to the hub, and the hub routes the published dashboard from
|
|
2684
|
+
* that set. So a command that flips this flag is claiming, on the operator's
|
|
2685
|
+
* behalf, that this terminal serves this project — for the next two hours.
|
|
2686
|
+
*
|
|
2687
|
+
* A read must never make that claim. Running `lattice todo status` inside a
|
|
2688
|
+
* clone to look at it once used to register the project here, contest it with
|
|
2689
|
+
* whichever terminal actually serves it, and (before partial acceptance landed)
|
|
2690
|
+
* take that terminal's whole project set down with it. The diagnosing operator
|
|
2691
|
+
* extended the very outage they were investigating (2026-08-10).
|
|
2692
|
+
*
|
|
2693
|
+
* The rule is an allowlist of store writes, not a denylist of known-bad reads.
|
|
2694
|
+
* The denylist it replaces had grown one incident at a time — `verify` was
|
|
2695
|
+
* added to it the day before, after `todo verify` turned out to be unreachable
|
|
2696
|
+
* for exactly the store inconsistency it exists to diagnose — and every read
|
|
2697
|
+
* added later would have defaulted back to claiming ownership.
|
|
2698
|
+
*
|
|
2699
|
+
* `dashboard` commands are excluded: they own the registry themselves, and
|
|
2700
|
+
* pre-registering the current repo ahead of `dashboard remove` would re-add
|
|
2701
|
+
* what the operator is asking to drop.
|
|
2702
|
+
*/
|
|
2703
|
+
function writesTodoStore(argv) {
|
|
2704
|
+
const [command, second, third] = argv;
|
|
2705
|
+
if (argv.includes('--schema')) return false;
|
|
2706
|
+
switch (command) {
|
|
2707
|
+
case 'note': return second !== 'list';
|
|
2708
|
+
case 'migrate': return !argv.includes('--dry-run');
|
|
2709
|
+
case 'snapshot': return argv.includes('--rebuild');
|
|
2710
|
+
case 'independence': return second === 'compile'
|
|
2711
|
+
|| (second === 'witness' && ['migrate', 'scaffold'].includes(third));
|
|
2712
|
+
case 'seam-proposal': return ['compile', 'apply', 'land'].includes(second);
|
|
2713
|
+
case 'evidence': return second === 'promote';
|
|
2714
|
+
case 'dependency': return second === 'connect';
|
|
2715
|
+
case 'phase': return second !== 'status';
|
|
2716
|
+
case 'start': case 'retract': case 'block': case 'unblock': case 'done':
|
|
2717
|
+
case 'reopen': case 'split': case 'revise': case 'revise-phase': case 'revise-set':
|
|
2718
|
+
return true;
|
|
2719
|
+
default: return false;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2676
2723
|
async function ensureActiveProjectDashboard({ repoRoot, env }) {
|
|
2677
2724
|
if (env.LATTICE_DASHBOARD_AUTOSTART === '0') return null;
|
|
2678
2725
|
const actorIdentity = ACTOR_ENV_KEYS.map((key) => env[key]);
|
|
@@ -3134,21 +3181,12 @@ export async function runTodoCli({ argv, cwd, stdout, stderr, env = process.env
|
|
|
3134
3181
|
|
|
3135
3182
|
try {
|
|
3136
3183
|
const repoRoot = resolveRepoRoot(cwd);
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
// `
|
|
3141
|
-
//
|
|
3142
|
-
|
|
3143
|
-
// side effect (registering this session as an active dashboard project),
|
|
3144
|
-
// and readTodoStoreStable retries a persistent STORE_INCONSISTENT as if it
|
|
3145
|
-
// were a transient in-flight write before giving up and reporting a
|
|
3146
|
-
// content-free STORE_BUSY. Running that pre-hook ahead of `verify` meant
|
|
3147
|
-
// the one command meant to surface the real inconsistency never got to —
|
|
3148
|
-
// it died in the same generic way every other command did (2026-08-10 P0:
|
|
3149
|
-
// `todo verify` was unreachable for the exact case it exists to diagnose).
|
|
3150
|
-
const verifyCommand = argv[0] === 'verify';
|
|
3151
|
-
if (!ganttCommand && !dashboardAdopt && !migrationDryRun && !verifyCommand) {
|
|
3184
|
+
// Only a store write claims this terminal as the project's live source; see
|
|
3185
|
+
// `writesTodoStore`. This also keeps the pre-hook away from `verify`, whose
|
|
3186
|
+
// whole job is to diagnose a store too inconsistent for the pre-hook's own
|
|
3187
|
+
// `readTodoStoreStable` to read (2026-08-10 P0: `todo verify` was
|
|
3188
|
+
// unreachable for exactly the case it exists to diagnose).
|
|
3189
|
+
if (writesTodoStore(argv)) {
|
|
3152
3190
|
await ensureActiveProjectDashboard({ repoRoot, env });
|
|
3153
3191
|
}
|
|
3154
3192
|
const result = atomicCommit
|
package/src/todo-gantt-live.mjs
CHANGED
|
@@ -62,7 +62,7 @@ function withExternalPane(html, pane) {
|
|
|
62
62
|
|
|
63
63
|
function liveHtml(html, headDigest, eventsPath, externalPane = null) {
|
|
64
64
|
const controller = `<script>(()=>{const badge=document.createElement('div');badge.setAttribute('role','status');badge.style.cssText='position:fixed;right:12px;bottom:12px;z-index:99;padding:6px 10px;border:1px solid #d9d8d4;border-radius:4px;background:#fcfcfb;font:600 12px system-ui';badge.textContent='進捗: 接続中';document.body.append(badge);const head=${JSON.stringify(headDigest)};let lastReceipt=Date.now();let stream=null;const receive=event=>{const next=JSON.parse(event.data);if(typeof next.head_digest!=='string')return;lastReceipt=Date.now();badge.textContent='進捗: 最新';if(next.head_digest!==head){badge.textContent='進捗: 更新を反映中';location.reload();}};const connect=()=>{if(stream)stream.close();badge.textContent='進捗: 接続中';stream=new EventSource(${JSON.stringify(eventsPath)});stream.onopen=()=>{lastReceipt=Date.now();};stream.addEventListener('state',receive);stream.addEventListener('ping',receive);stream.addEventListener('lattice-error',event=>{lastReceipt=Date.now();const detail=JSON.parse(event.data);badge.textContent='進捗: エラー '+detail.code;badge.style.borderColor='#d03b3b';});stream.onerror=()=>{badge.textContent='進捗: 再接続中';};};connect();setInterval(()=>{if(Date.now()-lastReceipt>${SSE_STALE_MS})connect();},${SSE_WATCHDOG_MS});})();</script>`;
|
|
65
|
-
const publicMetadata = '<meta name="description" content="Latticeで管理しているプロジェクトの依存工程と進捗を確認できます。"><meta name="
|
|
65
|
+
const publicMetadata = '<meta name="description" content="Latticeで管理しているプロジェクトの依存工程と進捗を確認できます。"><meta name="theme-color" content="#f7f3ea">';
|
|
66
66
|
const publicStyle = '<style>body[data-gantt-root]{grid-template-rows:auto minmax(0,1fr)}.lattice-live-brand{z-index:10;display:flex;align-items:center;gap:8px;min-width:0;padding:10px 16px;border-bottom:1px solid #d8d0c5;background:#f7f3ea;color:#6c655d;font:600 12px/1.5 system-ui,-apple-system,"Hiragino Sans","Yu Gothic UI",sans-serif}.lattice-live-brand a{color:#201d19;text-decoration:none}.lattice-live-brand a:hover{color:#315cbe}.lattice-live-brand strong{color:#201d19}.lattice-live-brand-note{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lattice-live-back{margin-left:auto!important;color:#315cbe!important;font-weight:750}@media(max-width:560px){.lattice-live-brand{padding:9px 12px}.lattice-live-brand-note{display:none}}</style>';
|
|
67
67
|
const publicHeader = '<header class="lattice-live-brand"><a href="https://kitepon.dev/">kitepon.dev</a><span aria-hidden="true">/</span><strong>Lattice</strong><span class="lattice-live-brand-note">公開工程表</span><a class="lattice-live-back" href="/projects/">一覧へ戻る</a></header>';
|
|
68
68
|
const live = html
|
|
@@ -87,14 +87,14 @@ function dashboardHtml(projects) {
|
|
|
87
87
|
}).join('');
|
|
88
88
|
const content = rows.length === 0 ? '<p>アクティブなプロジェクトはありません。</p>'
|
|
89
89
|
: `<ul>${rows}</ul>`;
|
|
90
|
-
return `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Latticeが管理している公開中の工程と現在地を確認できます。"><meta
|
|
90
|
+
return `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Latticeが管理している公開中の工程と現在地を確認できます。"><meta property="og:title" content="公開中の工程表 — Lattice"><meta property="og:description" content="Latticeが管理している公開中の工程と現在地を確認できます。"><meta name="theme-color" content="#f7f3ea"><title>公開中の工程表 — Lattice</title><style>:root{color-scheme:light;--paper:#f7f3ea;--panel:#fffdf8;--ink:#201d19;--soft:#6c655d;--line:#d8d0c5;--cobalt:#315cbe;--orange:#e85f2a}*{box-sizing:border-box}body{min-height:100vh;margin:0;color:var(--ink);background:var(--paper);font:16px/1.7 system-ui,-apple-system,sans-serif}.shell{max-width:880px;margin:0 auto;padding:28px 22px 40px}.brand{display:flex;align-items:center;gap:9px;padding-bottom:24px;border-bottom:1px solid var(--line);font-size:.88rem}.brand a,.footer a{color:var(--ink);font-weight:800;text-decoration:none}.brand a:hover,.footer a:hover{color:var(--cobalt)}.brand span{color:var(--soft)}main{padding:64px 0 72px}.eyebrow{margin:0 0 8px;color:var(--orange);font-size:.76rem;font-weight:800;letter-spacing:.14em}.lead{max-width:620px;margin:0 0 34px;color:var(--soft)}h1{margin:0 0 14px;font-size:clamp(2rem,6vw,3.4rem);line-height:1.12;letter-spacing:-.04em}ul{display:grid;gap:12px;margin:0;padding:0;list-style:none}li a{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:20px;padding:18px 20px;border:1px solid var(--line);border-radius:12px;color:inherit;background:var(--panel);text-decoration:none;box-shadow:0 8px 28px rgba(48,39,27,.04)}li a:hover{border-color:var(--cobalt);transform:translateY(-1px)}li strong{font-size:1.04rem}li code{color:var(--soft);font-size:.78rem}li span{color:var(--cobalt);font-weight:800}.note{margin:28px 0 0;padding:16px 18px;border-left:3px solid var(--orange);color:var(--soft);background:rgba(255,253,248,.72);font-size:.88rem}.footer{display:flex;flex-wrap:wrap;justify-content:space-between;gap:16px;padding-top:20px;border-top:1px solid var(--line);color:var(--soft);font-size:.82rem}.footer nav{display:flex;gap:18px}@media(max-width:560px){.shell{padding:20px 16px 32px}main{padding:44px 0 56px}li a{grid-template-columns:minmax(0,1fr) auto;padding:16px}li code{grid-column:1/-1;grid-row:2}.footer{display:block}.footer nav{margin-top:10px}}</style></head><body><div class="shell"><header class="brand"><a href="https://kitepon.dev/">kitepon.dev</a><span aria-hidden="true">/</span><strong>Lattice</strong></header><main><p class="eyebrow">LIVE DEVELOPMENT</p><h1>公開中の工程表</h1><p class="lead">Latticeが管理しているプロジェクトの工程と、いまどこまで進んでいるかを公開データから確認できます。</p>${content}<p class="note">表示内容はLatticeの記録から自動生成されます。製品の紹介や使い方はGitHubをご覧ください。</p></main><footer class="footer"><span>kitepon.dev の開発工程を、Latticeで可視化しています。</span><nav aria-label="関連リンク"><a href="https://kitepon.dev/">kitepon.dev</a><a href="https://github.com/kitepon-rgb/Lattice">GitHub</a></nav></footer></div></body></html>`;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
function notFoundHtml(code, path) {
|
|
94
94
|
const reason = code === 'PROJECT_NOT_FOUND'
|
|
95
95
|
? '指定された工程表は、公開を終了したかURLが変わった可能性があります。'
|
|
96
96
|
: '指定されたページは、この公開工程表にはありません。';
|
|
97
|
-
return `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="
|
|
97
|
+
return `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="theme-color" content="#f7f3ea"><title>ページが見つかりません — Lattice</title><style>:root{color-scheme:light;--paper:#f7f3ea;--panel:#fffdf8;--ink:#201d19;--soft:#6c655d;--line:#d8d0c5;--cobalt:#315cbe;--orange:#e85f2a}*{box-sizing:border-box}body{min-height:100vh;margin:0;color:var(--ink);background:var(--paper);font:16px/1.7 system-ui,-apple-system,"Hiragino Sans","Yu Gothic UI",sans-serif}.shell{width:min(720px,calc(100% - 32px));margin:0 auto;padding:28px 0 40px}.brand{display:flex;align-items:center;gap:9px;padding-bottom:24px;border-bottom:1px solid var(--line);font-size:.88rem}.brand a{color:var(--ink);font-weight:800;text-decoration:none}.brand a:hover{color:var(--cobalt)}.brand span{color:var(--soft)}main{padding:clamp(64px,12vw,112px) 0}.eyebrow{margin:0 0 10px;color:var(--orange);font-size:.76rem;font-weight:800;letter-spacing:.14em}h1{margin:0 0 16px;font-size:clamp(2.1rem,7vw,4rem);line-height:1.1;letter-spacing:-.045em}p{max-width:620px;margin:0;color:var(--soft)}code{display:block;margin-top:22px;padding:12px 14px;overflow-wrap:anywhere;border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--soft);font-size:.78rem}.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:28px}.actions a{display:inline-flex;align-items:center;min-height:44px;padding:0 16px;border:1px solid var(--line);border-radius:8px;color:var(--ink);background:var(--panel);font-weight:750;text-decoration:none}.actions a:first-child{border-color:var(--cobalt);color:#fff;background:var(--cobalt)}.actions a:hover{transform:translateY(-1px)}</style></head><body><div class="shell"><header class="brand"><a href="https://kitepon.dev/">kitepon.dev</a><span aria-hidden="true">/</span><strong>Lattice</strong></header><main><p class="eyebrow">404 · ${escapeHtml(code)}</p><h1>ページが見つかりません</h1><p>${reason}</p><code>${escapeHtml(path)}</code><nav class="actions" aria-label="戻り先"><a href="/projects/">公開工程表の一覧へ</a><a href="https://kitepon.dev/">kitepon.devへ</a></nav></main></div></body></html>`;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function validateProject(project) {
|