feat(E4.6): activity panel fed from sync-state.json (log() → activity bridge)
AutoSyncController.log() now appends to sync-state.json.activity (when
features.syncStatus on), so the activity panel reads from the persisted state
instead of the in-memory events array. The in-memory events stay for the flag-off
/api/autosync path. Also fixes a concurrent-save race in saveSyncState.
- src/server.ts log(): appends to sync-state.json.activity via appendActivity
(fire-and-forget async) when features.syncStatus on + syncState loaded. Kind
inferred from status: pushed→push, skipped→skip, error→error. The in-memory
events array + counters stay (for /api/autosync when features off).
- src/sync-state.ts saveSyncState: unique tmp path per save
(tmp-${pid}-${seq}) so concurrent saves (log()→appendActivity + a mode-flip
handler) don't race on the same tmp file — one rename would consume the
other's tmp → ENOENT. Each save is atomic; the last rename wins (same content
since both write the shared in-memory state).
- src/dashboard.html refreshSyncState: renders #autoSyncLog (last 200) +
#autoSyncCounts (pushed/skipped/errors derived from activity) from
/api/sync-state when features on. Falls back to refreshAutosync
(controller.events) when off.
tsc clean; 235 passing project-wide (18 pre-existing fixture-missing unchanged).
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -252,6 +252,21 @@ async function refreshSyncState() {
|
||||
// (not a toast). In PREP, the banner is NOT shown (PREP is "not available", not
|
||||
// "paused" — the syncModeBtn + disabled autoSyncBtn handle that).
|
||||
document.getElementById('syncPausedBanner').style.display = (s.mode === 'RUN-THE-MATCH' && !s.autoSyncOn) ? '' : 'none';
|
||||
// E4.6: activity panel (last 200) from sync-state.json.activity — replaces the
|
||||
// in-memory events view when features.syncStatus is on. Counts derived from activity.
|
||||
const log = document.getElementById('autoSyncLog');
|
||||
if (log) {
|
||||
log.textContent = (s.activity && s.activity.length)
|
||||
? s.activity.map(e => `${e.time.replace('T',' ').slice(5,19)} ${(e.kind||e.status||'').padEnd(7)} ${e.name} — ${e.message}`).join('\n')
|
||||
: '(no activity yet — save a linked, seeded note in your vault to trigger a push)';
|
||||
}
|
||||
const counts = document.getElementById('autoSyncCounts');
|
||||
if (counts && s.activity) {
|
||||
const pushed = s.activity.filter(e => e.kind === 'push').length;
|
||||
const skipped = s.activity.filter(e => e.kind === 'skip').length;
|
||||
const errors = s.activity.filter(e => e.kind === 'error').length;
|
||||
counts.textContent = `pushed ${pushed} · skipped ${skipped} · errors ${errors}`;
|
||||
}
|
||||
}
|
||||
async function checkAuth() {
|
||||
// E7.3: /api/auth/status is the only always-open data endpoint. Fetch it first;
|
||||
|
||||
@@ -892,6 +892,13 @@ export class AutoSyncController {
|
||||
if (this.foundryGuardEnabled) {
|
||||
this.writeLogLine({ time, level: status === "error" ? "error" : "info", name, status, message });
|
||||
}
|
||||
// E4.6: also append to sync-state.json.activity (when features.syncStatus on)
|
||||
// so the activity panel reads from the persisted state. Kind inferred from
|
||||
// status: pushed→push, skipped→skip, error→error. Fire-and-forget (async).
|
||||
if (this.state.cfg.features?.syncStatus && this.state.syncState) {
|
||||
const kind = status === "pushed" ? "push" : status === "error" ? "error" : "skip";
|
||||
void appendActivity(this.state.cfg.outDir, this.state.syncState, { time, kind, name, status, message });
|
||||
}
|
||||
}
|
||||
|
||||
/** E1b.8: append a JSON-line to the daily log file (rotating on date change or
|
||||
|
||||
@@ -104,11 +104,15 @@ export async function loadSyncState(outDir: string, watchedDir: string): Promise
|
||||
return { state, freshened: false };
|
||||
}
|
||||
|
||||
/** E4.1: atomically write sync-state.json (tmp + rename). Never throws out of a
|
||||
* state-mutation path — callers catch + log (the flag-off-failure-path rule). */
|
||||
/** E4.1: atomically write sync-state.json (tmp + rename). Each save uses a
|
||||
* UNIQUE tmp path so concurrent saves (e.g. log()→appendActivity + a
|
||||
* mode-flip handler) don't race on the same tmp file (one rename would
|
||||
* consume the other's tmp → ENOENT). Never throws out of a state-mutation
|
||||
* path — callers catch + log (the flag-off-failure-path rule). */
|
||||
let saveSeq = 0;
|
||||
export async function saveSyncState(outDir: string, state: SyncState): Promise<void> {
|
||||
const path = join(outDir, "sync-state.json");
|
||||
const tmp = `${path}.tmp`;
|
||||
const tmp = `${path}.tmp-${process.pid}-${++saveSeq}`;
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
state.updatedAt = new Date().toISOString();
|
||||
await writeFile(tmp, JSON.stringify(state, null, 2), "utf8");
|
||||
|
||||
Reference in New Issue
Block a user