ltcai 3.0.1 → 3.1.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/README.md +27 -20
- package/docs/CHANGELOG.md +37 -0
- package/docs/V3_FRONTEND.md +20 -17
- package/latticeai/__init__.py +1 -1
- package/latticeai/api/auth.py +4 -1
- package/latticeai/api/search.py +4 -0
- package/latticeai/core/config.py +2 -0
- package/latticeai/core/embedding_providers.py +123 -0
- package/latticeai/core/workspace_os.py +1 -1
- package/latticeai/server_app.py +22 -6
- package/package.json +9 -4
- package/scripts/build_v3_assets.mjs +164 -0
- package/scripts/capture/README.md +28 -0
- package/scripts/capture/capture_enterprise.js +8 -0
- package/scripts/capture/capture_graph.js +8 -0
- package/scripts/capture/capture_onboarding.js +8 -0
- package/scripts/capture/capture_page.js +43 -0
- package/scripts/capture/capture_release_media.js +125 -0
- package/scripts/capture/capture_skills.js +8 -0
- package/scripts/capture/capture_workspace.js +8 -0
- package/scripts/generate_diagrams.py +513 -0
- package/scripts/lint_v3.mjs +33 -0
- package/scripts/release-0.3.1.sh +105 -0
- package/scripts/take_screenshots.js +69 -0
- package/scripts/validate_release_artifacts.py +167 -0
- package/static/account.html +9 -9
- package/static/activity.html +4 -4
- package/static/admin.html +8 -8
- package/static/agents.html +4 -4
- package/static/chat.html +9 -9
- package/static/css/tokens.5a595671.css +260 -0
- package/static/css/tokens.css +1 -1
- package/static/graph.html +9 -9
- package/static/plugins.html +4 -4
- package/static/sw.js +3 -1
- package/static/v3/asset-manifest.json +47 -0
- package/static/v3/css/lattice.base.e4cdd05d.css +128 -0
- package/static/v3/css/lattice.components.011e988b.css +447 -0
- package/static/v3/css/lattice.components.css +2 -2
- package/static/v3/css/lattice.shell.4920f42d.css +407 -0
- package/static/v3/css/lattice.tokens.c597ff81.css +132 -0
- package/static/v3/css/lattice.views.3ee19d4e.css +277 -0
- package/static/v3/index.html +38 -9
- package/static/v3/js/app.46fb61d9.js +26 -0
- package/static/v3/js/core/api.22a41d42.js +344 -0
- package/static/v3/js/core/api.js +68 -51
- package/static/v3/js/core/components.4c83e0a9.js +222 -0
- package/static/v3/js/core/components.js +9 -2
- package/static/v3/js/core/dom.a2773eb0.js +148 -0
- package/static/v3/js/core/router.584570f2.js +37 -0
- package/static/v3/js/core/routes.f935dd50.js +78 -0
- package/static/v3/js/core/routes.js +6 -1
- package/static/v3/js/core/shell.1b6199d6.js +363 -0
- package/static/v3/js/core/store.34ebd5e6.js +113 -0
- package/static/v3/js/views/admin-audit.660a1fb1.js +185 -0
- package/static/v3/js/views/admin-audit.js +1 -1
- package/static/v3/js/views/admin-permissions.a7ae5f09.js +177 -0
- package/static/v3/js/views/admin-permissions.js +4 -5
- package/static/v3/js/views/admin-policies.3658fd86.js +102 -0
- package/static/v3/js/views/admin-policies.js +4 -5
- package/static/v3/js/views/admin-private-vpc.7d342d36.js +135 -0
- package/static/v3/js/views/admin-private-vpc.js +2 -5
- package/static/v3/js/views/admin-security.07c66b72.js +180 -0
- package/static/v3/js/views/admin-security.js +4 -5
- package/static/v3/js/views/admin-users.03bac88c.js +168 -0
- package/static/v3/js/views/admin-users.js +6 -6
- package/static/v3/js/views/agents.14e48bdd.js +193 -0
- package/static/v3/js/views/agents.js +1 -2
- package/static/v3/js/views/chat.718144ce.js +449 -0
- package/static/v3/js/views/chat.js +2 -3
- package/static/v3/js/views/files.4935197e.js +186 -0
- package/static/v3/js/views/files.js +27 -21
- package/static/v3/js/views/home.cdde3b32.js +119 -0
- package/static/v3/js/views/hybrid-search.b22b97e0.js +195 -0
- package/static/v3/js/views/hybrid-search.js +1 -1
- package/static/v3/js/views/knowledge-graph.a14ea7e7.js +237 -0
- package/static/v3/js/views/knowledge-graph.js +2 -3
- package/static/v3/js/views/models.a1ffa147.js +256 -0
- package/static/v3/js/views/models.js +17 -8
- package/static/v3/js/views/my-computer.1b2ff621.js +237 -0
- package/static/v3/js/views/my-computer.js +5 -5
- package/static/v3/js/views/pipeline.c522f1ce.js +157 -0
- package/static/v3/js/views/pipeline.js +3 -7
- package/static/v3/js/views/settings.4f777210.js +250 -0
- package/static/v3/js/views/settings.js +6 -14
- package/static/workflows.html +4 -4
- package/static/workspace.html +5 -5
- package/docs/images/tmp_frames/frame_00.png +0 -0
- package/docs/images/tmp_frames/frame_01.png +0 -0
- package/docs/images/tmp_frames/frame_02.png +0 -0
- package/docs/images/tmp_frames/frame_03.png +0 -0
- package/docs/images/tmp_frames/hero_00.png +0 -0
- package/docs/images/tmp_frames/hero_01.png +0 -0
- package/docs/images/tmp_frames/hero_02.png +0 -0
- package/docs/images/tmp_frames/hero_03.png +0 -0
- package/static/v3/js/core/fixtures.js +0 -171
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Lattice AI v3 — Application shell
|
|
3
|
+
* Builds the persistent chrome (nav rail, topbar, view outlet), wires the
|
|
4
|
+
* router, workspace + mode + theme switchers, command palette, and mobile
|
|
5
|
+
* drawer. Renders views by lazy-loading their module and calling render(ctx).
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
import { h, icon, $, $$ } from "./dom.a2773eb0.js";
|
|
9
|
+
import { store } from "./store.34ebd5e6.js";
|
|
10
|
+
import { api } from "./api.22a41d42.js";
|
|
11
|
+
import * as c from "./components.4c83e0a9.js";
|
|
12
|
+
import { createRouter } from "./router.584570f2.js";
|
|
13
|
+
import { GROUPS, ROUTES, ROUTE_BY_KEY, MODE_RANK, visibleRoutes, loadView } from "./routes.f935dd50.js";
|
|
14
|
+
|
|
15
|
+
const MODES = [
|
|
16
|
+
{ key: "basic", label: "Basic", icon: "circle" },
|
|
17
|
+
{ key: "advanced", label: "Advanced", icon: "circles" },
|
|
18
|
+
{ key: "admin", label: "Admin", icon: "shield-half" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const ctxBase = { h, icon, api, store, c };
|
|
22
|
+
|
|
23
|
+
let els = {};
|
|
24
|
+
let router;
|
|
25
|
+
let currentRoute = null;
|
|
26
|
+
|
|
27
|
+
export function boot(rootEl) {
|
|
28
|
+
rootEl.classList.add("lt3-app");
|
|
29
|
+
rootEl.append(
|
|
30
|
+
h("a.lt3-skip", { href: "#lt3-view" }, "Skip to content"),
|
|
31
|
+
h("div.lt3-rail__scrim", { on: { click: closeDrawer } }),
|
|
32
|
+
buildRail(),
|
|
33
|
+
buildMain(),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
cacheEls(rootEl);
|
|
37
|
+
store.subscribe(onStateChange);
|
|
38
|
+
|
|
39
|
+
router = createRouter({ onRoute: renderRoute, fallback: "home" });
|
|
40
|
+
wireGlobalKeys();
|
|
41
|
+
router.start();
|
|
42
|
+
|
|
43
|
+
// Background: hydrate workspaces, identity, index status.
|
|
44
|
+
hydrate();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ── Rail ────────────────────────────────────────────────────────────────── */
|
|
48
|
+
function buildRail() {
|
|
49
|
+
return h("aside.lt3-rail", { id: "lt3-rail", "aria-label": "Primary" },
|
|
50
|
+
h("div.lt3-rail__brand",
|
|
51
|
+
h("div.lt3-rail__logo", { html: latticeMark() }),
|
|
52
|
+
h("div.lt3-rail__word", h("b", "Lattice AI"), h("small", "Local-First Workspace")),
|
|
53
|
+
h("button.lt3-iconbtn.lt3-iconbtn--sm.lt3-rail__close", { "aria-label": "Close menu", on: { click: closeDrawer } }, icon("x")),
|
|
54
|
+
),
|
|
55
|
+
h("div.lt3-rail__scope", { id: "lt3-scope" }),
|
|
56
|
+
h("nav.lt3-rail__nav", { id: "lt3-nav", "aria-label": "Sections" }),
|
|
57
|
+
h("div.lt3-rail__foot",
|
|
58
|
+
h("button.lt3-rail__user", { id: "lt3-user", "aria-label": "Account", on: { click: () => router.navigate("settings") } }),
|
|
59
|
+
h("button.lt3-iconbtn", { id: "lt3-theme", "aria-label": "Toggle theme", title: "Toggle theme", on: { click: () => store.toggleTheme() } }, icon("moon")),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderNav() {
|
|
65
|
+
const nav = els.nav;
|
|
66
|
+
const mode = store.get().mode;
|
|
67
|
+
const routes = visibleRoutes(mode);
|
|
68
|
+
nav.replaceChildren();
|
|
69
|
+
for (const group of GROUPS) {
|
|
70
|
+
const items = routes.filter((r) => r.group === group.id);
|
|
71
|
+
if (!items.length) continue;
|
|
72
|
+
const groupEl = h("div.lt3-navgroup",
|
|
73
|
+
h("div.lt3-navgroup__label", group.label),
|
|
74
|
+
items.map((r) => navItem(r)),
|
|
75
|
+
);
|
|
76
|
+
nav.append(groupEl);
|
|
77
|
+
}
|
|
78
|
+
markActive();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function navItem(route) {
|
|
82
|
+
return h("a.lt3-navitem", {
|
|
83
|
+
href: "#/" + route.key,
|
|
84
|
+
dataset: { key: route.key },
|
|
85
|
+
on: { click: () => closeDrawer() },
|
|
86
|
+
},
|
|
87
|
+
icon(route.icon),
|
|
88
|
+
h("span.lt3-navitem__label", route.label),
|
|
89
|
+
route.key === "hybrid-search" ? h("span.lt3-navitem__dot", { style: { background: "var(--lt3-pillar-hybrid)" } }) : null,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function markActive() {
|
|
94
|
+
$$(".lt3-navitem", els.nav).forEach((a) => {
|
|
95
|
+
const on = a.dataset.key === (currentRoute && currentRoute.key);
|
|
96
|
+
if (on) a.setAttribute("aria-current", "page"); else a.removeAttribute("aria-current");
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderScope() {
|
|
101
|
+
const ws = store.activeWorkspace();
|
|
102
|
+
els.scope.replaceChildren(
|
|
103
|
+
h("button.lt3-scope", { "aria-haspopup": "listbox", on: { click: openScopeMenu } },
|
|
104
|
+
h("div.lt3-scope__icon", icon(ws.type === "organization" ? "building-community" : "user")),
|
|
105
|
+
h("div.lt3-scope__meta", h("b", ws.name), h("small", `${ws.type} · ${ws.your_role || "member"}`)),
|
|
106
|
+
icon("selector"),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderUser() {
|
|
112
|
+
const u = store.get().user;
|
|
113
|
+
const initials = (u.nickname || u.email || "U").slice(0, 2);
|
|
114
|
+
els.user.replaceChildren(
|
|
115
|
+
h("span.lt3-avatar", initials),
|
|
116
|
+
h("div.lt3-rail__user-meta", h("b", u.nickname || u.email || "You"), h("small", u.role || "local")),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function updateThemeIcon() {
|
|
121
|
+
const dark = document.documentElement.getAttribute("data-lt-theme") === "dark"
|
|
122
|
+
|| (!store.get().theme && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
123
|
+
els.theme.replaceChildren(icon(dark ? "sun" : "moon"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ── Main / topbar ──────────────────────────────────────────────────────── */
|
|
127
|
+
function buildMain() {
|
|
128
|
+
return h("div.lt3-main",
|
|
129
|
+
h("header.lt3-topbar",
|
|
130
|
+
h("button.lt3-iconbtn.lt3-topbar__menu", { "aria-label": "Open menu", on: { click: openDrawer } }, icon("menu-2")),
|
|
131
|
+
h("div.lt3-topbar__crumbs", { id: "lt3-crumbs" }),
|
|
132
|
+
h("div.lt3-spacer"),
|
|
133
|
+
h("button.lt3-cmd-trigger", { "aria-label": "Search and commands", on: { click: openPalette } },
|
|
134
|
+
icon("search"), h("span", "Search & commands"), h("span.lt3-kbd", "⌘K")),
|
|
135
|
+
h("div", { id: "lt3-idxchip" }),
|
|
136
|
+
h("div.lt3-mode", { id: "lt3-mode", role: "tablist", "aria-label": "Workspace mode" },
|
|
137
|
+
MODES.map((m) => h("button", {
|
|
138
|
+
type: "button", role: "tab", dataset: { mode: m.key },
|
|
139
|
+
on: { click: () => store.setMode(m.key) },
|
|
140
|
+
}, icon(m.icon), h("span", m.label))),
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
h("main.lt3-view", { id: "lt3-view", tabindex: "-1" },
|
|
144
|
+
h("div.lt3-view__inner", { id: "lt3-outlet" }),
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderMode() {
|
|
150
|
+
$$("#lt3-mode button", els.root).forEach((b) => b.dataset.active = String(b.dataset.mode === store.get().mode));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderCrumbs() {
|
|
154
|
+
const r = currentRoute;
|
|
155
|
+
if (!r) return;
|
|
156
|
+
const parts = [h("span.lt3-crumb", store.activeWorkspace().name)];
|
|
157
|
+
if (r.group === "admin") parts.push(icon("chevron-right"), h("span.lt3-crumb", "Admin"));
|
|
158
|
+
parts.push(icon("chevron-right"), h("span.lt3-crumb.lt3-crumb--current", r.title || r.label));
|
|
159
|
+
els.crumbs.replaceChildren(...parts);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderIndexChip() {
|
|
163
|
+
els.idxchip.replaceChildren(c.indexChip(store.get().indexStatus));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ── View rendering ─────────────────────────────────────────────────────── */
|
|
167
|
+
async function renderRoute({ key, params }) {
|
|
168
|
+
let route = ROUTE_BY_KEY[key] || ROUTE_BY_KEY.home;
|
|
169
|
+
// Deep-linking into an admin area surfaces Admin mode so the rail matches.
|
|
170
|
+
if (route.admin && store.get().mode !== "admin") store.setMode("admin");
|
|
171
|
+
currentRoute = route;
|
|
172
|
+
store.setRoute({ key: route.key, params });
|
|
173
|
+
|
|
174
|
+
document.title = `${route.title || route.label} · Lattice AI`;
|
|
175
|
+
markActive();
|
|
176
|
+
renderCrumbs();
|
|
177
|
+
|
|
178
|
+
const outlet = els.outlet;
|
|
179
|
+
outlet.replaceChildren(c.loading({ lines: 4, block: true }));
|
|
180
|
+
els.view.scrollTop = 0;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const mod = await loadView(route.view);
|
|
184
|
+
if (currentRoute !== route) return; // navigated away during load
|
|
185
|
+
els.view.classList.toggle("lt3-view--flush", mod.layout === "flush");
|
|
186
|
+
const ctx = { ...ctxBase, route, params, navigate: router.navigate, toast: c.toast };
|
|
187
|
+
const node = await mod.render(ctx);
|
|
188
|
+
if (currentRoute !== route) return;
|
|
189
|
+
outlet.replaceChildren(node);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error("[shell] view render failed:", route.view, err);
|
|
192
|
+
outlet.replaceChildren(c.errorState(`View "${route.label}" failed to load.`, () => renderRoute({ key: route.key, params })));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function renderCurrent() {
|
|
197
|
+
if (currentRoute) renderRoute({ key: currentRoute.key, params: store.get().route.params || {} });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* ── State reactions ────────────────────────────────────────────────────── */
|
|
201
|
+
function onStateChange(_state, change) {
|
|
202
|
+
switch (change.type) {
|
|
203
|
+
case "mode": renderNav(); renderMode(); renderCurrent(); break;
|
|
204
|
+
case "workspace": renderScope(); renderCrumbs(); renderCurrent(); break;
|
|
205
|
+
case "workspaces": renderScope(); break;
|
|
206
|
+
case "user": renderUser(); break;
|
|
207
|
+
case "theme": updateThemeIcon(); break;
|
|
208
|
+
case "index": renderIndexChip(); break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ── Workspace scope menu ───────────────────────────────────────────────── */
|
|
213
|
+
function openScopeMenu(ev) {
|
|
214
|
+
ev.stopPropagation();
|
|
215
|
+
closeMenus();
|
|
216
|
+
const rect = ev.currentTarget.getBoundingClientRect();
|
|
217
|
+
const list = store.get().workspaces;
|
|
218
|
+
const menu = h("div.lt3-menu", { id: "lt3-scope-menu", role: "listbox", style: { top: rect.bottom + 6 + "px", left: rect.left + "px" } },
|
|
219
|
+
list.map((w) => h("button.lt3-menu__item", {
|
|
220
|
+
role: "option", dataset: { active: String(w.workspace_id === store.get().workspaceId) },
|
|
221
|
+
on: { click: () => { store.setWorkspace(w.workspace_id); closeMenus(); } },
|
|
222
|
+
},
|
|
223
|
+
icon(w.type === "organization" ? "building-community" : "user"),
|
|
224
|
+
h("div", h("div", { style: { fontWeight: 600 } }, w.name), h("small.lt3-faint", { style: { textTransform: "capitalize" } }, w.type)),
|
|
225
|
+
w.workspace_id === store.get().workspaceId ? icon("check", "") : null,
|
|
226
|
+
)),
|
|
227
|
+
h("div.lt3-menu__sep"),
|
|
228
|
+
h("button.lt3-menu__item", { on: { click: () => { c.toast("Organization creation opens in Settings", "info"); closeMenus(); router.navigate("settings"); } } },
|
|
229
|
+
icon("plus"), "New organization"),
|
|
230
|
+
);
|
|
231
|
+
document.body.append(menu);
|
|
232
|
+
setTimeout(() => document.addEventListener("click", closeMenusOnce, { once: true }), 0);
|
|
233
|
+
}
|
|
234
|
+
function closeMenus() { $$(".lt3-menu").forEach((m) => m.remove()); }
|
|
235
|
+
function closeMenusOnce() { closeMenus(); }
|
|
236
|
+
|
|
237
|
+
/* ── Mobile drawer ──────────────────────────────────────────────────────── */
|
|
238
|
+
function openDrawer() { els.root.dataset.drawer = "open"; }
|
|
239
|
+
function closeDrawer() { delete els.root.dataset.drawer; }
|
|
240
|
+
|
|
241
|
+
/* ── Command palette ────────────────────────────────────────────────────── */
|
|
242
|
+
function paletteItems() {
|
|
243
|
+
const nav = ROUTES.map((r) => ({
|
|
244
|
+
group: "Go to", label: r.label, icon: r.icon, hint: r.group,
|
|
245
|
+
run: () => router.navigate(r.key),
|
|
246
|
+
}));
|
|
247
|
+
const actions = [
|
|
248
|
+
{ group: "Actions", label: "Toggle light / dark theme", icon: "contrast", run: () => store.toggleTheme() },
|
|
249
|
+
{ group: "Actions", label: "Mode: Basic", icon: "circle", run: () => store.setMode("basic") },
|
|
250
|
+
{ group: "Actions", label: "Mode: Advanced", icon: "circles", run: () => store.setMode("advanced") },
|
|
251
|
+
{ group: "Actions", label: "Mode: Admin", icon: "shield-half", run: () => store.setMode("admin") },
|
|
252
|
+
{ group: "Actions", label: "New chat", icon: "message-plus", run: () => router.navigate("chat", { new: "1" }) },
|
|
253
|
+
{ group: "Actions", label: "Run hybrid search", icon: "arrows-join", run: () => router.navigate("hybrid-search") },
|
|
254
|
+
];
|
|
255
|
+
return [...nav, ...actions];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function openPalette() {
|
|
259
|
+
if ($("#lt3-palette")) return;
|
|
260
|
+
const all = paletteItems();
|
|
261
|
+
let active = 0, filtered = all;
|
|
262
|
+
|
|
263
|
+
const listEl = h("div.lt3-palette__list");
|
|
264
|
+
const input = h("input", { type: "text", placeholder: "Search views, run a command…", "aria-label": "Command palette", autocomplete: "off" });
|
|
265
|
+
const palette = h("div.lt3-palette", { id: "lt3-palette", role: "dialog", "aria-modal": "true", "aria-label": "Command palette" },
|
|
266
|
+
h("div.lt3-palette__input", icon("search"), input, h("span.lt3-kbd", "Esc")),
|
|
267
|
+
listEl,
|
|
268
|
+
);
|
|
269
|
+
const scrim = h("div.lt3-scrim", { id: "lt3-palette-scrim", on: { click: close } });
|
|
270
|
+
document.body.append(scrim, palette);
|
|
271
|
+
input.focus();
|
|
272
|
+
|
|
273
|
+
function renderList() {
|
|
274
|
+
listEl.replaceChildren();
|
|
275
|
+
if (!filtered.length) { listEl.append(h("div.lt3-palette__empty", "No matches")); return; }
|
|
276
|
+
let lastGroup = null;
|
|
277
|
+
filtered.forEach((item, i) => {
|
|
278
|
+
if (item.group !== lastGroup) { listEl.append(h("div.lt3-palette__group-label", item.group)); lastGroup = item.group; }
|
|
279
|
+
listEl.append(h("button.lt3-palette__item", {
|
|
280
|
+
dataset: { active: String(i === active) },
|
|
281
|
+
on: { click: () => { item.run(); close(); }, mousemove: () => { if (active !== i) { active = i; paint(); } } },
|
|
282
|
+
}, icon(item.icon), h("span", item.label), item.hint && h("small", item.hint)));
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function paint() { $$(".lt3-palette__item", listEl).forEach((el, i) => el.dataset.active = String(i === active)); ensureVisible(); }
|
|
286
|
+
function ensureVisible() {
|
|
287
|
+
const el = $$(".lt3-palette__item", listEl)[active];
|
|
288
|
+
if (el) el.scrollIntoView({ block: "nearest" });
|
|
289
|
+
}
|
|
290
|
+
function filter() {
|
|
291
|
+
const q = input.value.trim().toLowerCase();
|
|
292
|
+
filtered = !q ? all : all.filter((it) => (it.label + " " + (it.hint || "")).toLowerCase().includes(q));
|
|
293
|
+
active = 0; renderList();
|
|
294
|
+
}
|
|
295
|
+
function close() { palette.remove(); scrim.remove(); document.removeEventListener("keydown", onKey, true); }
|
|
296
|
+
function onKey(e) {
|
|
297
|
+
if (e.key === "Escape") { e.preventDefault(); e.stopPropagation(); close(); }
|
|
298
|
+
else if (e.key === "ArrowDown") { e.preventDefault(); active = Math.min(filtered.length - 1, active + 1); paint(); }
|
|
299
|
+
else if (e.key === "ArrowUp") { e.preventDefault(); active = Math.max(0, active - 1); paint(); }
|
|
300
|
+
else if (e.key === "Enter") { e.preventDefault(); const it = filtered[active]; if (it) { it.run(); close(); } }
|
|
301
|
+
}
|
|
302
|
+
input.addEventListener("input", filter);
|
|
303
|
+
document.addEventListener("keydown", onKey, true);
|
|
304
|
+
renderList();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function wireGlobalKeys() {
|
|
308
|
+
document.addEventListener("keydown", (e) => {
|
|
309
|
+
if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) { e.preventDefault(); openPalette(); }
|
|
310
|
+
if (e.key === "Escape") { closeMenus(); closeDrawer(); }
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* ── Hydration ──────────────────────────────────────────────────────────── */
|
|
315
|
+
async function hydrate() {
|
|
316
|
+
// Identity (best-effort; never blocks the UI).
|
|
317
|
+
api.raw("/account/profile").then((r) => {
|
|
318
|
+
if (r.ok && r.data && (r.data.email || r.data.nickname)) {
|
|
319
|
+
store.setUser({ email: r.data.email, nickname: r.data.nickname || r.data.email, role: r.data.role || "user" });
|
|
320
|
+
} else { renderUser(); }
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Workspaces from the OS payload (fallback-safe).
|
|
324
|
+
api.workspaceOs().then((r) => {
|
|
325
|
+
const reg = r.data && r.data.workspace_registry;
|
|
326
|
+
if (reg && Array.isArray(reg.workspaces) && reg.workspaces.length) store.setWorkspaces(reg.workspaces);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Index status powers the topbar chip + Home pillars.
|
|
330
|
+
api.indexStatus().then((r) => store.setIndexStatus(r.data));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ── Init helpers ───────────────────────────────────────────────────────── */
|
|
334
|
+
function cacheEls(root) {
|
|
335
|
+
els = {
|
|
336
|
+
root,
|
|
337
|
+
nav: $("#lt3-nav", root),
|
|
338
|
+
scope: $("#lt3-scope", root),
|
|
339
|
+
user: $("#lt3-user", root),
|
|
340
|
+
theme: $("#lt3-theme", root),
|
|
341
|
+
crumbs: $("#lt3-crumbs", root),
|
|
342
|
+
idxchip: $("#lt3-idxchip", root),
|
|
343
|
+
outlet: $("#lt3-outlet", root),
|
|
344
|
+
view: $("#lt3-view", root),
|
|
345
|
+
};
|
|
346
|
+
renderNav();
|
|
347
|
+
renderScope();
|
|
348
|
+
renderUser();
|
|
349
|
+
renderMode();
|
|
350
|
+
updateThemeIcon();
|
|
351
|
+
renderIndexChip();
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function latticeMark() {
|
|
355
|
+
// Crystalline lattice glyph — the product mark.
|
|
356
|
+
return `<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
|
357
|
+
<path d="M12 2.5 4 7v10l8 4.5L20 17V7L12 2.5Z" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" opacity=".55"/>
|
|
358
|
+
<path d="M12 7.5 7.5 10v4L12 16.5 16.5 14v-4L12 7.5Z" fill="currentColor" opacity=".9"/>
|
|
359
|
+
<circle cx="12" cy="2.5" r="1.3" fill="currentColor"/><circle cx="4" cy="7" r="1.1" fill="currentColor"/>
|
|
360
|
+
<circle cx="20" cy="7" r="1.1" fill="currentColor"/><circle cx="4" cy="17" r="1.1" fill="currentColor"/>
|
|
361
|
+
<circle cx="20" cy="17" r="1.1" fill="currentColor"/><circle cx="12" cy="21.5" r="1.3" fill="currentColor"/>
|
|
362
|
+
</svg>`;
|
|
363
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* Lattice AI v3 — App state store
|
|
3
|
+
* A minimal observable store with namespaced persistence. Holds the cross-view
|
|
4
|
+
* product state: theme, mode (Basic/Advanced/Admin), and the active workspace
|
|
5
|
+
* (Personal/Organization). Views/shell subscribe to react to changes.
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
const LS = {
|
|
9
|
+
theme: "lt-theme", // shared with the rest of Lattice (data-lt-theme)
|
|
10
|
+
mode: "lt3-mode",
|
|
11
|
+
workspace: "lt3-workspace",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function load(key, fallback) {
|
|
15
|
+
try { const v = localStorage.getItem(key); return v == null ? fallback : v; }
|
|
16
|
+
catch { return fallback; }
|
|
17
|
+
}
|
|
18
|
+
function save(key, value) {
|
|
19
|
+
try { localStorage.setItem(key, value); } catch { /* private mode */ }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const VALID_MODES = ["basic", "advanced", "admin"];
|
|
23
|
+
|
|
24
|
+
const state = {
|
|
25
|
+
theme: load(LS.theme, ""), // "" → follow OS
|
|
26
|
+
mode: VALID_MODES.includes(load(LS.mode)) ? load(LS.mode) : "basic",
|
|
27
|
+
workspaceId: load(LS.workspace, "personal"),
|
|
28
|
+
workspaces: [
|
|
29
|
+
{ workspace_id: "personal", name: "Personal Workspace", type: "personal", your_role: "owner" },
|
|
30
|
+
],
|
|
31
|
+
user: { email: "", nickname: "You", role: "user" },
|
|
32
|
+
indexStatus: null,
|
|
33
|
+
route: { key: "home", params: {} },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const subscribers = new Set();
|
|
37
|
+
|
|
38
|
+
function emit(change) {
|
|
39
|
+
for (const fn of subscribers) {
|
|
40
|
+
try { fn(state, change); } catch (err) { console.error("[store] subscriber", err); }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const store = {
|
|
45
|
+
get: () => state,
|
|
46
|
+
|
|
47
|
+
subscribe(fn) { subscribers.add(fn); return () => subscribers.delete(fn); },
|
|
48
|
+
|
|
49
|
+
/* ── Theme ─────────────────────────────────────────────── */
|
|
50
|
+
applyTheme() {
|
|
51
|
+
const root = document.documentElement;
|
|
52
|
+
if (state.theme === "dark" || state.theme === "light") {
|
|
53
|
+
root.setAttribute("data-lt-theme", state.theme);
|
|
54
|
+
} else {
|
|
55
|
+
root.removeAttribute("data-lt-theme"); // OS-follow via tokens.css media query
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
setTheme(theme) {
|
|
59
|
+
state.theme = theme === "dark" || theme === "light" ? theme : "";
|
|
60
|
+
if (state.theme) save(LS.theme, state.theme); else { try { localStorage.removeItem(LS.theme); } catch {} }
|
|
61
|
+
store.applyTheme();
|
|
62
|
+
emit({ type: "theme" });
|
|
63
|
+
},
|
|
64
|
+
toggleTheme() {
|
|
65
|
+
const effective = document.documentElement.getAttribute("data-lt-theme")
|
|
66
|
+
|| (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
|
67
|
+
store.setTheme(effective === "dark" ? "light" : "dark");
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/* ── Mode ──────────────────────────────────────────────── */
|
|
71
|
+
setMode(mode) {
|
|
72
|
+
if (!VALID_MODES.includes(mode) || mode === state.mode) return;
|
|
73
|
+
state.mode = mode;
|
|
74
|
+
save(LS.mode, mode);
|
|
75
|
+
emit({ type: "mode" });
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/* ── Workspace ─────────────────────────────────────────── */
|
|
79
|
+
setWorkspaces(list) {
|
|
80
|
+
if (Array.isArray(list) && list.length) {
|
|
81
|
+
state.workspaces = list;
|
|
82
|
+
if (!list.some((w) => w.workspace_id === state.workspaceId)) {
|
|
83
|
+
state.workspaceId = list[0].workspace_id;
|
|
84
|
+
save(LS.workspace, state.workspaceId);
|
|
85
|
+
}
|
|
86
|
+
emit({ type: "workspaces" });
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
setWorkspace(id) {
|
|
90
|
+
if (id === state.workspaceId) return;
|
|
91
|
+
state.workspaceId = id;
|
|
92
|
+
save(LS.workspace, id);
|
|
93
|
+
emit({ type: "workspace" });
|
|
94
|
+
},
|
|
95
|
+
activeWorkspace() {
|
|
96
|
+
return state.workspaces.find((w) => w.workspace_id === state.workspaceId) || state.workspaces[0];
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
setUser(user) { state.user = { ...state.user, ...user }; emit({ type: "user" }); },
|
|
100
|
+
setIndexStatus(s) { state.indexStatus = s; emit({ type: "index" }); },
|
|
101
|
+
setRoute(route) { state.route = route; emit({ type: "route" }); },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
store.applyTheme();
|
|
105
|
+
|
|
106
|
+
// Follow OS theme changes while the user hasn't pinned a preference.
|
|
107
|
+
if (window.matchMedia) {
|
|
108
|
+
try {
|
|
109
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
|
110
|
+
if (!state.theme) emit({ type: "theme" });
|
|
111
|
+
});
|
|
112
|
+
} catch { /* legacy Safari */ }
|
|
113
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
* View: Audit Logs — Administration · activity and access trail.
|
|
3
|
+
* Reads /admin/audit (live) and renders unavailable state when it cannot load.
|
|
4
|
+
* Severity filter narrows the rendered events; a compact stat row summarizes
|
|
5
|
+
* actors, volume and risk at a glance.
|
|
6
|
+
* ========================================================================== */
|
|
7
|
+
|
|
8
|
+
import { timeAgo } from "../core/dom.a2773eb0.js";
|
|
9
|
+
|
|
10
|
+
const SEVERITY = {
|
|
11
|
+
warning: { variant: "warn", label: "Warning", icon: "alert-triangle" },
|
|
12
|
+
notice: { variant: "info", label: "Notice", icon: "info-circle" },
|
|
13
|
+
informational: { variant: "", label: "Informational", icon: "point" },
|
|
14
|
+
};
|
|
15
|
+
function severityMeta(s) {
|
|
16
|
+
return SEVERITY[String(s || "").toLowerCase()] || { variant: "", label: titleCase(s) || "Event", icon: "point" };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const FILTERS = [
|
|
20
|
+
{ key: "all", label: "All" },
|
|
21
|
+
{ key: "informational", label: "Informational" },
|
|
22
|
+
{ key: "notice", label: "Notice" },
|
|
23
|
+
{ key: "warning", label: "Warning" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export async function render(ctx) {
|
|
27
|
+
const { h, icon, api, c } = ctx;
|
|
28
|
+
|
|
29
|
+
const state = { events: [], source: "pending", filter: "all", loaded: false };
|
|
30
|
+
|
|
31
|
+
const srcSlot = h("span", c.sourceBadge("pending"));
|
|
32
|
+
const filterHost = h("div", buildTabs());
|
|
33
|
+
const statHost = h("div.lt3-statrow", c.loading({ lines: 1 }));
|
|
34
|
+
const tableHost = h("div", c.loading({ lines: 6 }));
|
|
35
|
+
|
|
36
|
+
const root = h("div.lt3-stack-6",
|
|
37
|
+
c.viewHeader({
|
|
38
|
+
eyebrow: "Administration",
|
|
39
|
+
title: "Audit Logs",
|
|
40
|
+
sub: "Activity and access trail",
|
|
41
|
+
actions: [
|
|
42
|
+
srcSlot,
|
|
43
|
+
h("button.lt3-btn.lt3-btn--ghost", {
|
|
44
|
+
on: { click: () => ctx.toast("Audit export is not available in this build (SIEM export is an Enterprise feature).", "warn") },
|
|
45
|
+
}, icon("download"), "Export"),
|
|
46
|
+
],
|
|
47
|
+
}),
|
|
48
|
+
statHost,
|
|
49
|
+
c.panel({
|
|
50
|
+
eyebrow: "Trail",
|
|
51
|
+
title: "Recent events",
|
|
52
|
+
head: h("div.lt3-row", { style: { "justify-content": "space-between", flex: "1 1 auto", gap: "var(--lt3-space-3)" } },
|
|
53
|
+
h("div", h("div.lt3-eyebrow", "Trail"), h("h3.lt3-panel__title", "Recent events")),
|
|
54
|
+
filterHost,
|
|
55
|
+
),
|
|
56
|
+
children: tableHost,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
function buildTabs() {
|
|
61
|
+
return c.tabs(FILTERS, state.filter, (key) => {
|
|
62
|
+
state.filter = key;
|
|
63
|
+
filterHost.replaceChildren(buildTabs());
|
|
64
|
+
renderTable();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function visibleEvents() {
|
|
69
|
+
if (state.filter === "all") return state.events;
|
|
70
|
+
return state.events.filter((e) => String(e.severity || "").toLowerCase() === state.filter);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderStats() {
|
|
74
|
+
const events = state.events;
|
|
75
|
+
const actors = new Set(events.map((e) => e.actor).filter(Boolean)).size;
|
|
76
|
+
const startOfDay = new Date(); startOfDay.setHours(0, 0, 0, 0);
|
|
77
|
+
const today = events.filter((e) => {
|
|
78
|
+
const t = e.ts ? new Date(e.ts).getTime() : NaN;
|
|
79
|
+
return !Number.isNaN(t) && t >= startOfDay.getTime();
|
|
80
|
+
}).length;
|
|
81
|
+
const high = events.filter((e) => ["warning", "high", "critical"].includes(String(e.severity || "").toLowerCase())).length;
|
|
82
|
+
statHost.replaceChildren(
|
|
83
|
+
c.stat({ label: "Total events", value: c.fmtNum(events.length), icon: "list-details" }),
|
|
84
|
+
c.stat({ label: "Actors", value: c.fmtNum(actors), icon: "users" }),
|
|
85
|
+
c.stat({ label: "Today", value: c.fmtNum(today), icon: "calendar-event" }),
|
|
86
|
+
c.stat({ label: "High-severity", value: c.fmtNum(high), icon: "shield-exclamation" }),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderTable() {
|
|
91
|
+
const rows = visibleEvents();
|
|
92
|
+
if (!rows.length) {
|
|
93
|
+
tableHost.replaceChildren(state.loaded
|
|
94
|
+
? c.emptyState({
|
|
95
|
+
icon: "history-off",
|
|
96
|
+
title: state.filter === "all" ? "No audit events" : "No matching events",
|
|
97
|
+
body: state.filter === "all"
|
|
98
|
+
? "Activity will appear here as users act in the workspace."
|
|
99
|
+
: "No events match this severity. Try a broader filter.",
|
|
100
|
+
action: state.filter === "all" ? null : h("button.lt3-btn.lt3-btn--subtle.lt3-btn--sm", {
|
|
101
|
+
on: { click: () => { state.filter = "all"; filterHost.replaceChildren(buildTabs()); renderTable(); } },
|
|
102
|
+
}, icon("filter-off"), "Clear filter"),
|
|
103
|
+
})
|
|
104
|
+
: c.loading({ lines: 6 }));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
tableHost.replaceChildren(c.table(columns(ctx), rows));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function load() {
|
|
111
|
+
const res = await api.adminAudit();
|
|
112
|
+
state.events = normalize(res.data);
|
|
113
|
+
state.source = res.source;
|
|
114
|
+
state.loaded = true;
|
|
115
|
+
srcSlot.replaceChildren(c.sourceBadge(res.source));
|
|
116
|
+
renderStats();
|
|
117
|
+
renderTable();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
load();
|
|
121
|
+
return root;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* ── table ───────────────────────────────────────────────────────────────── */
|
|
125
|
+
function columns({ h, icon, c }) {
|
|
126
|
+
return [
|
|
127
|
+
{
|
|
128
|
+
key: "ts", label: "Time", width: "1%",
|
|
129
|
+
render: (e) => h("span.lt3-mono.lt3-faint", { style: { "white-space": "nowrap", "font-size": "var(--lt3-text-2xs)" } },
|
|
130
|
+
e.ts ? timeAgo(e.ts) : "—"),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "actor", label: "Actor",
|
|
134
|
+
render: (e) => h("div.lt3-row-2",
|
|
135
|
+
h("span.lt3-avatar", { style: { width: "26px", height: "26px" } }, initials(e.actor)),
|
|
136
|
+
h("span", { style: { "font-size": "var(--lt3-text-sm)", "white-space": "nowrap" } }, e.actor || "system"),
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
key: "action", label: "Action", width: "1%",
|
|
141
|
+
render: (e) => h("span.lt3-pill.lt3-mono", { style: { "white-space": "nowrap" } }, e.action || "event"),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
key: "target", label: "Target",
|
|
145
|
+
render: (e) => h("span.lt3-faint", { style: { "font-size": "var(--lt3-text-sm)" } }, e.target || "—"),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
key: "severity", label: "Severity", width: "1%",
|
|
149
|
+
render: (e) => {
|
|
150
|
+
const m = severityMeta(e.severity);
|
|
151
|
+
return c.pill(m.label, m.variant, { dot: true });
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ── helpers ─────────────────────────────────────────────────────────────── */
|
|
158
|
+
function normalize(data) {
|
|
159
|
+
const list = Array.isArray(data) ? data
|
|
160
|
+
: Array.isArray(data && data.recent_events) ? data.recent_events
|
|
161
|
+
: Array.isArray(data && data.events) ? data.events
|
|
162
|
+
: [];
|
|
163
|
+
return list.map((e) => ({
|
|
164
|
+
ts: e.ts || e.timestamp || e.time || null,
|
|
165
|
+
actor: e.actor || e.user || e.email || "system",
|
|
166
|
+
action: e.action || e.event || "event",
|
|
167
|
+
target: e.target || e.resource || "",
|
|
168
|
+
severity: e.severity || e.level || "informational",
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function initials(name) {
|
|
173
|
+
const s = String(name || "·").trim();
|
|
174
|
+
if (!s || s === "system") return "SY";
|
|
175
|
+
const at = s.indexOf("@");
|
|
176
|
+
const base = at > 0 ? s.slice(0, at) : s;
|
|
177
|
+
const parts = base.split(/[\s._-]+/).filter(Boolean);
|
|
178
|
+
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
179
|
+
return base.slice(0, 2).toUpperCase();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function titleCase(s) {
|
|
183
|
+
s = String(s || "").trim();
|
|
184
|
+
return s ? s[0].toUpperCase() + s.slice(1).toLowerCase() : "";
|
|
185
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* ============================================================================
|
|
2
2
|
* View: Audit Logs — Administration · activity and access trail.
|
|
3
|
-
* Reads /admin/audit (live) and
|
|
3
|
+
* Reads /admin/audit (live) and renders unavailable state when it cannot load.
|
|
4
4
|
* Severity filter narrows the rendered events; a compact stat row summarizes
|
|
5
5
|
* actors, volume and risk at a glance.
|
|
6
6
|
* ========================================================================== */
|