@evomap/evolver-webui 2.0.0-beta.1 → 2.0.0-beta.4
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/dist/assetLineage.d.ts +70 -0
- package/dist/assetLineage.js +256 -0
- package/dist/console.d.ts +1 -1
- package/dist/console.js +103 -43
- package/dist/diagnosticSanitize.d.ts +2 -0
- package/dist/diagnosticSanitize.js +49 -0
- package/dist/eventSnapshot.d.ts +18 -0
- package/dist/eventSnapshot.js +149 -0
- package/dist/githubPrDiagnostics.d.ts +57 -0
- package/dist/githubPrDiagnostics.js +164 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/logDiagnostics.d.ts +18 -0
- package/dist/logDiagnostics.js +81 -0
- package/dist/observabilityRelations.d.ts +22 -0
- package/dist/observabilityRelations.js +93 -0
- package/dist/personalityDiagnostics.d.ts +41 -0
- package/dist/personalityDiagnostics.js +101 -0
- package/dist/server.d.ts +39 -3
- package/dist/server.js +234 -20
- package/package.json +7 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { assetstore, events as ev } from '@evomap/evolver-core';
|
|
2
|
+
import { type ObservabilityRelations } from './observabilityRelations.js';
|
|
3
|
+
export interface LineagePage<T> {
|
|
4
|
+
items: T[];
|
|
5
|
+
page: number;
|
|
6
|
+
pageSize: number;
|
|
7
|
+
hasMore: boolean;
|
|
8
|
+
truncated: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface LineageSource<T> {
|
|
11
|
+
available: boolean;
|
|
12
|
+
data?: T;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface AssetLineageRow {
|
|
16
|
+
assetId: string;
|
|
17
|
+
id: string;
|
|
18
|
+
type: assetstore.AssetKind;
|
|
19
|
+
category: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
}
|
|
22
|
+
interface AssetLineageEvent {
|
|
23
|
+
seq: number;
|
|
24
|
+
ts: string;
|
|
25
|
+
type: string;
|
|
26
|
+
cycleId: string;
|
|
27
|
+
title: string;
|
|
28
|
+
why: string;
|
|
29
|
+
trajectories: ObservabilityRelations['trajectories'];
|
|
30
|
+
pullRequests: ObservabilityRelations['pullRequests'];
|
|
31
|
+
}
|
|
32
|
+
export interface AssetLineageResponse {
|
|
33
|
+
id: string;
|
|
34
|
+
asset: LineageSource<AssetLineageRow | null>;
|
|
35
|
+
capsules: LineageSource<LineagePage<AssetLineageRow>>;
|
|
36
|
+
events: LineageSource<LineagePage<AssetLineageEvent>>;
|
|
37
|
+
review: LineageSource<{
|
|
38
|
+
state: string;
|
|
39
|
+
at: string;
|
|
40
|
+
by: string;
|
|
41
|
+
reason: string;
|
|
42
|
+
} | null>;
|
|
43
|
+
provenance: LineageSource<{
|
|
44
|
+
source: string;
|
|
45
|
+
trusted: boolean;
|
|
46
|
+
at: string;
|
|
47
|
+
promotedBy: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
} | null>;
|
|
50
|
+
relations: ObservabilityRelations;
|
|
51
|
+
}
|
|
52
|
+
export interface AssetLineageDeps {
|
|
53
|
+
store?: assetstore.AssetStoreProvider;
|
|
54
|
+
events: () => readonly ev.ReportEvent[] | Promise<readonly ev.ReportEvent[]>;
|
|
55
|
+
review?: assetstore.ReviewLedger;
|
|
56
|
+
provenance?: assetstore.ProvenanceStore;
|
|
57
|
+
}
|
|
58
|
+
export interface PageInput {
|
|
59
|
+
page?: number;
|
|
60
|
+
pageSize?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface AssetLineagePageInput extends PageInput {
|
|
63
|
+
capsulePage?: number;
|
|
64
|
+
capsulePageSize?: number;
|
|
65
|
+
eventPage?: number;
|
|
66
|
+
eventPageSize?: number;
|
|
67
|
+
}
|
|
68
|
+
export declare function listLineageAssets(store: assetstore.AssetStoreProvider | undefined, input: PageInput): Promise<LineageSource<LineagePage<AssetLineageRow>>>;
|
|
69
|
+
export declare function loadAssetLineage(deps: AssetLineageDeps, requestedId: string, input: AssetLineagePageInput): Promise<AssetLineageResponse>;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { assetstore, events as ev } from '@evomap/evolver-core';
|
|
2
|
+
import { eventListRelations, eventRelations } from './observabilityRelations.js';
|
|
3
|
+
import { redactDiagnosticText } from './diagnosticSanitize.js';
|
|
4
|
+
const DEFAULT_PAGE_SIZE = 20;
|
|
5
|
+
const MAX_PAGE_SIZE = 50;
|
|
6
|
+
const MAX_PAGE = 20;
|
|
7
|
+
const MAX_EVENT_SCAN = 5_000;
|
|
8
|
+
const MAX_ASSET_SCAN = 5_000;
|
|
9
|
+
const MAX_TEXT = 240;
|
|
10
|
+
function boundedPage(input) {
|
|
11
|
+
const requestedPage = Number.isFinite(input.page) ? Math.floor(input.page ?? 1) : 1;
|
|
12
|
+
const page = Math.min(Math.max(requestedPage, 1), MAX_PAGE);
|
|
13
|
+
const requestedSize = Number.isFinite(input.pageSize) ? Math.floor(input.pageSize ?? DEFAULT_PAGE_SIZE) : DEFAULT_PAGE_SIZE;
|
|
14
|
+
const pageSize = Math.min(Math.max(requestedSize, 1), MAX_PAGE_SIZE);
|
|
15
|
+
const offset = (page - 1) * pageSize;
|
|
16
|
+
return { page, pageSize, offset, limit: offset + pageSize + 1, truncated: requestedPage > MAX_PAGE };
|
|
17
|
+
}
|
|
18
|
+
function redactText(value) {
|
|
19
|
+
return redactDiagnosticText(value, MAX_TEXT).replace(/[\r\n\t]+/g, ' ').trim();
|
|
20
|
+
}
|
|
21
|
+
function idText(value) {
|
|
22
|
+
return redactText(value).slice(0, 160);
|
|
23
|
+
}
|
|
24
|
+
function lookupId(value) {
|
|
25
|
+
if (!value || value.length > 512)
|
|
26
|
+
return null;
|
|
27
|
+
for (const char of value) {
|
|
28
|
+
const code = char.codePointAt(0) ?? 0;
|
|
29
|
+
if (code < 0x20 || code === 0x7f)
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return value.trim() ? value : null;
|
|
33
|
+
}
|
|
34
|
+
function assetRow(record) {
|
|
35
|
+
return {
|
|
36
|
+
assetId: idText(record.asset_id),
|
|
37
|
+
id: idText(record['id'] ?? record.asset_id),
|
|
38
|
+
type: record.type,
|
|
39
|
+
category: redactText(record['category'] ?? record['intent']),
|
|
40
|
+
summary: redactText(record['summary']),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function pageItems(items, input, sourceTruncated = false) {
|
|
44
|
+
const page = boundedPage(input);
|
|
45
|
+
const hasMoreItems = items.length > page.offset + page.pageSize;
|
|
46
|
+
const pageLimitReached = page.page === MAX_PAGE && hasMoreItems;
|
|
47
|
+
return {
|
|
48
|
+
items: items.slice(page.offset, page.offset + page.pageSize),
|
|
49
|
+
page: page.page,
|
|
50
|
+
pageSize: page.pageSize,
|
|
51
|
+
hasMore: page.page < MAX_PAGE && hasMoreItems,
|
|
52
|
+
truncated: sourceTruncated || page.truncated || pageLimitReached,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function resolveAsset(store, id) {
|
|
56
|
+
const direct = await store.get(id);
|
|
57
|
+
if (direct)
|
|
58
|
+
return { asset: direct, ambiguous: false, truncated: false };
|
|
59
|
+
if (store.findByLogicalId) {
|
|
60
|
+
const matches = await store.findByLogicalId(id, 2);
|
|
61
|
+
return {
|
|
62
|
+
asset: matches.length === 1 ? matches[0] : null,
|
|
63
|
+
ambiguous: matches.length > 1,
|
|
64
|
+
truncated: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const records = await store.list(undefined, MAX_ASSET_SCAN + 1);
|
|
68
|
+
const truncated = records.length > MAX_ASSET_SCAN;
|
|
69
|
+
const matches = records.filter((record) => record['id'] === id);
|
|
70
|
+
if (matches.length > 1)
|
|
71
|
+
return { asset: null, ambiguous: true, truncated: false };
|
|
72
|
+
if (truncated)
|
|
73
|
+
return { asset: null, ambiguous: false, truncated: true };
|
|
74
|
+
return { asset: matches.length === 1 ? matches[0] : null, ambiguous: false, truncated: false };
|
|
75
|
+
}
|
|
76
|
+
function pageInput(page, pageSize, fallback) {
|
|
77
|
+
return { page: page ?? fallback.page, pageSize: pageSize ?? fallback.pageSize };
|
|
78
|
+
}
|
|
79
|
+
function eventMentions(event, ids) {
|
|
80
|
+
if (ids.size === 0)
|
|
81
|
+
return false;
|
|
82
|
+
const payload = event.payload ?? {};
|
|
83
|
+
for (const key of ['assetId', 'asset_id', 'geneId', 'gene', 'capsuleId', 'capsule_id']) {
|
|
84
|
+
const value = payload[key];
|
|
85
|
+
if (typeof value === 'string' && ids.has(value))
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
for (const key of ['genes', 'genesUsed', 'genes_used', 'assetIds', 'asset_ids']) {
|
|
89
|
+
const value = payload[key];
|
|
90
|
+
if (Array.isArray(value) && value.some((entry) => typeof entry === 'string' && ids.has(entry)))
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
function eventRow(event) {
|
|
96
|
+
return {
|
|
97
|
+
seq: Number.isFinite(event.seq) ? event.seq : 0,
|
|
98
|
+
ts: redactText(event.ts),
|
|
99
|
+
type: redactText(event.type),
|
|
100
|
+
cycleId: redactText(event.payload?.['cycleId']),
|
|
101
|
+
title: redactText(event.human?.title),
|
|
102
|
+
why: redactText(event.human?.why),
|
|
103
|
+
trajectories: eventRelations(event).trajectories,
|
|
104
|
+
pullRequests: eventRelations(event).pullRequests,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function unavailable(error) {
|
|
108
|
+
return { available: false, error };
|
|
109
|
+
}
|
|
110
|
+
export async function listLineageAssets(store, input) {
|
|
111
|
+
if (!store)
|
|
112
|
+
return unavailable('asset_store_unavailable');
|
|
113
|
+
const page = boundedPage(input);
|
|
114
|
+
try {
|
|
115
|
+
const records = await store.list(undefined, page.limit);
|
|
116
|
+
return { available: true, data: pageItems(records.map(assetRow), input) };
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return unavailable('asset_store_unavailable');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export async function loadAssetLineage(deps, requestedId, input) {
|
|
123
|
+
const requestedLookupId = lookupId(requestedId);
|
|
124
|
+
const id = idText(requestedId);
|
|
125
|
+
if (!requestedLookupId) {
|
|
126
|
+
return {
|
|
127
|
+
id,
|
|
128
|
+
asset: unavailable('asset_id_required'),
|
|
129
|
+
capsules: unavailable('asset_id_required'),
|
|
130
|
+
events: unavailable('asset_id_required'),
|
|
131
|
+
review: unavailable('asset_id_required'),
|
|
132
|
+
provenance: unavailable('asset_id_required'),
|
|
133
|
+
relations: { assets: [], trajectories: [], pullRequests: [] },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
let asset = null;
|
|
137
|
+
let ambiguousAsset = false;
|
|
138
|
+
let truncatedLookup = false;
|
|
139
|
+
let assetSource;
|
|
140
|
+
if (!deps.store) {
|
|
141
|
+
assetSource = unavailable('asset_store_unavailable');
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
try {
|
|
145
|
+
const resolved = await resolveAsset(deps.store, requestedLookupId);
|
|
146
|
+
asset = resolved.asset;
|
|
147
|
+
ambiguousAsset = resolved.ambiguous;
|
|
148
|
+
truncatedLookup = resolved.truncated;
|
|
149
|
+
if (ambiguousAsset)
|
|
150
|
+
assetSource = unavailable('asset_id_ambiguous');
|
|
151
|
+
else if (truncatedLookup)
|
|
152
|
+
assetSource = unavailable('asset_lookup_truncated');
|
|
153
|
+
else
|
|
154
|
+
assetSource = { available: true, data: asset ? assetRow(asset) : null };
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
assetSource = unavailable('asset_store_unavailable');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const resolutionError = ambiguousAsset ? 'asset_id_ambiguous' : truncatedLookup ? 'asset_lookup_truncated' : null;
|
|
161
|
+
if (resolutionError) {
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
asset: assetSource,
|
|
165
|
+
capsules: unavailable(resolutionError),
|
|
166
|
+
events: unavailable(resolutionError),
|
|
167
|
+
review: unavailable(resolutionError),
|
|
168
|
+
provenance: unavailable(resolutionError),
|
|
169
|
+
relations: { assets: [], trajectories: [], pullRequests: [] },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const logicalId = asset && typeof asset['id'] === 'string' ? asset['id'] : requestedLookupId;
|
|
173
|
+
const parentGene = asset?.type === 'Capsule' && typeof asset['gene'] === 'string' ? asset['gene'] : undefined;
|
|
174
|
+
const parentGeneRefs = new Set(parentGene ? [parentGene] : []);
|
|
175
|
+
if (parentGene && deps.store) {
|
|
176
|
+
try {
|
|
177
|
+
const resolvedParent = await resolveAsset(deps.store, parentGene);
|
|
178
|
+
if (resolvedParent.asset?.type === 'Gene') {
|
|
179
|
+
parentGeneRefs.add(resolvedParent.asset.asset_id);
|
|
180
|
+
if (typeof resolvedParent.asset['id'] === 'string')
|
|
181
|
+
parentGeneRefs.add(resolvedParent.asset['id']);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// The selected Capsule remains usable even if its parent cannot be expanded.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const ids = new Set([requestedLookupId, logicalId, ...parentGeneRefs, asset?.asset_id].filter((value) => typeof value === 'string' && value.length > 0));
|
|
189
|
+
const capsuleInput = pageInput(input.capsulePage, input.capsulePageSize, input);
|
|
190
|
+
const eventInput = pageInput(input.eventPage, input.eventPageSize, input);
|
|
191
|
+
let capsules;
|
|
192
|
+
if (!deps.store) {
|
|
193
|
+
capsules = unavailable('asset_store_unavailable');
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
try {
|
|
197
|
+
const page = boundedPage(capsuleInput);
|
|
198
|
+
const geneRefs = asset?.type === 'Capsule' ? [...parentGeneRefs] : [logicalId, asset?.asset_id];
|
|
199
|
+
const records = [];
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
for (const gene of geneRefs) {
|
|
202
|
+
if (!gene)
|
|
203
|
+
continue;
|
|
204
|
+
for (const record of await deps.store.search({ kind: 'Capsule', gene, limit: page.limit })) {
|
|
205
|
+
if (seen.has(record.asset_id))
|
|
206
|
+
continue;
|
|
207
|
+
seen.add(record.asset_id);
|
|
208
|
+
records.push(record);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
capsules = { available: true, data: pageItems(records.map(assetRow), capsuleInput) };
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
capsules = unavailable('asset_store_unavailable');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
let events;
|
|
218
|
+
let relations = { assets: [], trajectories: [], pullRequests: [] };
|
|
219
|
+
try {
|
|
220
|
+
const snapshot = await deps.events();
|
|
221
|
+
const start = Math.max(0, snapshot.length - MAX_EVENT_SCAN);
|
|
222
|
+
const matchedEvents = snapshot.slice(start).filter((event) => eventMentions(event, ids));
|
|
223
|
+
relations = eventListRelations(matchedEvents);
|
|
224
|
+
const matches = matchedEvents.reverse().map(eventRow);
|
|
225
|
+
events = { available: true, data: pageItems(matches, eventInput, start > 0) };
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
events = unavailable('event_snapshot_unavailable');
|
|
229
|
+
}
|
|
230
|
+
let review;
|
|
231
|
+
try {
|
|
232
|
+
if (ambiguousAsset)
|
|
233
|
+
throw new Error('asset_id_ambiguous');
|
|
234
|
+
const record = deps.review?.get(asset?.asset_id ?? requestedLookupId) ?? null;
|
|
235
|
+
review = { available: true, data: record ? {
|
|
236
|
+
state: redactText(record.state), at: redactText(record.at), by: redactText(record.by), reason: redactText(record.reason),
|
|
237
|
+
} : null };
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
review = unavailable(ambiguousAsset ? 'asset_id_ambiguous' : 'review_ledger_unavailable');
|
|
241
|
+
}
|
|
242
|
+
let provenance;
|
|
243
|
+
try {
|
|
244
|
+
if (ambiguousAsset)
|
|
245
|
+
throw new Error('asset_id_ambiguous');
|
|
246
|
+
const record = deps.provenance?.get(asset?.asset_id ?? requestedLookupId) ?? null;
|
|
247
|
+
provenance = { available: true, data: record ? {
|
|
248
|
+
source: redactText(record.source), trusted: record.trusted, at: redactText(record.at),
|
|
249
|
+
promotedBy: redactText(record.promotedBy), reason: redactText(record.reason),
|
|
250
|
+
} : null };
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
provenance = unavailable(ambiguousAsset ? 'asset_id_ambiguous' : 'provenance_unavailable');
|
|
254
|
+
}
|
|
255
|
+
return { id, asset: assetSource, capsules, events, review, provenance, relations };
|
|
256
|
+
}
|
package/dist/console.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** 3 栏主控台 HTML(自包含, 无构建步; 轮询 /api/* 渲染). 军杰 §9.5: Triggers|Timeline|Inspector. */
|
|
2
|
-
export declare const CONSOLE_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><title>Evolver \u63A7\u53F0</title>\n<style>\n*{box-sizing:border-box}body{margin:0;font:13px/1.5 ui-monospace,Menlo,monospace;background:#0d1117;color:#c9d1d9}\nheader{padding:8px 12px;background:#161b22;border-bottom:1px solid #30363d;display:flex;gap:16px;align-items:center}\nheader b{color:#58a6ff}.muted{color:#8b949e}\n.cols{display:grid;grid-template-columns:1fr 1fr 1.2fr;height:calc(100vh - 42px)}\n.col{overflow:auto;border-right:1px solid #30363d;padding:8px}\nh2{font-size:12px;text-transform:uppercase;color:#8b949e;margin:4px 0 8px;letter-spacing:.05em}\n.row{padding:6px 8px;border:1px solid #30363d;border-radius:6px;margin-bottom:6px;cursor:pointer}\n.row:hover{border-color:#58a6ff}.tag{display:inline-block;padding:0 6px;border-radius:10px;font-size:11px}\n.ok{background:#1a3d1a;color:#6fdd6f}.fail{background:#3d1a1a;color:#ff7b72}.warn{background:#3d3a1a;color:#e3b341}\n.act{display:flex;gap:6px;margin-top:8px}button{background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:4px 8px;cursor:pointer}\nbutton:hover{border-color:#58a6ff}pre{white-space:pre-wrap;word-break:break-all;background:#161b22;padding:8px;border-radius:6px}\n.valuecard{padding:10px 14px;background:#11161d;border-bottom:1px solid #30363d;display:flex;gap:24px;align-items:center;flex-wrap:wrap}\n.valuecard .big{font-size:18px;color:#6fdd6f;font-weight:600}.valuecard .est{color:#e3b341}.valuecard .lbl{color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.05em}\n</style></head><body>\n<header><b>EVOLVER</b> <span id=\"stat\" class=\"muted\">\u2026</span><span id=\"daily\" class=\"muted\"></span></header>\n<div class=\"valuecard\" id=\"valuecard\"><span class=\"muted\">\u4EF7\u503C / value \u2026</span></div>\n<div class=\"valuecard\" id=\"reviewqueue\"><span class=\"muted\">review queue \u2026</span></div>\n<div class=\"cols\">\n <div class=\"col\"><h2>Triggers</h2><div id=\"triggers\"></div></div>\n <div class=\"col\"><h2>Now & Timeline</h2><div id=\"cycles\"></div></div>\n <div class=\"col\"><h2>Inspector</h2><div id=\"inspector\" class=\"muted\">\u9009\u4E00\u4E2A cycle \u6216\u4E8B\u4EF6</div>\n <div class=\"act\"><button onclick=\"act('observe')\">Observe</button><button onclick=\"act('nudge')\">Nudge</button><button onclick=\"act('intervene')\">Intervene</button><button onclick=\"act('teach')\">Teach</button></div>\n </div>\n</div>\n<script>\nlet sel=null;\nconst TOK=new URLSearchParams(location.search).get('token')||'';\nasync function j(u,o){o=o||{};o.headers=Object.assign({},o.headers||{},{authorization:'Bearer '+TOK});const r=await fetch(u,o);return r.json()}\n// XSS guards (#200): server data (gene summaries / event titles / payloads) is built from untrusted material \u2014\n// never put it into innerHTML raw. esc() = HTML-text escape (numeric entities) for text positions; q() strips a\n// value to the content-addressed id charset for inline-handler string args so a malicious id cannot break the\n// attribute/JS string. Every dynamic value below goes through one of these before reaching innerHTML.\nconst esc=s=>String(s==null?'':s).replace(/[&<>\"']/g,c=>'&#'+c.charCodeAt(0)+';');\nconst q=s=>String(s==null?'':s).replace(/[^a-zA-Z0-9:_.-]/g,'');\nasync function refresh(){\n const s=await j('/api/status');document.getElementById('stat').textContent='events='+s.totalEvents+' cycles='+s.cycles;\n const d=await j('/api/daily-summary');document.getElementById('daily').textContent='\u4ECA\u65E5 '+d.solidified+'\u2713/'+d.failed+'\u2717 \u89E6\u53D1'+d.triggered;\n const v=await j('/api/value?window=7d');const nf=n=>Math.round(n||0).toLocaleString('en-US');\n const top=(v.topGenes||[]).slice(0,3).map(g=>esc(g.assetId)+' \u00D7'+esc(g.reuses)).join(' \u00B7 ')||'\u2014';\n document.getElementById('valuecard').innerHTML=\n '<div><div class=\"lbl\">measured saved (7d)</div><span class=\"big\">'+nf(v.totalTokensSaved)+' tokens</span> <span class=\"muted\">$'+(v.totalCostUsd||0).toFixed(4)+'</span></div>'\n +'<div><div class=\"lbl\">estimated</div><span class=\"est\">'+nf(v.estimated&&v.estimated.totalTokensSaved)+' tokens</span> <span class=\"muted\">(tracked separately)</span></div>'\n +'<div><div class=\"lbl\">top reused</div><span class=\"muted\">'+top+'</span></div>';\n const rv=await j('/api/review');const stag=s=>s==='quarantined'?'warn':s==='approved'?'ok':s==='rejected'?'fail':'muted';\n const pend=rv.filter(g=>g.state==='quarantined').length;\n const btns=g=>g.state==='quarantined'?' <button onclick=\"reviewAct(\\''+q(g.assetId)+'\\',\\'approve\\')\">Approve</button><button onclick=\"reviewAct(\\''+q(g.assetId)+'\\',\\'reject\\')\">Reject</button>':'';\n document.getElementById('reviewqueue').innerHTML='<div><div class=\"lbl\">review queue</div><span class=\"big\">'+pend+'</span> <span class=\"muted\">\u5F85\u5BA1 pending</span></div>'\n +rv.slice(0,8).map(g=>'<div><span class=\"tag '+stag(g.state)+'\">'+esc(g.state)+'</span> '+esc(g.geneId)+(g.autoDrafted?' <span class=\"muted\">auto</span>':'')+btns(g)+'</div>').join('');\n const tr=await j('/api/triggers');document.getElementById('triggers').innerHTML=tr.map(t=>'<div class=\"row\"><span class=\"tag '+(t.triggered?'ok':'warn')+'\">'+(t.triggered?'\u89E6\u53D1':'\u6291\u5236')+'</span> '+esc(t.patternId)+' <span class=\"muted\">v='+((t.value||0).toFixed?t.value.toFixed(2):esc(t.value))+'</span></div>').join('')||'<div class=\"muted\">\u65E0</div>';\n const cs=await j('/api/cycles');document.getElementById('cycles').innerHTML=cs.slice(-30).reverse().map(c=>'<div class=\"row\" onclick=\"showCycle(\\''+q(c.cycleId)+'\\')\"><span class=\"tag '+(c.finalStage==='solidified'?'ok':c.finalStage==='failed'?'fail':'warn')+'\">'+esc(c.finalStage)+'</span> '+esc(c.cycleId)+' <span class=\"muted\">('+esc(c.events)+')</span></div>').join('')||'<div class=\"muted\">\u65E0 cycle</div>';\n}\nasync function showCycle(id){sel=id;const t=await j('/api/cycle?id='+encodeURIComponent(id));\n document.getElementById('inspector').innerHTML='<b>'+esc(id)+'</b>'+t.timeline.map(e=>'<div class=\"row\">#'+esc(e.seq)+' <b>'+esc(e.type)+'</b><br>'+esc(e.title)+(e.why?'<br><span class=\"muted\">why: '+esc(e.why)+'</span>':'')+(e.payload?'<pre>'+esc(JSON.stringify(e.payload,null,1))+'</pre>':'')+'</div>').join('');\n}\nasync function reviewAct(gene,action){const reason=prompt(action+' \u7406\u7531 reason:');if(reason===null)return;\n try{const r=await j('/api/review',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({gene,action,reason})});\n if(!r||r.ok!==true){alert('review \u5931\u8D25 failed: '+((r&&r.error)||'unknown'));return;}}\n catch(e){alert('review \u5931\u8D25 failed: '+e);return;}\n refresh();}\nasync function act(kind){const note=prompt(kind+' \u5907\u6CE8:');if(note===null)return;\n await j('/api/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:kind,title:kind+(sel?(' @'+sel):''),note,cycleId:sel})});refresh();}\nrefresh();setInterval(refresh,4000);\n</script></body></html>";
|
|
2
|
+
export declare const CONSOLE_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><title>Evolver \u63A7\u53F0</title>\n<style>\n*{box-sizing:border-box}html{height:100%}body{min-height:100%;margin:0;font:13px/1.5 ui-monospace,Menlo,monospace;background:#0d1117;color:#c9d1d9;display:grid;grid-template-rows:auto auto auto auto auto minmax(320px,1fr);overflow:auto}\nheader{padding:8px 12px;background:#161b22;border-bottom:1px solid #30363d;display:flex;gap:16px;align-items:center}\nheader b{color:#58a6ff}.muted{color:#8b949e}\n.cols{display:grid;grid-template-columns:1fr 1fr 1.2fr;min-height:320px}\n.col{overflow:auto;border-right:1px solid #30363d;padding:8px}\nh2{font-size:12px;text-transform:uppercase;color:#8b949e;margin:4px 0 8px;letter-spacing:.05em}\n.row{padding:6px 8px;border:1px solid #30363d;border-radius:6px;margin-bottom:6px}\n.clickable{cursor:pointer}.clickable:hover,.row:hover{border-color:#58a6ff}.tag{display:inline-block;padding:0 6px;border-radius:10px;font-size:11px}\n.ok{background:#1a3d1a;color:#6fdd6f}.fail{background:#3d1a1a;color:#ff7b72}.warn{background:#3d3a1a;color:#e3b341}\n.act{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}button{background:#21262d;color:#c9d1d9;border:1px solid #30363d;border-radius:6px;padding:4px 8px;cursor:pointer}\nbutton:hover{border-color:#58a6ff}button:disabled{cursor:default;opacity:.5}pre{white-space:pre-wrap;word-break:break-word;background:#161b22;padding:8px;border-radius:6px}\na{color:#58a6ff;overflow-wrap:anywhere}.valuecard{padding:10px 14px;background:#11161d;border-bottom:1px solid #30363d;display:flex;gap:24px;align-items:center;flex-wrap:wrap}\n.valuecard .big{font-size:18px;color:#6fdd6f;font-weight:600}.valuecard .est{color:#e3b341}.valuecard .lbl{color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.05em}\n#reviewqueue{max-height:156px;overflow:auto;align-items:flex-start}\n#assetbar{max-height:190px;overflow:auto;align-items:flex-start}.assetgrid{display:flex;gap:6px;flex-wrap:wrap}.assetbutton{text-align:left;max-width:360px}.pager{display:flex;gap:6px;align-items:center}.lineage-section{margin-top:12px;scroll-margin-top:8px}.lineage-section h3{font-size:11px;color:#8b949e;text-transform:uppercase}.lineage-empty{color:#8b949e}\n.observability{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));background:#0d1117;border-bottom:1px solid #30363d}.surface{padding:10px 14px;min-height:150px;border-right:1px solid #30363d;border-top:1px solid #30363d;overflow:auto}.surface-head{display:flex;gap:8px;align-items:center;justify-content:space-between;margin-bottom:8px}.surface-body{max-height:260px;overflow:auto}.surface h3{margin:0;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:.05em}.metricgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:6px}.metric{padding:6px;background:#161b22;border-radius:6px}.metric b{display:block;color:#8b949e;font-size:11px}.logview{max-height:220px;overflow:auto;margin:0}.relation-list{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}.relation-card{padding:6px 8px;border:1px solid #30363d;border-radius:6px;min-width:180px}.section-nav{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0}\n@media(max-width:760px){body{display:block;overflow:auto}.cols,.observability{display:block}.col{border-right:0;border-bottom:1px solid #30363d;min-height:240px}#reviewqueue{max-height:none}.surface{border-right:0}}\n</style></head><body>\n<header><b>EVOLVER</b> <span id=\"stat\" class=\"muted\">\u2026</span><span id=\"daily\" class=\"muted\"></span></header>\n<div class=\"valuecard\" id=\"valuecard\"><span class=\"muted\">\u4EF7\u503C / value \u2026</span></div>\n<div class=\"valuecard\" id=\"reviewqueue\"><span class=\"muted\">review queue \u2026</span></div>\n<div class=\"valuecard\" id=\"retentionbar\"><span class=\"muted\">retention health \u2026</span></div>\n<div class=\"diagnostics\">\n <div class=\"valuecard\" id=\"assetbar\"><span class=\"muted\">asset lineage \u2026</span></div>\n <div class=\"observability\" aria-label=\"deep observability\">\n <section class=\"surface\"><div class=\"surface-head\"><h3>Personality</h3><span id=\"personality-status\" class=\"muted\">\u2026</span></div><div id=\"personality\" class=\"surface-body\"></div></section>\n <section class=\"surface\"><div class=\"surface-head\"><h3>MemoryGraph</h3><span id=\"memorygraph-status\" class=\"muted\">\u2026</span></div><div id=\"memorygraph\" class=\"surface-body\"></div></section>\n <section class=\"surface\"><div class=\"surface-head\"><h3>Logs</h3><span id=\"logs-status\" class=\"muted\">\u2026</span></div><div id=\"logs\" class=\"surface-body\"></div></section>\n <section class=\"surface\"><div class=\"surface-head\"><h3>GitHub PR status</h3><span id=\"githubprs-status\" class=\"muted\">\u2026</span></div><div id=\"githubprs\" class=\"surface-body\"></div></section>\n </div>\n</div>\n<div class=\"cols\">\n <div class=\"col\"><h2>Triggers</h2><div id=\"triggers\"></div></div>\n <div class=\"col\"><h2>Now & Timeline</h2><div id=\"cycles\"></div></div>\n <div class=\"col\"><h2>Inspector</h2><div id=\"inspector\" class=\"muted\">\u9009\u4E00\u4E2A cycle \u6216\u4E8B\u4EF6</div>\n <div class=\"act\"><button id=\"act-observe\">Observe</button><button id=\"act-nudge\">Nudge</button><button id=\"act-intervene\">Intervene</button><button id=\"act-teach\">Teach</button></div>\n </div>\n</div>\n<script>\nlet sel=null,assetPage=1,lineageCapsulePage=1,lineageEventPage=1,lineageId='';\nconst FRAG=new URLSearchParams(location.hash.slice(1));\nconst QUERY=new URLSearchParams(location.search);\nlet STORED='';try{STORED=sessionStorage.getItem('evolver.dashboard.token')||''}catch{}\nconst TOK=FRAG.get('token')||QUERY.get('token')||STORED;\nif(TOK)try{sessionStorage.setItem('evolver.dashboard.token',TOK)}catch{}\nif(FRAG.has('token')||QUERY.has('token')){QUERY.delete('token');const query=QUERY.toString();history.replaceState(null,'',location.pathname+(query?'?'+query:''))}\nasync function j(url,options){options=options||{};options.headers=Object.assign({},options.headers||{});if(TOK)options.headers.authorization='Bearer '+TOK;options.credentials='same-origin';const response=await fetch(url,options);return response.json()}\n// Kept for compatibility with older embedded-console extensions. New observability surfaces use DOM text nodes only.\nconst esc=value=>String(value==null?'':value).replace(/[&<>\"']/g,char=>'&#'+char.charCodeAt(0)+';');\nconst q=value=>String(value==null?'':value).replace(/[^a-zA-Z0-9:_.-]/g,'');\nconst nf=value=>Math.round(Number(value)||0).toLocaleString('en-US');\nconst el=(tag,text,cls)=>{const node=document.createElement(tag);if(text!=null)node.textContent=String(text);if(cls)node.className=cls;return node};\nconst clear=node=>{while(node.firstChild)node.removeChild(node.firstChild)};\nconst appendLine=(parent,label,value)=>{const row=el('div');row.append(el('b',label+': '),document.createTextNode(value==null||value===''?'\u2014':String(value)));parent.append(row)};\nconst asArray=value=>Array.isArray(value)?value:[];\nfunction safeJson(value){try{return JSON.stringify(value,null,1)}catch{return '[unavailable]'}}\nfunction sourceSection(parent,title,source,render,id){const section=el('section',null,'lineage-section');if(id)section.id=id;section.append(el('h3',title));\n if(!source||source.available!==true){section.append(el('div',(source&&source.error)||'unavailable','tag warn'));parent.append(section);return;}\n render(section,source.data);parent.append(section);\n}\nfunction setSurfaceStatus(name,text,cls){const status=document.getElementById(name+'-status');status.textContent=text;status.className=cls||'muted'}\nfunction surfaceFailure(name,message){setSurfaceStatus(name,'unavailable','tag warn');const root=document.getElementById(name);clear(root);root.append(el('div',message||'unavailable','muted'))}\nfunction githubUrl(value){try{const url=new URL(String(value));const host=url.hostname.toLowerCase();if(url.protocol!=='https:'||(host!=='github.com'&&!host.endsWith('.github.com'))||url.port)return null;url.username='';url.password='';url.search='';url.hash='';return url.href}catch{return null}}\nfunction externalLink(url,label){const valid=githubUrl(url);if(!valid)return null;const link=el('a',label||valid);link.href=valid;link.target='_blank';link.rel='noopener noreferrer';return link}\nfunction relationItems(value){if(Array.isArray(value))return value;if(value&&value.available===true){if(Array.isArray(value.data))return value.data;if(value.data&&Array.isArray(value.data.items))return value.data.items}if(value&&Array.isArray(value.items))return value.items;return []}\nfunction relationLabel(value){if(value==null)return 'relation';if(typeof value!=='object')return String(value);const fields=['title','type','id','trajectoryId','traceId','sessionId','number','state'];const parts=[];for(const field of fields){if(value[field]!=null&&value[field]!=='')parts.push(field+' '+String(value[field]))}return parts.join(' \u00B7 ')||'relation'}\nfunction renderRelationCards(parent,items,kind){const list=el('div',null,'relation-list');for(const item of items){const card=el('div',null,'relation-card');if(kind==='asset'){\n const assetId=String(item&&item.assetId||'');if(!assetId)continue;const button=el('button',relationLabel(item));button.addEventListener('click',()=>void showAsset(assetId,1,1));card.append(button);\n }else if(kind==='pr'){\n const link=externalLink(item&&item.url,relationLabel(item));card.append(link||el('span',relationLabel(item)));\n }else card.append(el('span',relationLabel(item)));\n list.append(card)}if(list.childNodes.length)parent.append(list)}\nfunction renderOptionalRelations(parent,relations,scope){if(!relations)return;const assets=relationItems(relations.assets);const trajectories=relationItems(relations.trajectories);const prs=relationItems(relations.pullRequests);\n if(assets.length)sourceSection(parent,'related assets',{available:true,data:assets},(section,data)=>renderRelationCards(section,data,'asset'));\n if(trajectories.length)sourceSection(parent,'trajectories',{available:true,data:trajectories},(section,data)=>renderRelationCards(section,data,'trajectory'),scope?scope+'-trajectories':undefined);\n if(prs.length)sourceSection(parent,'related pull requests',{available:true,data:prs},(section,data)=>renderRelationCards(section,data,'pr'));\n}\nasync function refreshStatus(){try{const status=await j('/api/status');document.getElementById('stat').textContent='events='+status.totalEvents+' cycles='+status.cycles}catch{document.getElementById('stat').textContent='status unavailable'}}\nasync function refreshDaily(){try{const daily=await j('/api/daily-summary');document.getElementById('daily').textContent='\u4ECA\u65E5 '+daily.solidified+'\u2713/'+daily.failed+'\u2717 \u89E6\u53D1'+daily.triggered}catch{document.getElementById('daily').textContent='daily unavailable'}}\nasync function refreshValue(){const root=document.getElementById('valuecard');try{const value=await j('/api/value?window=7d');clear(root);\n const measured=el('div');measured.append(el('div','measured saved (7d)','lbl'),el('span',nf(value.totalTokensSaved)+' tokens','big'),document.createTextNode(' '),el('span','$'+Number(value.totalCostUsd||0).toFixed(4),'muted'));\n const estimated=el('div');estimated.append(el('div','estimated','lbl'),el('span',nf(value.estimated&&value.estimated.totalTokensSaved)+' tokens','est'),document.createTextNode(' '),el('span','(tracked separately)','muted'));\n const top=el('div');top.append(el('div','top reused','lbl'));const topText=asArray(value.topGenes).slice(0,3).map(g=>String(g.assetId)+' \u00D7'+String(g.reuses)).join(' \u00B7 ')||'\u2014';top.append(el('span',topText,'muted'));root.append(measured,estimated,top);\n }catch{clear(root);root.append(el('span','value unavailable','tag warn'))}}\nasync function refreshReview(){const root=document.getElementById('reviewqueue');try{const review=await j('/api/review');clear(root);const pending=asArray(review).filter(item=>item.state==='quarantined').length;const summary=el('div');summary.append(el('div','review queue','lbl'),el('span',pending,'big'),document.createTextNode(' '),el('span','\u5F85\u5BA1 pending','muted'));root.append(summary);\n const stateClass=state=>state==='quarantined'?'warn':state==='approved'?'ok':state==='rejected'?'fail':'muted';for(const item of asArray(review).slice(0,8)){const row=el('div');row.append(el('span',item.state,'tag '+stateClass(item.state)),document.createTextNode(' '),el('span',item.geneId));if(item.autoDrafted)row.append(document.createTextNode(' '),el('span','auto','muted'));\n if(item.state==='quarantined'){const assetId=String(item.assetId||'');const approve=el('button','Approve');const reject=el('button','Reject');approve.addEventListener('click',()=>void reviewAct(assetId,'approve'));reject.addEventListener('click',()=>void reviewAct(assetId,'reject'));row.append(document.createTextNode(' '),approve,reject)}root.append(row)}\n }catch{clear(root);root.append(el('span','review unavailable','tag warn'))}}\nasync function refreshTriggers(){const root=document.getElementById('triggers');try{const triggers=await j('/api/triggers');clear(root);for(const trigger of asArray(triggers)){const row=el('div',null,'row');row.append(el('span',trigger.triggered?'\u89E6\u53D1':'\u6291\u5236','tag '+(trigger.triggered?'ok':'warn')),document.createTextNode(' '+String(trigger.patternId)+' '));const value=Number(trigger.value);row.append(el('span','v='+(Number.isFinite(value)?value.toFixed(2):String(trigger.value)),'muted'));root.append(row)}if(!root.firstChild)root.append(el('div','\u65E0','muted'))}catch{clear(root);root.append(el('div','triggers unavailable','muted'))}}\nasync function refreshCycles(){const root=document.getElementById('cycles');try{const cycles=await j('/api/cycles');clear(root);for(const cycle of asArray(cycles).slice(-30).reverse()){const cycleId=String(cycle.cycleId||'');const row=el('div',null,'row clickable');const stageClass=cycle.finalStage==='solidified'?'ok':cycle.finalStage==='failed'?'fail':'warn';row.append(el('span',cycle.finalStage,'tag '+stageClass),document.createTextNode(' '+String(cycle.cycleId)+' '),el('span','('+String(cycle.events)+')','muted'));row.addEventListener('click',()=>void showCycle(cycleId));root.append(row)}if(!root.firstChild)root.append(el('div','\u65E0 cycle','muted'))}catch{clear(root);root.append(el('div','cycles unavailable','muted'))}}\nfunction refresh(){void refreshStatus();void refreshDaily();void refreshValue();void refreshReview();void refreshTriggers();void refreshCycles()}\nasync function refreshRetention(){const root=document.getElementById('retentionbar');try{const retention=await j('/api/retention');clear(root);if(!retention||retention.available!==true){root.append(el('span','retention unavailable','muted'));return;}\n const safe=!!(retention.rootEvents&&retention.rootEvents.archiveRotationSafe&&retention.material&&retention.material.archiveRotationSafe);const summary=el('div');summary.append(el('div','retention health','lbl'),el('span',safe?'safe':'attention','tag '+(safe?'ok':'warn')),document.createTextNode(' '),el('span','warnings '+asArray(retention.warnings).length,'muted'));root.append(summary);\n const rootEvents=el('div');rootEvents.append(el('div','root events \u00B7 '+String(retention.rootEvents.state),'lbl'),el('span',nf(retention.rootEvents.records),'big'),document.createTextNode(' '),el('span','active \u00B7 '+nf(retention.rootEvents.archiveRecords)+' archive \u00B7 '+nf(retention.rootEvents.historyRecords)+' history','muted'));root.append(rootEvents);\n const material=el('div');material.append(el('div','material \u00B7 '+String(retention.material.state),'lbl'),el('span',nf(retention.material.records),'big'),document.createTextNode(' '),el('span','active \u00B7 '+nf(retention.material.archiveRecords)+' archive \u00B7 '+nf(retention.material.historyRecords)+' history \u00B7 '+nf(retention.material.pending)+' pending \u00B7 cursor '+String(retention.material.cursor),'muted'));root.append(material);\n }catch{clear(root);root.append(el('span','retention unavailable','tag warn'))}}\nasync function refreshAssets(){const root=document.getElementById('assetbar');try{const source=await j('/api/asset-lineage/assets?page='+assetPage+'&pageSize=12');clear(root);const title=el('div');title.append(el('div','asset lineage','lbl'));root.append(title);\n if(!source||source.available!==true){root.append(el('span',(source&&source.error)||'asset store unavailable','tag warn'));return;}const grid=el('div',null,'assetgrid');for(const asset of source.data.items){const assetId=String(asset.assetId||'');const button=el('button',asset.type+' \u00B7 '+asset.id+(asset.summary?' \u00B7 '+asset.summary:''),'assetbutton');button.addEventListener('click',()=>void showAsset(assetId,1,1));grid.append(button)}if(source.data.items.length===0)grid.append(el('span','no assets','muted'));root.append(grid);\n const pager=el('div',null,'pager');const previous=el('button','Previous');previous.disabled=source.data.page<=1;previous.addEventListener('click',()=>{assetPage=Math.max(1,assetPage-1);void refreshAssets()});const next=el('button','Next');next.disabled=!source.data.hasMore;next.addEventListener('click',()=>{assetPage+=1;void refreshAssets()});pager.append(previous,el('span','page '+source.data.page+(source.data.truncated?' \u00B7 bounded':''),'muted'),next);root.append(pager);\n }catch{clear(root);root.append(el('span','asset store unavailable','tag warn'))}}\nfunction renderPersonalityRecords(parent,title,records){if(!Array.isArray(records)||records.length===0)return;const section=el('section',null,'lineage-section');section.append(el('h3',title));for(const record of records.slice(-8).reverse()){const row=el('div',null,'row');if(record&&typeof record==='object'){for(const key of ['at','updatedAt','key','success','fail','avgScore','n','outcome','score'])if(record[key]!=null)appendLine(row,key,record[key]);if(!row.firstChild)row.append(el('span',safeJson(record)))}else row.append(el('span',record));section.append(row)}parent.append(section)}\nasync function refreshPersonality(){const root=document.getElementById('personality');try{const response=await j('/api/personality');if(!response||response.available!==true){surfaceFailure('personality',(response&&response.error)||'personality unavailable');return;}clear(root);setSurfaceStatus('personality','live','tag ok');const data=response.data||{};const current=data.current&&typeof data.current==='object'?data.current:{};const grid=el('div',null,'metricgrid');for(const key of Object.keys(current).slice(0,5)){const metric=el('div',null,'metric');metric.append(el('b',key),el('span',current[key]));grid.append(metric)}if(!grid.firstChild)grid.append(el('span','no current profile','muted'));root.append(grid);appendLine(root,'updated',data.updatedAt);renderPersonalityRecords(root,'stats',data.stats);renderPersonalityRecords(root,'history',data.history)}catch{surfaceFailure('personality','personality unavailable')}}\nasync function refreshMemoryGraph(){const root=document.getElementById('memorygraph');try{const response=await j('/api/memory-graph');if(!response||response.available!==true){surfaceFailure('memorygraph',(response&&response.error)||'memory graph unavailable');return;}clear(root);const recovery=String(response.recovery||'degraded');const gate=recovery==='healthy'?'ok':recovery==='degraded'?'fail':'warn';setSurfaceStatus('memorygraph',recovery,'tag '+gate);appendLine(root,'selection',response.selectionReason||'no scoped selection yet');appendLine(root,'compacted records',nf(response.compactedRecords));appendLine(root,'active records',nf(response.activeRecords));appendLine(root,'archives',nf(response.archives));appendLine(root,'corrupt lines',nf(response.corruptLines));appendLine(root,'oversized lines',nf(response.oversizedLines));appendLine(root,'oversized files',nf(response.oversizedFiles))}catch{surfaceFailure('memorygraph','memory graph unavailable')}}\nasync function refreshLogs(){const root=document.getElementById('logs');try{const response=await j('/api/logs');if(!response||response.available!==true){surfaceFailure('logs',(response&&response.error)||'logs unavailable');return;}clear(root);setSurfaceStatus('logs',response.data&&response.data.truncated?'bounded':'live',response.data&&response.data.truncated?'tag warn':'tag ok');const pre=el('pre',null,'logview');pre.append(document.createTextNode(asArray(response.data&&response.data.lines).join('\\n')));root.append(pre)}catch{surfaceFailure('logs','logs unavailable')}}\nfunction checksText(checks){checks=checks||{};return 'checks '+Number(checks.passed||0)+' passing \u00B7 '+Number(checks.failed||0)+' failing \u00B7 '+Number(checks.pending||0)+' pending / '+Number(checks.total||0)}\nasync function refreshGitHubPrs(){const root=document.getElementById('githubprs');try{const response=await j('/api/github-prs');if(!response||response.available!==true){surfaceFailure('githubprs',(response&&response.error)||'GitHub PR status unavailable');return;}clear(root);setSurfaceStatus('githubprs',response.data&&response.data.truncated?'bounded':'live',response.data&&response.data.truncated?'tag warn':'tag ok');const items=asArray(response.data&&response.data.prs);for(const item of items){const row=el('div',null,'row');const title='#'+String(item.number)+' '+String(item.title||'');const link=externalLink(item.url,title);row.append(link||el('span',title));const state=String(item.state||'unknown')+(item.isDraft?' \u00B7 draft':'');appendLine(row,'state',state);appendLine(row,'branch',String(item.head||'\u2014')+' \u2192 '+String(item.base||'\u2014'));appendLine(row,'review',item.reviewDecision);appendLine(row,'updated',item.updatedAt);row.append(el('div',checksText(item.checks),'muted'));root.append(row)}if(!root.firstChild)root.append(el('div','no pull requests','muted'))}catch{surfaceFailure('githubprs','GitHub PR status unavailable')}}\nfunction renderRows(section,page,fields){if(!page||!Array.isArray(page.items)||page.items.length===0){section.append(el('div','none','lineage-empty'));return;}for(const item of page.items){const row=el('div',null,'row');for(const field of fields)appendLine(row,field[0],item[field[1]]);section.append(row)}if(page.truncated)section.append(el('div','source window bounded for dashboard safety','tag warn'))}\nfunction renderEventRows(section,page){if(!page||!Array.isArray(page.items)||page.items.length===0){section.append(el('div','none','lineage-empty'));return;}for(const item of page.items){const row=el('div',null,'row');for(const field of [['seq','seq'],['type','type'],['time','ts'],['title','title'],['why','why']])appendLine(row,field[0],item[field[1]]);const cycleId=String(item.cycleId||'');if(cycleId){const button=el('button','Open cycle '+String(item.cycleId));button.addEventListener('click',()=>void showCycle(cycleId));row.append(button)}section.append(row)}if(page.truncated)section.append(el('div','source window bounded for dashboard safety','tag warn'))}\nfunction lineagePager(parent,label,page,data,onPage){const pager=el('div',null,'pager');const previous=el('button','Previous');previous.disabled=page<=1;previous.addEventListener('click',()=>onPage(page-1));const next=el('button','Next');next.disabled=!data||!data.hasMore;next.addEventListener('click',()=>onPage(page+1));pager.append(el('span',label,'muted'),previous,el('span','page '+page,'muted'),next);parent.append(pager)}\nfunction inspectorNav(parent,targetId,label){const button=el('button',label);button.addEventListener('click',()=>{const target=document.getElementById(targetId);if(target)target.scrollIntoView({block:'start'})});parent.append(button)}\nasync function showAsset(id,capsulePage,eventPage){lineageId=id;lineageCapsulePage=capsulePage||1;lineageEventPage=eventPage||1;sel=null;const root=document.getElementById('inspector');clear(root);root.append(el('span','loading asset lineage \u2026','muted'));try{const response=await j('/api/asset-lineage?id='+encodeURIComponent(id)+'&capsulePage='+lineageCapsulePage+'&eventPage='+lineageEventPage+'&pageSize=20');const relations=response.relations||{trajectories:response.trajectories,pullRequests:response.pullRequests};clear(root);root.append(el('h2','Asset lineage'));const nav=el('div',null,'section-nav');inspectorNav(nav,'asset-source','Source');inspectorNav(nav,'asset-review','Review');if(relationItems(relations.trajectories).length)inspectorNav(nav,'asset-trajectories','Trajectory');root.append(nav);\n sourceSection(root,'asset',response.asset,(section,data)=>{if(!data){section.append(el('div','asset not found','tag warn'));return;}appendLine(section,'type',data.type);appendLine(section,'id',data.id);appendLine(section,'asset',data.assetId);appendLine(section,'category',data.category);appendLine(section,'summary',data.summary)});\n sourceSection(root,'review',response.review,(section,data)=>{if(!data){section.append(el('div','default eligible / no record','lineage-empty'));return;}appendLine(section,'state',data.state);appendLine(section,'at',data.at);appendLine(section,'by',data.by);appendLine(section,'reason',data.reason)},'asset-review');\n sourceSection(root,'provenance / source',response.provenance,(section,data)=>{if(!data){section.append(el('div','local default / no record','lineage-empty'));return;}appendLine(section,'source',data.source);appendLine(section,'trusted',data.trusted);appendLine(section,'at',data.at);appendLine(section,'promoted by',data.promotedBy);appendLine(section,'reason',data.reason)},'asset-source');\n sourceSection(root,'capsules',response.capsules,(section,data)=>renderRows(section,data,[['type','type'],['id','id'],['asset','assetId'],['summary','summary']]));sourceSection(root,'events',response.events,(section,data)=>renderEventRows(section,data));renderOptionalRelations(root,relations,'asset');lineagePager(root,'capsules',lineageCapsulePage,response.capsules&&response.capsules.data,page=>showAsset(lineageId,page,lineageEventPage));lineagePager(root,'events',lineageEventPage,response.events&&response.events.data,page=>showAsset(lineageId,lineageCapsulePage,page));\n }catch{clear(root);root.append(el('span','asset lineage unavailable','tag warn'))}}\nasync function showCycle(id){sel=id;const root=document.getElementById('inspector');clear(root);root.append(el('span','loading cycle \u2026','muted'));try{const response=await j('/api/cycle?id='+encodeURIComponent(id));const relations=response.relations;clear(root);root.append(el('h2','Cycle '+String(id)));if(relationItems(relations&&relations.trajectories).length){const nav=el('div',null,'section-nav');inspectorNav(nav,'cycle-trajectories','Trajectory');root.append(nav)}renderOptionalRelations(root,relations,'cycle');for(const event of asArray(response.timeline)){const row=el('div',null,'row');const heading=el('div');heading.append(document.createTextNode('#'+String(event.seq)+' '),el('b',event.type));row.append(heading);if(event.title)row.append(el('div',event.title));if(event.why)appendLine(row,'why',event.why);if(event.payload!=null){const pre=el('pre');pre.append(document.createTextNode(safeJson(event.payload)));row.append(pre)}root.append(row)}if(!asArray(response.timeline).length)root.append(el('div','no timeline events','muted'))}catch{clear(root);root.append(el('span','cycle unavailable','tag warn'))}}\nasync function reviewAct(gene,action){const reason=prompt(action+' \u7406\u7531 reason:');if(reason===null)return;try{const response=await j('/api/review',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({gene,action,reason})});if(!response||response.ok!==true){alert('review \u5931\u8D25 failed: '+((response&&response.error)||'unknown'));return;}}catch(error){alert('review \u5931\u8D25 failed: '+error);return;}refresh()}\nasync function act(kind){const note=prompt(kind+' \u5907\u6CE8:');if(note===null)return;await j('/api/action',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:kind,title:kind+(sel?(' @'+sel):''),note,cycleId:sel})});refresh()}\nfor(const kind of ['observe','nudge','intervene','teach'])document.getElementById('act-'+kind).addEventListener('click',()=>void act(kind));\nrefresh();void refreshRetention();void refreshAssets();void refreshPersonality();void refreshMemoryGraph();void refreshLogs();void refreshGitHubPrs();\nsetInterval(refresh,4000);setInterval(refreshRetention,60000);setInterval(refreshAssets,30000);setInterval(refreshPersonality,30000);setInterval(refreshMemoryGraph,30000);setInterval(refreshLogs,10000);setInterval(refreshGitHubPrs,60000);\n</script></body></html>";
|