Spaces:
Running
Running
File size: 3,312 Bytes
422d12a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | // Refs the operator has hidden from the Overview.
//
// A deliberate, standing choice — "I don't want to see the cats group when I'm
// looking at my fleet" — and therefore NOT the same thing as archiving, which is
// derived from how long a session has been quiet and expires by itself. Hiding
// never expires, never depends on state, and hides a group whose agents are
// working right now if that is what you asked for.
//
// It lives on the server rather than in localStorage because it is a fact about
// the fleet, not about the browser looking at it: hide the group on the desktop
// and it stays hidden on the phone. (Contrast the Overview's sort and view, which
// are per-browser localStorage — those describe how you are looking, not what is
// worth looking at.)
//
// Scope: the OVERVIEW only. The sidebar still lists everything, which is both the
// way back to a hidden group and the reason hiding can never lose an agent.
//
// The stored values are the same refs `tree.order` speaks — `g:<id>` for a group,
// `s:<id>` for a loose session — so one list covers "hide this whole group" and
// "hide this one agent" without a second concept.
import fs from 'node:fs';
import path from 'node:path';
import { DATA_DIR } from './config.js';
const FILE = path.join(DATA_DIR, 'overview-hidden.json');
let refs = new Set();
const REF_RE = /^[gs]:[A-Za-z0-9._-]+$/;
function persist() {
try {
const tmp = `${FILE}.tmp`;
fs.writeFileSync(tmp, JSON.stringify([...refs], null, 2));
fs.renameSync(tmp, FILE);
} catch (e) {
console.error('[hidden.persist]', e && e.message);
}
}
export function init() {
try {
const p = JSON.parse(fs.readFileSync(FILE, 'utf8'));
refs = new Set(Array.isArray(p) ? p.filter((r) => typeof r === 'string' && REF_RE.test(r)) : []);
} catch { refs = new Set(); } // no file yet → nothing hidden
}
export const list = () => [...refs];
export const has = (ref) => refs.has(ref);
/** Hide or unhide one ref. Returns false for anything that isn't a ref. */
export function set(ref, hidden) {
if (typeof ref !== 'string' || !REF_RE.test(ref)) return false;
if (hidden) refs.add(ref); else refs.delete(ref);
persist();
return true;
}
/**
* Drop refs that no longer name anything, given the live set.
*
* Self-cleaning on read rather than a hook on every delete path: a group can stop
* existing in more ways than the delete endpoint (an edited groups.json, a
* restore, a bucket that lost a write), and a stale ref that outlives its group
* would silently hide a NEW group that later reuses the id. Only persists when
* something actually went away.
*
* Feed it EVERY live group and session, not the refs `tree.order` happens to
* hold: order lists loose sessions only, so a hidden agent that gets dragged into
* a group would otherwise quietly un-hide itself. And feed it the unfiltered
* lists — under demo mode the visible tree is a subset, and pruning against that
* would erase the operator's choices for everything demo mode is covering up.
*/
export function retain(validRefs) {
const live = validRefs instanceof Set ? validRefs : new Set(validRefs);
let dropped = false;
for (const r of refs) if (!live.has(r)) { refs.delete(r); dropped = true; }
if (dropped) persist();
return dropped;
}
|