@ibgib/space-gib 0.0.3 → 0.0.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.
Files changed (62) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/IMPLEMENTATION.md +9 -13
  3. package/dist/client/bootstrap.mjs +1 -1
  4. package/dist/client/bootstrap.mjs.map +1 -1
  5. package/dist/client/chunk-NCXKCVYS.mjs +42 -0
  6. package/dist/client/chunk-NCXKCVYS.mjs.map +7 -0
  7. package/dist/client/chunk-ZUEU37Z5.mjs +1920 -0
  8. package/dist/client/chunk-ZUEU37Z5.mjs.map +7 -0
  9. package/dist/client/index.html +103 -5
  10. package/dist/client/index.mjs +1 -1
  11. package/dist/client/script.mjs +1 -1
  12. package/dist/client/style.css +466 -61
  13. package/dist/respec-gib.node.mjs +5 -0
  14. package/dist/server/server.mjs +294 -225
  15. package/dist/server/server.mjs.map +2 -2
  16. package/package.json +6 -6
  17. package/src/client/AUTO-GENERATED-version.mts +1 -1
  18. package/src/client/components/identity-header/IMPLEMENTATION.md +45 -0
  19. package/src/client/components/identity-header/identity-header.css +74 -0
  20. package/src/client/components/identity-header/identity-header.html +10 -0
  21. package/src/client/components/identity-header/identity-header.mts +361 -0
  22. package/src/client/components/identity-manager/IMPLEMENTATION.md +100 -0
  23. package/src/client/components/identity-manager/identity-manager.css +467 -0
  24. package/src/client/components/identity-manager/identity-manager.html +113 -0
  25. package/src/client/components/identity-manager/identity-manager.mts +767 -0
  26. package/src/client/components/keystone-creator/keystone-creator.css +2 -76
  27. package/src/client/components/keystone-creator/keystone-creator.html +41 -26
  28. package/src/client/components/keystone-creator/keystone-creator.mts +178 -41
  29. package/src/client/dev-tools/base-tools.mts +252 -0
  30. package/src/client/dev-tools/common.mts +217 -0
  31. package/src/client/dev-tools/phase-1.mts +156 -0
  32. package/src/client/dev-tools/phase-2.mts +143 -0
  33. package/src/client/dev-tools/phase-3.mts +189 -0
  34. package/src/client/dev-tools/phase-4-1.mts +197 -0
  35. package/src/client/dev-tools/phase-4-10.mts +884 -0
  36. package/src/client/dev-tools/phase-4-2.mts +388 -0
  37. package/src/client/dev-tools/phase-4-3.mts +391 -0
  38. package/src/client/dev-tools/phase-4-4.mts +374 -0
  39. package/src/client/dev-tools/phase-4-5.mts +376 -0
  40. package/src/client/dev-tools/phase-4-6.mts +273 -0
  41. package/src/client/dev-tools/phase-4-7.mts +399 -0
  42. package/src/client/dev-tools/phase-4-8.mts +430 -0
  43. package/src/client/dev-tools/phase-4-9.mts +398 -0
  44. package/src/client/dev-tools/phase-4.mts +1302 -0
  45. package/src/client/dev-tools.mts +52 -1194
  46. package/src/client/index.html +103 -5
  47. package/src/client/style.css +466 -61
  48. package/src/client/ui/shell/space-gib-shell-constants.mts +0 -2
  49. package/src/client/ui/shell/space-gib-shell-service.mts +82 -10
  50. package/src/common/common-constants.mts +0 -0
  51. package/src/common/keystone-policies.json +40 -43
  52. package/src/common/keystone-policies.mts +3 -5
  53. package/src/server/path-helper.respec.mts +99 -94
  54. package/src/server/serve-gib/README.md +9 -0
  55. package/src/server/serve-gib/handlers/api/keystone/keystone-genesis.handler.mts +1 -1
  56. package/src/server/serve-gib/handlers/api/keystone/keystone-get.respec.mts +1 -1
  57. package/src/server/serve-gib/handlers/ws/sync-upgrade-handler-base.mts +31 -3
  58. package/src/server/serve-gib/handlers/ws/ws-helper.mts +73 -45
  59. package/dist/client/chunk-2KJC5XKE.mjs +0 -31
  60. package/dist/client/chunk-2KJC5XKE.mjs.map +0 -7
  61. package/dist/client/chunk-QNIXTRFO.mjs +0 -235
  62. package/dist/client/chunk-QNIXTRFO.mjs.map +0 -7
@@ -0,0 +1,767 @@
1
+ import styleCss from "../../style.css";
2
+ import thisCss from "./identity-manager.css";
3
+ import thisHtml from "./identity-manager.html";
4
+
5
+ import { extractErrorMsg, pretty } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
6
+ import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs";
7
+ import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
8
+ import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
9
+ import { ROOT_ADDR } from "@ibgib/ts-gib/dist/V1/constants.mjs";
10
+ import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs";
11
+ import { KeystoneIbGib_V1 } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
12
+ import {
13
+ IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase,
14
+ } from "@ibgib/web-gib/dist/ui/component/ibgib-dynamic-component-bases.mjs";
15
+ import {
16
+ ElementsBase, IbGibDynamicComponentInstance,
17
+ IbGibDynamicComponentInstanceInitOpts,
18
+ } from "@ibgib/web-gib/dist/ui/component/component-types.mjs";
19
+ import { SettingsType } from "@ibgib/web-gib/dist/common/settings/settings-constants.mjs";
20
+ import { Settings_General } from "@ibgib/web-gib/dist/common/settings/settings-types.mjs";
21
+ import { getComponentSvc } from "@ibgib/web-gib/dist/ui/component/ibgib-component-service.mjs";
22
+ import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED } from "@ibgib/web-gib/dist/ui/ui-constants.mjs";
23
+
24
+ import { getComponentCtorArg, getIbGibGlobalThis_SpaceGib } from "../../helpers.web.mjs";
25
+ import { APP_CONFIG } from "../../constants.mjs";
26
+ import { devLog } from "../../dev-tools.mjs";
27
+
28
+ export const IDENTITY_MANAGER_COMPONENT_NAME = 'ibgib-identity-manager';
29
+
30
+ /**
31
+ * Metadata for the Identity Manager component.
32
+ */
33
+ export class IdentityManagerComponentMeta extends IbGibDynamicComponentMetaBase {
34
+ protected lc: string = `[IdentityManagerComponentMeta]`;
35
+
36
+ routeRegExp?: RegExp = new RegExp(`^${IDENTITY_MANAGER_COMPONENT_NAME}$`);
37
+ componentName = IDENTITY_MANAGER_COMPONENT_NAME;
38
+
39
+ constructor() {
40
+ super(getComponentCtorArg());
41
+ if (!customElements.get(this.componentName)) {
42
+ customElements.define(this.componentName, IdentityManagerComponentInstance);
43
+ }
44
+ }
45
+
46
+ async createInstance({
47
+ path,
48
+ ibGibAddr
49
+ }: {
50
+ path: string;
51
+ ibGibAddr: IbGibAddr;
52
+ }): Promise<IbGibDynamicComponentInstance> {
53
+ const lc = `${this.lc}[${this.createInstance.name}]`;
54
+ const component = document.createElement(this.componentName) as IdentityManagerComponentInstance;
55
+ await component.initialize({
56
+ ibGibAddr,
57
+ meta: this,
58
+ html: thisHtml,
59
+ css: [styleCss, thisCss],
60
+ });
61
+ return component;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * References to DOM elements within the component's shadow root.
67
+ */
68
+ interface IdentityManagerElements extends ElementsBase {
69
+ containerEl: HTMLDivElement;
70
+ titleEl: HTMLHeadingElement;
71
+ tabsListEl: HTMLDivElement;
72
+ btnAddIdentityEl: HTMLButtonElement;
73
+ noIdentityPlaceholderEl: HTMLDivElement;
74
+ btnCreatePlaceholderEl: HTMLButtonElement;
75
+ identityDetailsViewEl: HTMLDivElement;
76
+ identityAddrEl: HTMLElement;
77
+ identityGenEl: HTMLElement;
78
+ identityTimestampEl: HTMLElement;
79
+ identityUuidEl: HTMLElement;
80
+ identityNameEl: HTMLElement;
81
+ identityDescriptionEl: HTMLElement;
82
+ identityFrameDetailsEl: HTMLPreElement;
83
+ identityAggrDetailsEl: HTMLPreElement;
84
+
85
+ // Scrubber elements
86
+ btnScrubFirstEl: HTMLButtonElement;
87
+ btnScrubPrev10El: HTMLButtonElement;
88
+ btnScrubPrevEl: HTMLButtonElement;
89
+ scrubberStatusEl: HTMLSpanElement;
90
+ btnScrubNextEl: HTMLButtonElement;
91
+ btnScrubNext10El: HTMLButtonElement;
92
+ btnScrubLatestEl: HTMLButtonElement;
93
+
94
+ poolsContainerEl: HTMLDivElement;
95
+ btnVerifyChainEl: HTMLButtonElement;
96
+ // btnSyncIdentityEl: HTMLButtonElement;
97
+ btnSetActiveEl: HTMLButtonElement;
98
+ verificationStatusEl: HTMLDivElement;
99
+ replicasListEl: HTMLDivElement;
100
+ // inputReplicaUrlEl: HTMLInputElement;
101
+ // btnAddReplicaEl: HTMLButtonElement;
102
+ rawAccordionHeaderEl: HTMLDivElement;
103
+ rawAccordionContentEl: HTMLDivElement;
104
+ rawKeystoneJsonEl: HTMLPreElement;
105
+ }
106
+
107
+ /**
108
+ * Concrete implementation of the Identity Manager component.
109
+ */
110
+ export class IdentityManagerComponentInstance
111
+ extends IbGibDynamicComponentInstanceBase<IbGib_V1, IdentityManagerElements>
112
+ implements IbGibDynamicComponentInstance<IbGib_V1, IdentityManagerElements> {
113
+
114
+ protected lc: string = `[IdentityManagerComponentInstance]`;
115
+
116
+ private activeAddr: string | null = null;
117
+ private globalActiveAddr: string | null = null;
118
+ private _onIdentityChanged: ((e: any) => void) | null = null;
119
+ private history: any[] = [];
120
+ private currentFrameIndex: number = -1;
121
+ private lastLoadedHistoryAddr: string | null = null;
122
+ private keystoneCache: Map<string, { username: string, description: string }> = new Map();
123
+
124
+ constructor() {
125
+ super();
126
+ }
127
+
128
+ private async getKeystoneDetails(addr: string): Promise<{ username: string, description: string }> {
129
+ const cached = this.keystoneCache.get(addr);
130
+ if (cached) return cached;
131
+
132
+ const metaspace = getIbGibGlobalThis_SpaceGib(APP_CONFIG).metaspace;
133
+ if (!metaspace) return { username: '', description: '' };
134
+ const space = await metaspace.getLocalUserSpace({ lock: false });
135
+ if (!space) return { username: '', description: '' };
136
+
137
+ try {
138
+ const res = await metaspace.get({ addr, space });
139
+ if (res.success && res.ibGibs && res.ibGibs.length > 0) {
140
+ const keystone = res.ibGibs[0] as KeystoneIbGib_V1;
141
+ const keystoneSvc = new KeystoneService_V1();
142
+ const aggregated = await keystoneSvc.getAggregateDetails({
143
+ latestKeystone: keystone,
144
+ metaspace,
145
+ space
146
+ });
147
+ const username = aggregated?.username || aggregated?.profile?.name || aggregated?.name || '';
148
+ const description = aggregated?.description || aggregated?.profile?.description || '';
149
+ const details = { username, description };
150
+ this.keystoneCache.set(addr, details);
151
+ return details;
152
+ }
153
+ } catch (err) {
154
+ console.warn(`Error getting details for keystone ${addr}: ${extractErrorMsg(err)}`);
155
+ }
156
+
157
+ return { username: '', description: '' };
158
+ }
159
+
160
+ override async initialize(opts: IbGibDynamicComponentInstanceInitOpts): Promise<void> {
161
+ await super.initialize(opts);
162
+ }
163
+
164
+ override async created(): Promise<void> {
165
+ this.elements = {} as IdentityManagerElements;
166
+
167
+ const shadow = this.shadowRoot!;
168
+ this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
169
+ this.elements.titleEl = shadow.getElementById('title') as HTMLHeadingElement;
170
+ this.elements.tabsListEl = shadow.getElementById('identity-tabs') as HTMLDivElement;
171
+ this.elements.btnAddIdentityEl = shadow.getElementById('btn-add-identity') as HTMLButtonElement;
172
+ this.elements.noIdentityPlaceholderEl = shadow.getElementById('no-identity-placeholder') as HTMLDivElement;
173
+ this.elements.btnCreatePlaceholderEl = shadow.getElementById('btn-create-placeholder') as HTMLButtonElement;
174
+ this.elements.identityDetailsViewEl = shadow.getElementById('identity-details-view') as HTMLDivElement;
175
+ this.elements.identityAddrEl = shadow.getElementById('identity-addr') as HTMLElement;
176
+ this.elements.identityGenEl = shadow.getElementById('identity-gen') as HTMLSpanElement;
177
+ this.elements.identityTimestampEl = shadow.getElementById('identity-timestamp') as HTMLSpanElement;
178
+ this.elements.identityUuidEl = shadow.getElementById('identity-uuid') as HTMLSpanElement;
179
+ this.elements.identityNameEl = shadow.getElementById('identity-name') as HTMLElement;
180
+ this.elements.identityDescriptionEl = shadow.getElementById('identity-description') as HTMLElement;
181
+ this.elements.identityFrameDetailsEl = shadow.getElementById('identity-frame-details') as HTMLPreElement;
182
+ this.elements.identityAggrDetailsEl = shadow.getElementById('identity-aggr-details') as HTMLPreElement;
183
+
184
+ // Scrubber elements
185
+ this.elements.btnScrubFirstEl = shadow.getElementById('btn-scrub-first') as HTMLButtonElement;
186
+ this.elements.btnScrubPrev10El = shadow.getElementById('btn-scrub-prev10') as HTMLButtonElement;
187
+ this.elements.btnScrubPrevEl = shadow.getElementById('btn-scrub-prev') as HTMLButtonElement;
188
+ this.elements.scrubberStatusEl = shadow.getElementById('scrubber-status') as HTMLSpanElement;
189
+ this.elements.btnScrubNextEl = shadow.getElementById('btn-scrub-next') as HTMLButtonElement;
190
+ this.elements.btnScrubNext10El = shadow.getElementById('btn-scrub-next10') as HTMLButtonElement;
191
+ this.elements.btnScrubLatestEl = shadow.getElementById('btn-scrub-latest') as HTMLButtonElement;
192
+
193
+ this.elements.poolsContainerEl = shadow.getElementById('pools-container') as HTMLDivElement;
194
+ this.elements.btnVerifyChainEl = shadow.getElementById('btn-verify-chain') as HTMLButtonElement;
195
+ // this.elements.btnSyncIdentityEl = shadow.getElementById('btn-sync-identity') as HTMLButtonElement;
196
+ this.elements.btnSetActiveEl = shadow.getElementById('btn-set-active') as HTMLButtonElement;
197
+ this.elements.verificationStatusEl = shadow.getElementById('verification-status') as HTMLDivElement;
198
+ this.elements.replicasListEl = shadow.getElementById('replicas-list') as HTMLDivElement;
199
+ // this.elements.inputReplicaUrlEl = shadow.getElementById('input-replica-url') as HTMLInputElement;
200
+ // this.elements.btnAddReplicaEl = shadow.getElementById('btn-add-replica') as HTMLButtonElement;
201
+ this.elements.rawAccordionHeaderEl = shadow.getElementById('raw-accordion-header') as HTMLDivElement;
202
+ this.elements.rawAccordionContentEl = shadow.getElementById('raw-accordion-content') as HTMLDivElement;
203
+ this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json') as HTMLPreElement;
204
+
205
+ // ElementBase requires contentEl (points to the main container/body area)
206
+ this.elements.contentEl = shadow.getElementById('identity-manager-content') as HTMLDivElement;
207
+
208
+ this.initHandlers();
209
+
210
+ // Subscribe to global active identity changes
211
+ this._onIdentityChanged = (e: any) => {
212
+ this.globalActiveAddr = e.detail?.activeIdentityAddr || null;
213
+ this.renderUI();
214
+ };
215
+ window.addEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
216
+
217
+ if (this.ibGibAddr === ROOT_ADDR || !this.ibGib) {
218
+ await this.loadKeystonesIndex();
219
+ } else {
220
+ await this.initSettings();
221
+ }
222
+ await this.renderUI();
223
+ }
224
+
225
+ override async disconnected(): Promise<void> {
226
+ if (this._onIdentityChanged) {
227
+ window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
228
+ }
229
+ }
230
+
231
+ private async loadKeystonesIndex(): Promise<void> {
232
+ const lc = `${this.lc}[loadKeystonesIndex]`;
233
+ try {
234
+ const metaspace = getIbGibGlobalThis_SpaceGib(APP_CONFIG).metaspace;
235
+ if (!metaspace) { return; }
236
+ const space = await metaspace.getLocalUserSpace({ lock: false });
237
+ if (!space) { return; }
238
+ const keystonesIndex = await metaspace.getSpecialIbGib({
239
+ type: "keystones",
240
+ space,
241
+ initialize: true,
242
+ });
243
+ if (keystonesIndex) {
244
+ this.ibGibAddr = getIbGibAddr({ ibGib: keystonesIndex });
245
+ await this.loadIbGib();
246
+ await this.initSettings();
247
+ }
248
+ } catch (error) {
249
+ console.error(`${lc} ${extractErrorMsg(error)}`);
250
+ }
251
+ }
252
+
253
+
254
+
255
+ private initHandlers() {
256
+ // Setup raw JSON accordion toggle
257
+ this.elements!.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
258
+
259
+ // Buttons
260
+ this.elements!.btnAddIdentityEl.addEventListener('click', () => this.handleAddNewIdentityFlow());
261
+ this.elements!.btnCreatePlaceholderEl.addEventListener('click', () => this.handleAddNewIdentityFlow());
262
+ this.elements!.btnVerifyChainEl.addEventListener('click', () => this.handleVerifyIdentityChain());
263
+ // this.elements!.btnSyncIdentityEl.addEventListener('click', () => this.handleSyncIdentity());
264
+ this.elements!.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
265
+ // this.elements!.btnAddReplicaEl.addEventListener('click', () => this.handleAddReplicaSpace());
266
+
267
+ // Scrubber Buttons
268
+ this.elements!.btnScrubFirstEl.addEventListener('click', () => this.scrubFirst());
269
+ this.elements!.btnScrubPrev10El.addEventListener('click', () => this.scrubPrev10());
270
+ this.elements!.btnScrubPrevEl.addEventListener('click', () => this.scrubPrev());
271
+ this.elements!.btnScrubNextEl.addEventListener('click', () => this.scrubNext());
272
+ this.elements!.btnScrubNext10El.addEventListener('click', () => this.scrubNext10());
273
+ this.elements!.btnScrubLatestEl.addEventListener('click', () => this.scrubLatest());
274
+
275
+ // Convert vertical mouse wheel scrolling into horizontal scroll for tabs
276
+ this.elements!.tabsListEl.addEventListener('wheel', (e: WheelEvent) => {
277
+ if (e.deltaY !== 0) {
278
+ e.preventDefault();
279
+ // this.elements!.tabsListEl.scrollLeft += e.deltaY;
280
+ this.elements!.tabsListEl.scrollBy({ left: e.deltaY, behavior: 'smooth' });
281
+ }
282
+ }, { passive: false });
283
+ }
284
+
285
+ private toggleAccordion() {
286
+ const header = this.elements!.rawAccordionHeaderEl;
287
+ const content = this.elements!.rawAccordionContentEl;
288
+ header.classList.toggle('expanded');
289
+ content.classList.toggle('expanded');
290
+ }
291
+
292
+ protected async handleAddNewIdentityFlow() {
293
+ const lc = `${this.lc}[${this.handleAddNewIdentityFlow.name}]`;
294
+ devLog(`${lc} Triggering identity creation/import flow...`);
295
+ try {
296
+ const componentSvc = await getComponentSvc();
297
+ const creator = await componentSvc.getComponentInstance({
298
+ path: 'ibgib-keystone-creator',
299
+ ibGibAddr: ROOT_ADDR,
300
+ useRegExpPrefilter: true,
301
+ });
302
+ if (creator) {
303
+ // Hide placeholder and details panel
304
+ this.elements!.noIdentityPlaceholderEl.classList.add('hidden');
305
+ this.elements!.identityDetailsViewEl.classList.add('hidden');
306
+
307
+ // Clear any previous creator
308
+ const existing = this.elements!.contentEl.querySelector('ibgib-keystone-creator');
309
+ if (existing) { existing.remove(); }
310
+
311
+ this.elements!.contentEl.appendChild(creator as any);
312
+ devLog(`${lc} Mounted ibgib-keystone-creator successfully.`);
313
+ } else {
314
+ throw new Error("Could not find meta for ibgib-keystone-creator");
315
+ }
316
+ } catch (error) {
317
+ console.error(`${lc} ${extractErrorMsg(error)}`);
318
+ this.setVerificationStatus(`Failed to load creator: ${extractErrorMsg(error)}`, "error");
319
+ }
320
+ }
321
+
322
+ protected async handleVerifyIdentityChain() {
323
+ const lc = `${this.lc}[${this.handleVerifyIdentityChain.name}]`;
324
+ devLog(`${lc} Verifying identity keystone timeline...`);
325
+ this.setVerificationStatus("Verification request sent...", "info");
326
+ try {
327
+ // Skeleton logic placeholder
328
+ this.setVerificationStatus("Verification complete. Root Keystone is verified (Genesis phase 1).", "success");
329
+ } catch (error) {
330
+ this.setVerificationStatus(`Verification failed: ${extractErrorMsg(error)}`, "error");
331
+ }
332
+ }
333
+
334
+ // leave this in
335
+ // protected async handleSyncIdentity() {
336
+ // const lc = `${this.lc}[${this.handleSyncIdentity.name}]`;
337
+ // devLog(`${lc} Starting Peer-to-Peer sync saga...`);
338
+ // this.setVerificationStatus("Starting synchronization session...", "info");
339
+ // }
340
+
341
+ // leave this in
342
+ // protected async handleAddReplicaSpace() {
343
+ // const lc = `${this.lc}[${this.handleAddReplicaSpace.name}]`;
344
+ // const url = this.elements!.inputReplicaUrlEl.value;
345
+ // if (!url) {
346
+ // this.setVerificationStatus("Please enter a valid space replication URL.", "error");
347
+ // return;
348
+ // }
349
+ // devLog(`${lc} Registering replica space: ${url}`);
350
+
351
+ // // Add to UI list for verification
352
+ // const replicaItem = document.createElement('div');
353
+ // replicaItem.className = 'address-code';
354
+ // replicaItem.style.marginTop = '0.5rem';
355
+ // replicaItem.textContent = url;
356
+ // this.elements!.replicasListEl.appendChild(replicaItem);
357
+
358
+ // this.elements!.inputReplicaUrlEl.value = '';
359
+ // this.setVerificationStatus(`Registered replica endpoint: ${url}`, "success");
360
+ // }
361
+
362
+ /**
363
+ * dispatches the event to set {@link activeAddr} (if truthy) as the active
364
+ * identity. IOW, this "logs it in".
365
+ */
366
+ protected async handleSetActiveIdentity() {
367
+ if (!this.activeAddr) { return; }
368
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, {
369
+ detail: { activeIdentityAddr: this.activeAddr },
370
+ bubbles: true,
371
+ composed: true
372
+ }));
373
+ }
374
+
375
+ private setVerificationStatus(msg: string, type: 'info' | 'success' | 'error') {
376
+ const statusEl = this.elements!.verificationStatusEl;
377
+ statusEl.textContent = msg;
378
+ statusEl.className = `status-msg ${type}`;
379
+ }
380
+
381
+ private async viewIdentityDetails(addr: string): Promise<void> {
382
+ this.activeAddr = addr;
383
+ await this.renderUI();
384
+ }
385
+
386
+ private async resolveActiveIdentity() {
387
+ const lc = `${this.lc}[resolveActiveIdentity]`;
388
+ try {
389
+ if (!this.settings) {
390
+ this.activeAddr = null;
391
+ this.globalActiveAddr = null;
392
+ return;
393
+ }
394
+ const generalSettings = await this.getSettings<Settings_General>({
395
+ settingsType: SettingsType.general,
396
+ useCase: 'current',
397
+ });
398
+ const activeAddrInSettings = (generalSettings as any)?.activeIdentityAddr;
399
+
400
+ const registeredKeystones = this.ibGib?.rel8ns?.keystone ?? [];
401
+ if (activeAddrInSettings && registeredKeystones.includes(activeAddrInSettings)) {
402
+ this.globalActiveAddr = activeAddrInSettings;
403
+ } else if (registeredKeystones.length > 0) {
404
+ this.globalActiveAddr = registeredKeystones[0];
405
+ } else {
406
+ this.globalActiveAddr = null;
407
+ }
408
+
409
+ if (!this.activeAddr) {
410
+ this.activeAddr = this.globalActiveAddr;
411
+ }
412
+ } catch (error) {
413
+ console.error(`${lc} ${extractErrorMsg(error)}`);
414
+ this.globalActiveAddr = null;
415
+ }
416
+ }
417
+
418
+ protected override async renderUI(): Promise<void> {
419
+ const lc = `${this.lc}[${this.renderUI.name}]`;
420
+ if (!this.elements) { return; }
421
+
422
+ // always resolve the current active identity, even if we aren't
423
+ // necessarily rendering the details for that identity (if it isn't
424
+ // this.activeAddr)
425
+ await this.resolveActiveIdentity();
426
+
427
+ const keystones = this.ibGib?.rel8ns?.keystone ?? [];
428
+
429
+ if (keystones.length === 0) {
430
+ this.elements.tabsListEl.innerHTML = '';
431
+ this.elements.noIdentityPlaceholderEl.classList.remove('hidden');
432
+ this.elements.identityDetailsViewEl.classList.add('hidden');
433
+
434
+ const existing = this.elements.contentEl.querySelector('ibgib-keystone-creator');
435
+ if (existing && !this.elements.noIdentityPlaceholderEl.classList.contains('hidden')) {
436
+ existing.remove();
437
+ }
438
+ return; /* <<<< returns early */
439
+ }
440
+
441
+ this.elements.noIdentityPlaceholderEl.classList.add('hidden');
442
+
443
+ // Render Tabs
444
+ this.elements.tabsListEl.innerHTML = '';
445
+ for (const addr of keystones) {
446
+ const tabEl = document.createElement('button');
447
+ tabEl.className = `tab-btn ${addr === this.activeAddr ? 'active' : ''} ${addr === this.globalActiveAddr ? 'global-active' : ''}`;
448
+
449
+ // Initial fallback label and tooltip
450
+ let displayTab = addr;
451
+ if (displayTab.length > 12) {
452
+ displayTab = displayTab.substring(0, 5) + '...' + displayTab.substring(displayTab.length - 4);
453
+ }
454
+ tabEl.textContent = displayTab;
455
+ tabEl.title = addr;
456
+
457
+ // Fetch details and update tab text/tooltip
458
+ const details = await this.getKeystoneDetails(addr);
459
+ let name = details.username;
460
+ const maxTabLen = 16;
461
+ if (name) {
462
+ if (name.length > maxTabLen) {
463
+ name = name.substring(0, maxTabLen - 3) + '...';
464
+ }
465
+ tabEl.textContent = name;
466
+ }
467
+
468
+ const tooltipParts: string[] = [];
469
+ if (details.username) {
470
+ tooltipParts.push(`User: ${details.username}`);
471
+ }
472
+ if (details.description) {
473
+ const desc = details.description.length > 100
474
+ ? details.description.substring(0, 97) + '...'
475
+ : details.description;
476
+ tooltipParts.push(`Desc: ${desc}`);
477
+ }
478
+ tooltipParts.push(`Addr: ${addr}`);
479
+ tabEl.title = tooltipParts.join('\n');
480
+
481
+ tabEl.addEventListener('click', async () => {
482
+ await this.viewIdentityDetails(addr);
483
+ });
484
+ this.elements.tabsListEl.appendChild(tabEl);
485
+ }
486
+
487
+ if (this.activeAddr) {
488
+ const existing = this.elements.contentEl.querySelector('ibgib-keystone-creator');
489
+ if (existing) { existing.remove(); }
490
+
491
+ this.elements.identityDetailsViewEl.classList.remove('hidden');
492
+
493
+ // Update Set Active button state
494
+ if (this.activeAddr === this.globalActiveAddr) {
495
+ this.elements.btnSetActiveEl.disabled = true;
496
+ this.elements.btnSetActiveEl.textContent = "Active Identity";
497
+ this.elements.btnSetActiveEl.classList.add('active-primary');
498
+ } else {
499
+ this.elements.btnSetActiveEl.disabled = false;
500
+ this.elements.btnSetActiveEl.textContent = "Set as Active Identity";
501
+ this.elements.btnSetActiveEl.classList.remove('active-primary');
502
+ }
503
+
504
+ if (this.activeAddr !== this.lastLoadedHistoryAddr) {
505
+ await this.loadHistory(this.activeAddr);
506
+ this.lastLoadedHistoryAddr = this.activeAddr;
507
+ }
508
+
509
+ await this.updateFrameDetailsView();
510
+ } else {
511
+ this.elements.identityDetailsViewEl.classList.add('hidden');
512
+ this.clearActiveDetails();
513
+ }
514
+ }
515
+
516
+ private clearActiveDetails() {
517
+ if (!this.elements) return;
518
+ this.elements.identityGenEl.textContent = '-';
519
+ this.elements.identityTimestampEl.textContent = '-';
520
+ this.elements.identityUuidEl.textContent = '-';
521
+ this.elements.identityNameEl.textContent = '-';
522
+ this.elements.identityDescriptionEl.textContent = '-';
523
+ this.elements.identityFrameDetailsEl.textContent = '{}';
524
+ this.elements.identityAggrDetailsEl.textContent = '{}';
525
+ this.elements.scrubberStatusEl.textContent = 'Frame - / -';
526
+ this.elements.poolsContainerEl.innerHTML = '';
527
+ this.history = [];
528
+ this.currentFrameIndex = -1;
529
+ }
530
+
531
+ private async loadHistory(tipAddr: string): Promise<void> {
532
+ const lc = `${this.lc}[loadHistory]`;
533
+ this.history = [];
534
+ this.currentFrameIndex = -1;
535
+
536
+ const metaspace = getIbGibGlobalThis_SpaceGib(APP_CONFIG).metaspace;
537
+ if (!metaspace) return;
538
+ const space = await metaspace.getLocalUserSpace({ lock: false });
539
+ if (!space) return;
540
+
541
+ let currentAddr: string | undefined = tipAddr;
542
+ const tempHistory: KeystoneIbGib_V1[] = [];
543
+
544
+ while (currentAddr) {
545
+ const res = await metaspace.get({ addr: currentAddr, space });
546
+ if (!res.success || !res.ibGibs || res.ibGibs.length === 0) {
547
+ console.warn(`${lc} Could not find frame at ${currentAddr}`);
548
+ break;
549
+ }
550
+ const keystone = res.ibGibs[0] as KeystoneIbGib_V1;
551
+ tempHistory.push(keystone);
552
+
553
+ if (keystone.data?.n === 0) {
554
+ break;
555
+ }
556
+
557
+ const past = keystone.rel8ns?.past;
558
+ currentAddr = (past && past.length > 0) ? past[past.length - 1] : undefined;
559
+ }
560
+
561
+ this.history = tempHistory.reverse();
562
+ this.currentFrameIndex = this.history.length - 1;
563
+ }
564
+
565
+ private scrubTo(index: number) {
566
+ if (this.history.length === 0) return;
567
+ this.currentFrameIndex = Math.max(0, Math.min(index, this.history.length - 1));
568
+ this.updateFrameDetailsView();
569
+ }
570
+
571
+ private scrubFirst() {
572
+ this.scrubTo(0);
573
+ }
574
+
575
+ private scrubPrev10() {
576
+ this.scrubTo(this.currentFrameIndex - 10);
577
+ }
578
+
579
+ private scrubPrev() {
580
+ this.scrubTo(this.currentFrameIndex - 1);
581
+ }
582
+
583
+ private scrubNext() {
584
+ this.scrubTo(this.currentFrameIndex + 1);
585
+ }
586
+
587
+ private scrubNext10() {
588
+ this.scrubTo(this.currentFrameIndex + 10);
589
+ }
590
+
591
+ private scrubLatest() {
592
+ this.scrubTo(this.history.length - 1);
593
+ }
594
+
595
+ private async updateFrameDetailsView(): Promise<void> {
596
+ const lc = `${this.lc}[updateFrameDetailsView]`;
597
+ if (!this.elements) return;
598
+
599
+ if (this.history.length === 0 || this.currentFrameIndex < 0 || this.currentFrameIndex >= this.history.length) {
600
+ this.clearActiveDetails();
601
+ return;
602
+ }
603
+
604
+ const keystone = this.history[this.currentFrameIndex];
605
+ const keystoneAddr = getIbGibAddr({ ibGib: keystone });
606
+
607
+ // Update Verifiable Address
608
+ this.elements.identityAddrEl.textContent = keystoneAddr;
609
+
610
+ // Update Generation
611
+ this.elements.identityGenEl.textContent = keystone.data?.n !== undefined ? String(keystone.data.n) : '0 (Genesis)';
612
+
613
+ // Update Timestamp
614
+ const timestampStr = keystone.data?.timestamp;
615
+ let formattedTimestamp = 'N/A';
616
+ if (timestampStr) {
617
+ const parsed = parseInt(timestampStr);
618
+ if (!isNaN(parsed)) {
619
+ formattedTimestamp = new Date(parsed).toLocaleString();
620
+ } else {
621
+ formattedTimestamp = new Date(timestampStr).toLocaleString();
622
+ }
623
+ }
624
+ this.elements.identityTimestampEl.textContent = formattedTimestamp;
625
+
626
+ // Update UUID
627
+ this.elements.identityUuidEl.textContent = keystone.data?.uuid || 'N/A';
628
+
629
+ // Update Scrubber Status
630
+ this.elements.scrubberStatusEl.textContent = `Frame ${this.currentFrameIndex + 1} / ${this.history.length}`;
631
+
632
+ // Disable/enable scrubber buttons based on boundaries
633
+ this.elements.btnScrubFirstEl.disabled = this.currentFrameIndex === 0;
634
+ this.elements.btnScrubPrev10El.disabled = this.currentFrameIndex === 0;
635
+ this.elements.btnScrubPrevEl.disabled = this.currentFrameIndex === 0;
636
+ this.elements.btnScrubNextEl.disabled = this.currentFrameIndex === this.history.length - 1;
637
+ this.elements.btnScrubNext10El.disabled = this.currentFrameIndex === this.history.length - 1;
638
+ this.elements.btnScrubLatestEl.disabled = this.currentFrameIndex === this.history.length - 1;
639
+
640
+ // Show Frame Details
641
+ const frameDetails = keystone.data?.frameDetails;
642
+ this.elements.identityFrameDetailsEl.textContent = frameDetails ? pretty(frameDetails) : '{}';
643
+
644
+ // Get and Show Aggregated details up to the scrolled-to frame
645
+ const metaspace = getIbGibGlobalThis_SpaceGib(APP_CONFIG).metaspace;
646
+ if (metaspace) {
647
+ const space = await metaspace.getLocalUserSpace({ lock: false });
648
+ if (space) {
649
+ try {
650
+ const keystoneSvc = new KeystoneService_V1();
651
+ const aggregated = await keystoneSvc.getAggregateDetails({
652
+ latestKeystone: keystone,
653
+ metaspace,
654
+ space
655
+ });
656
+
657
+ this.elements.identityAggrDetailsEl.textContent = aggregated ? pretty(aggregated) : '{}';
658
+
659
+ // Update Username and Description fields
660
+ this.elements.identityNameEl.textContent = aggregated?.username || aggregated?.profile?.name || aggregated?.name || '-';
661
+ this.elements.identityDescriptionEl.textContent = aggregated?.description || aggregated?.profile?.description || '-';
662
+ } catch (err) {
663
+ console.warn(`${lc} Error aggregating details: ${extractErrorMsg(err)}`);
664
+ this.elements.identityAggrDetailsEl.textContent = '{}';
665
+ this.elements.identityNameEl.textContent = '-';
666
+ this.elements.identityDescriptionEl.textContent = '-';
667
+ }
668
+ } else {
669
+ this.elements.identityAggrDetailsEl.textContent = '{}';
670
+ this.elements.identityNameEl.textContent = '-';
671
+ this.elements.identityDescriptionEl.textContent = '-';
672
+ }
673
+ } else {
674
+ this.elements.identityAggrDetailsEl.textContent = '{}';
675
+ this.elements.identityNameEl.textContent = '-';
676
+ this.elements.identityDescriptionEl.textContent = '-';
677
+ }
678
+
679
+ // Render Raw Timeline JSON
680
+ this.elements.rawKeystoneJsonEl.textContent = pretty(keystone);
681
+
682
+ // Render Challenge Pools
683
+ this.renderChallengePools(keystone.data?.challengePools || []);
684
+ }
685
+
686
+ private renderChallengePools(pools: any[]) {
687
+ if (!this.elements) return;
688
+ const container = this.elements.poolsContainerEl;
689
+ container.innerHTML = '';
690
+
691
+ if (!pools || pools.length === 0) {
692
+ container.innerHTML = `<p class="description">No challenge pools found in this keystone.</p>`;
693
+ return;
694
+ }
695
+
696
+ for (const pool of pools) {
697
+ const card = document.createElement('div');
698
+ card.className = 'pool-card';
699
+
700
+ const config = pool.config || {};
701
+ const behavior = config.behavior || {};
702
+ const allowedVerbs = config.allowedVerbs || [];
703
+ const activeCount = pool.challenges ? Object.keys(pool.challenges).length : 0;
704
+ const isForeign = !!pool.isForeign;
705
+
706
+ // Generate HTML for the challenges list
707
+ const challenges = pool.challenges || {};
708
+ const challengeListHtml = Object.entries(challenges).map(([id, challenge]: [string, any]) => {
709
+ const truncatedHash = challenge.hash && challenge.hash.length > 20
710
+ ? challenge.hash.substring(0, 10) + '...' + challenge.hash.substring(challenge.hash.length - 8)
711
+ : challenge.hash || 'N/A';
712
+ return `
713
+ <div class="pool-challenge-item">
714
+ <span class="challenge-id">${id}</span>
715
+ <span class="challenge-hash" title="${challenge.hash || ''}">${truncatedHash}</span>
716
+ </div>
717
+ `;
718
+ }).join('');
719
+
720
+ card.innerHTML = `
721
+ <div class="pool-header">
722
+ <span class="pool-title">${pool.id || 'Unnamed Pool'}</span>
723
+ <span class="pool-tag ${isForeign ? 'foreign' : 'native'}">${isForeign ? 'Foreign' : 'Native'}</span>
724
+ </div>
725
+ <div class="pool-grid">
726
+ <div class="pool-field">
727
+ <label>Challenge Type</label>
728
+ <span>${config.type || 'N/A'}</span>
729
+ </div>
730
+ <div class="pool-field">
731
+ <label>Target Size</label>
732
+ <span>${behavior.size ?? 'N/A'}</span>
733
+ </div>
734
+ <div class="pool-field">
735
+ <label>Active Challenges</label>
736
+ <span>${activeCount}</span>
737
+ </div>
738
+ <div class="pool-field">
739
+ <label>Replenish Strategy</label>
740
+ <span>${behavior.replenish || 'N/A'}</span>
741
+ </div>
742
+ <div class="pool-field">
743
+ <label>Allowed Verbs</label>
744
+ <span>${allowedVerbs.length > 0 ? allowedVerbs.join(', ') : 'Any'}</span>
745
+ </div>
746
+ <div class="pool-field">
747
+ <label>FIFO / Random cost</label>
748
+ <span>FIFO: ${behavior.selectSequentially ?? 0}, Rand: ${behavior.selectRandomly ?? 0}</span>
749
+ </div>
750
+ </div>
751
+
752
+ <!-- Collapsible Challenges List -->
753
+ <details class="pool-details">
754
+ <summary class="pool-details-summary">Challenges (${activeCount})</summary>
755
+ <div class="pool-challenges-list">
756
+ ${challengeListHtml || '<p class="placeholder-text" style="padding: 0.5rem; text-align: center;">No challenges active.</p>'}
757
+ </div>
758
+ </details>
759
+ `;
760
+ container.appendChild(card);
761
+ }
762
+ }
763
+
764
+ override async handleContextUpdated(): Promise<void> {
765
+ await this.renderUI();
766
+ }
767
+ }