295 lines
8.4 KiB
JavaScript
295 lines
8.4 KiB
JavaScript
/**
|
|
* session-manager.js — Session taxonomy primitives for the Nexus operator cockpit
|
|
*
|
|
* Operations: create, list, setActive, group, tag, pin, archive, restore, delete
|
|
* Persistence: localStorage under key `nexus-sessions`
|
|
*
|
|
* Refs: issue #1695 — ATLAS cockpit operator patterns
|
|
* Pattern sources: dodo-reach/hermes-desktop session sidebar, nesquena/hermes-webui session groups
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
const STORAGE_KEY = 'nexus-sessions';
|
|
const META_KEY = 'nexus-sessions-meta';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Data model
|
|
// ---------------------------------------------------------------------------
|
|
// Session:
|
|
// id: string (uuid-ish)
|
|
// name: string
|
|
// group: string | null
|
|
// tags: string[]
|
|
// pinned: boolean
|
|
// archived: boolean
|
|
// createdAt: number (epoch ms)
|
|
// updatedAt: number
|
|
// meta: {} (arbitrary caller-supplied data)
|
|
//
|
|
// Meta:
|
|
// activeId: string | null
|
|
|
|
let _sessions = [];
|
|
let _activeId = null;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Persistence
|
|
// ---------------------------------------------------------------------------
|
|
function load() {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
_sessions = raw ? JSON.parse(raw) : [];
|
|
} catch {
|
|
_sessions = [];
|
|
}
|
|
try {
|
|
const rawMeta = localStorage.getItem(META_KEY);
|
|
const meta = rawMeta ? JSON.parse(rawMeta) : {};
|
|
_activeId = meta.activeId || null;
|
|
} catch {
|
|
_activeId = null;
|
|
}
|
|
}
|
|
|
|
function save() {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(_sessions));
|
|
localStorage.setItem(META_KEY, JSON.stringify({ activeId: _activeId }));
|
|
} catch (e) {
|
|
console.warn('[SessionManager] Could not persist sessions:', e);
|
|
}
|
|
emit('change', { sessions: _sessions, activeId: _activeId });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ID generation
|
|
// ---------------------------------------------------------------------------
|
|
function genId() {
|
|
return 'sess_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 7);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Event bus (lightweight)
|
|
// ---------------------------------------------------------------------------
|
|
const _listeners = {};
|
|
|
|
function on(event, fn) {
|
|
if (!_listeners[event]) _listeners[event] = [];
|
|
_listeners[event].push(fn);
|
|
}
|
|
|
|
function off(event, fn) {
|
|
if (!_listeners[event]) return;
|
|
_listeners[event] = _listeners[event].filter(f => f !== fn);
|
|
}
|
|
|
|
function emit(event, data) {
|
|
(_listeners[event] || []).forEach(fn => { try { fn(data); } catch {} });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Core CRUD
|
|
// ---------------------------------------------------------------------------
|
|
function create(name, opts = {}) {
|
|
const session = {
|
|
id: genId(),
|
|
name: name || 'Untitled Session',
|
|
group: opts.group || null,
|
|
tags: opts.tags || [],
|
|
pinned: opts.pinned || false,
|
|
archived: false,
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
meta: opts.meta || {},
|
|
};
|
|
_sessions.push(session);
|
|
if (!_activeId) _activeId = session.id;
|
|
save();
|
|
return session;
|
|
}
|
|
|
|
function get(id) {
|
|
return _sessions.find(s => s.id === id) || null;
|
|
}
|
|
|
|
function list(opts = {}) {
|
|
let result = [..._sessions];
|
|
|
|
if (opts.group) result = result.filter(s => s.group === opts.group);
|
|
if (opts.tag) result = result.filter(s => s.tags.includes(opts.tag));
|
|
if (opts.pinned !== undefined) result = result.filter(s => s.pinned === opts.pinned);
|
|
if (opts.archived !== undefined) result = result.filter(s => s.archived === opts.archived);
|
|
|
|
// Default: hide archived unless explicitly requested
|
|
if (opts.archived === undefined) result = result.filter(s => !s.archived);
|
|
|
|
// Pinned items first, then by updatedAt desc
|
|
result.sort((a, b) => {
|
|
if (a.pinned !== b.pinned) return b.pinned ? 1 : -1;
|
|
return b.updatedAt - a.updatedAt;
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
function update(id, patch) {
|
|
const idx = _sessions.findIndex(s => s.id === id);
|
|
if (idx < 0) return null;
|
|
_sessions[idx] = { ..._sessions[idx], ...patch, updatedAt: Date.now() };
|
|
save();
|
|
return _sessions[idx];
|
|
}
|
|
|
|
function remove(id) {
|
|
const before = _sessions.length;
|
|
_sessions = _sessions.filter(s => s.id !== id);
|
|
if (_activeId === id) _activeId = _sessions.find(s => !s.archived)?.id || null;
|
|
if (_sessions.length !== before) save();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Taxonomy operations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Set or clear the group for a session. */
|
|
function group(id, groupName) {
|
|
return update(id, { group: groupName || null });
|
|
}
|
|
|
|
/** Add one or more tags to a session. */
|
|
function tag(id, ...tags) {
|
|
const s = get(id);
|
|
if (!s) return null;
|
|
const merged = Array.from(new Set([...s.tags, ...tags.filter(Boolean)]));
|
|
return update(id, { tags: merged });
|
|
}
|
|
|
|
/** Remove a tag from a session. */
|
|
function untag(id, tagName) {
|
|
const s = get(id);
|
|
if (!s) return null;
|
|
return update(id, { tags: s.tags.filter(t => t !== tagName) });
|
|
}
|
|
|
|
/** Toggle pin state. */
|
|
function pin(id) {
|
|
const s = get(id);
|
|
if (!s) return null;
|
|
return update(id, { pinned: !s.pinned });
|
|
}
|
|
|
|
/** Archive a session (hides from default list). */
|
|
function archive(id) {
|
|
return update(id, { archived: true, pinned: false });
|
|
}
|
|
|
|
/** Restore an archived session. */
|
|
function restore(id) {
|
|
return update(id, { archived: false });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Active session
|
|
// ---------------------------------------------------------------------------
|
|
function getActive() {
|
|
return _activeId;
|
|
}
|
|
|
|
function getActiveSession() {
|
|
return _activeId ? get(_activeId) : null;
|
|
}
|
|
|
|
function setActive(id) {
|
|
if (!get(id)) return false;
|
|
_activeId = id;
|
|
save();
|
|
return true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Group helpers
|
|
// ---------------------------------------------------------------------------
|
|
function listGroups() {
|
|
const seen = new Set();
|
|
_sessions.forEach(s => { if (s.group) seen.add(s.group); });
|
|
return Array.from(seen).sort();
|
|
}
|
|
|
|
function listTags() {
|
|
const seen = new Set();
|
|
_sessions.forEach(s => s.tags.forEach(t => seen.add(t)));
|
|
return Array.from(seen).sort();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Import / export (for backup / hand-off)
|
|
// ---------------------------------------------------------------------------
|
|
function exportAll() {
|
|
return JSON.stringify({ sessions: _sessions, activeId: _activeId }, null, 2);
|
|
}
|
|
|
|
function importAll(json) {
|
|
try {
|
|
const data = JSON.parse(json);
|
|
if (!Array.isArray(data.sessions)) throw new Error('Invalid format');
|
|
_sessions = data.sessions;
|
|
_activeId = data.activeId || null;
|
|
save();
|
|
return true;
|
|
} catch (e) {
|
|
console.error('[SessionManager] Import failed:', e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Init
|
|
// ---------------------------------------------------------------------------
|
|
load();
|
|
|
|
// Create a default session if store is empty
|
|
if (_sessions.length === 0) {
|
|
create('Default', { tags: ['auto'], meta: { auto: true } });
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public API
|
|
// ---------------------------------------------------------------------------
|
|
window.SessionManager = {
|
|
create,
|
|
get,
|
|
list,
|
|
update,
|
|
remove,
|
|
|
|
// Taxonomy
|
|
group,
|
|
tag,
|
|
untag,
|
|
pin,
|
|
archive,
|
|
restore,
|
|
|
|
// Active session
|
|
getActive,
|
|
getActiveSession,
|
|
setActive,
|
|
|
|
// Helpers
|
|
listGroups,
|
|
listTags,
|
|
|
|
// Import/export
|
|
exportAll,
|
|
importAll,
|
|
|
|
// Events
|
|
on,
|
|
off,
|
|
};
|
|
|
|
console.info('[SessionManager] Loaded. Sessions:', _sessions.length, '| Active:', _activeId);
|
|
})();
|