/** * @module ui/component/admin/admin-review/admin-review * * Web Component for Administrative Review of One-Off Challenge Keystones. * Displays live countdown timers for the Active Exposure Lease ("burn fuse"), * highlights prior inspections, provides cryptographic choice pools (accept/reject), * and computes client-side signature proofs via solveAdminChallengeKeystone. */ import thisCss from "./admin-review.css"; import thisHtml from "./admin-review.html"; import commonComponentStylesCss from "../../common-component-styles.css"; import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { ROOT_ADDR } from "@ibgib/ts-gib/dist/V1/constants.mjs"; import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs"; import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs"; import { KeystoneIbGib_V1 } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs"; import { POOL_ID_ACCEPT, POOL_ID_REJECT } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs"; import { solveAdminChallengeKeystone } from "@ibgib/core-gib/dist/keystone/admin/admin-helpers.mjs"; import { DEFAULT_EXPOSURE_LEASE_TIMEOUT_MS } from "@ibgib/core-gib/dist/keystone/admin/admin-constants.mjs"; import { IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase } from "../../ibgib-dynamic-component-bases.mjs"; import { ElementsBase, IbGibDynamicComponentInstance } from "../../component-types.mjs"; import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs"; export const ADMIN_REVIEW_COMPONENT_NAME = 'ibgib-admin-review'; /** * Metadata for the Keystone Admin Review component. */ export class AdminReviewComponentMeta extends IbGibDynamicComponentMetaBase { protected lc: string = `[AdminReviewComponentMeta]`; routeRegExp?: RegExp = new RegExp(`^${ADMIN_REVIEW_COMPONENT_NAME}$|^admin/review$`, 'i'); componentName = ADMIN_REVIEW_COMPONENT_NAME; constructor() { super(getComponentCtorArg()); if (typeof customElements !== 'undefined' && !customElements.get(this.componentName)) { customElements.define(this.componentName, AdminReviewComponentInstance); } } async createInstance({ path, ibGibAddr }: { path: string; ibGibAddr: IbGibAddr; }): Promise { const lc = `${this.lc}[${this.createInstance.name}]`; const component = document.createElement(this.componentName) as AdminReviewComponentInstance; await component.initialize({ ibGibAddr: ibGibAddr || ROOT_ADDR, meta: this, html: thisHtml, css: [commonComponentStylesCss, thisCss], }); return component; } } /** * References to DOM elements in the admin review shadow root. */ interface AdminReviewElements extends ElementsBase { btnBack?: HTMLButtonElement; errorContainer?: HTMLDivElement; errorTitle?: HTMLElement; errorMessage?: HTMLElement; lookupSection?: HTMLElement; adminIdInput?: HTMLInputElement; adminPasswordInput?: HTMLInputElement; tjpGibInput?: HTMLInputElement; btnToggleTjpMask?: HTMLButtonElement; btnFetchChallenge?: HTMLButtonElement; fetchSpinner?: HTMLDivElement; challengeSection?: HTMLElement; alertLocked?: HTMLDivElement; lockedAdminId?: HTMLElement; lockedTime?: HTMLElement; btnRefreshLockout?: HTMLButtonElement; alertAlreadyViewed?: HTMLDivElement; alreadyViewedText?: HTMLParagraphElement; countdownBanner?: HTMLDivElement; countdownTimer?: HTMLDivElement; countdownIcon?: HTMLElement; countdownProgressFill?: HTMLDivElement; metaScope?: HTMLElement; metaTarget?: HTMLElement; metaStatus?: HTMLElement; metaCreatedAt?: HTMLElement; applicantDetailsBox?: HTMLDivElement; appHandle?: HTMLElement; appEmail?: HTMLElement; appName?: HTMLElement; appProject?: HTMLElement; appProfile?: HTMLElement; appReason?: HTMLElement; rawKeystoneCode?: HTMLElement; rawSolvedDetails?: HTMLDetailsElement; rawSolvedCode?: HTMLElement; auditLogDetails?: HTMLDetailsElement; auditLogContainer?: HTMLDivElement; decisionForm?: HTMLDivElement; poolsContainer?: HTMLDivElement; resolutionNotesInput?: HTMLTextAreaElement; decisionErrorContainer?: HTMLDivElement; decisionErrorTitle?: HTMLElement; decisionErrorMessage?: HTMLElement; btnSubmitDecision?: HTMLButtonElement; submitSpinner?: HTMLDivElement; resolutionSuccessContainer?: HTMLDivElement; resolutionStatusText?: HTMLElement; emailDispatchedBox?: HTMLDivElement; } /** * Concrete Web Component for administrative challenge review. */ export class AdminReviewComponentInstance extends IbGibDynamicComponentInstanceBase { protected lc: string = `[AdminReviewComponentInstance]`; private activeGenesisKeystone?: KeystoneIbGib_V1; private activeMetadata?: any; private activeTjpGib?: string; private cachedAdminId?: string; private cachedAdminPassword?: string; private countdownInterval?: any; private countdownRemainingMs: number = 0; private countdownExpiresTimestampMs: number = 0; private totalLeaseMs: number = 180000; constructor() { super(); } public override async created(): Promise { const lc = `${this.lc}[${this.created.name}]`; try { const shadow = this.shadowRoot!; const contentEl = (shadow.querySelector('.admin-review-container') || this) as HTMLElement; this.elements = { contentEl, btnBack: shadow.querySelector('#btn-back') as HTMLButtonElement, errorContainer: shadow.querySelector('#error-container') as HTMLDivElement, errorTitle: shadow.querySelector('#error-title') as HTMLElement, errorMessage: shadow.querySelector('#error-message') as HTMLElement, lookupSection: shadow.querySelector('#lookup-section') as HTMLElement, adminIdInput: shadow.querySelector('#admin-id-input') as HTMLInputElement, adminPasswordInput: shadow.querySelector('#admin-password-input') as HTMLInputElement, tjpGibInput: shadow.querySelector('#tjp-gib-input') as HTMLInputElement, btnToggleTjpMask: shadow.querySelector('#btn-toggle-tjp-mask') as HTMLButtonElement, btnFetchChallenge: shadow.querySelector('#btn-fetch-challenge') as HTMLButtonElement, fetchSpinner: shadow.querySelector('#fetch-spinner') as HTMLDivElement, challengeSection: shadow.querySelector('#challenge-section') as HTMLElement, alertLocked: shadow.querySelector('#alert-locked') as HTMLDivElement, lockedAdminId: shadow.querySelector('#locked-admin-id') as HTMLElement, lockedTime: shadow.querySelector('#locked-time') as HTMLElement, btnRefreshLockout: shadow.querySelector('#btn-refresh-lockout') as HTMLButtonElement, alertAlreadyViewed: shadow.querySelector('#alert-already-viewed') as HTMLDivElement, alreadyViewedText: shadow.querySelector('#already-viewed-text') as HTMLParagraphElement, countdownBanner: shadow.querySelector('#countdown-banner') as HTMLDivElement, countdownTimer: shadow.querySelector('#countdown-timer') as HTMLDivElement, countdownIcon: shadow.querySelector('#countdown-icon') as HTMLElement, countdownProgressFill: shadow.querySelector('#countdown-progress-fill') as HTMLDivElement, metaScope: shadow.querySelector('#meta-scope') as HTMLElement, metaTarget: shadow.querySelector('#meta-target') as HTMLElement, metaStatus: shadow.querySelector('#meta-status') as HTMLElement, metaCreatedAt: shadow.querySelector('#meta-created-at') as HTMLElement, applicantDetailsBox: shadow.querySelector('#applicant-details-box') as HTMLDivElement, appHandle: shadow.querySelector('#app-handle') as HTMLElement, appEmail: shadow.querySelector('#app-email') as HTMLElement, appName: shadow.querySelector('#app-name') as HTMLElement, appProject: shadow.querySelector('#app-project') as HTMLElement, appProfile: shadow.querySelector('#app-profile') as HTMLElement, appReason: shadow.querySelector('#app-reason') as HTMLElement, rawKeystoneCode: shadow.querySelector('#raw-keystone-code') as HTMLElement, rawSolvedDetails: shadow.querySelector('#raw-solved-details') as HTMLDetailsElement, rawSolvedCode: shadow.querySelector('#raw-solved-code') as HTMLElement, auditLogDetails: shadow.querySelector('#audit-log-details') as HTMLDetailsElement, auditLogContainer: shadow.querySelector('#audit-log-container') as HTMLDivElement, decisionForm: shadow.querySelector('#decision-form') as HTMLDivElement, poolsContainer: shadow.querySelector('#pools-container') as HTMLDivElement, resolutionNotesInput: shadow.querySelector('#resolution-notes-input') as HTMLTextAreaElement, decisionErrorContainer: shadow.querySelector('#decision-error-container') as HTMLDivElement, decisionErrorTitle: shadow.querySelector('#decision-error-title') as HTMLElement, decisionErrorMessage: shadow.querySelector('#decision-error-message') as HTMLElement, btnSubmitDecision: shadow.querySelector('#btn-submit-decision') as HTMLButtonElement, submitSpinner: shadow.querySelector('#submit-spinner') as HTMLDivElement, resolutionSuccessContainer: shadow.querySelector('#resolution-success-container') as HTMLDivElement, resolutionStatusText: shadow.querySelector('#resolution-status-text') as HTMLElement, emailDispatchedBox: shadow.querySelector('#email-dispatched-box') as HTMLDivElement, }; // Hook back button this.elements.btnBack?.addEventListener('click', () => { if (window.history.length > 1) { window.history.back(); } else { window.location.href = '/'; } }); // Hook fetch button this.elements.btnFetchChallenge?.addEventListener('click', () => { this.handleFetchChallenge(); }); // Hook refresh lockout button this.elements.btnRefreshLockout?.addEventListener('click', () => { this.handleFetchChallenge(); }); // Hook visibility toggle for tjpGib this.elements.btnToggleTjpMask?.addEventListener('click', () => { if (!this.elements?.tjpGibInput) { return; } const isPass = this.elements.tjpGibInput.type === 'password'; this.elements.tjpGibInput.type = isPass ? 'text' : 'password'; if (this.elements.btnToggleTjpMask) { this.elements.btnToggleTjpMask.textContent = isPass ? '🙈' : '👁️'; } }); // Enter key on tjp-gib input triggers fetch this.elements.tjpGibInput?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { this.handleFetchChallenge(); } }); // Hook submit button this.elements.btnSubmitDecision?.addEventListener('click', () => { this.handleSubmitDecision(); }); // Check if tjpGib or adminId are provided in URL search params try { const urlParams = new URLSearchParams(window.location.search); const tjpParam = urlParams.get('tjpGib') || urlParams.get('tjp'); if (tjpParam && this.elements.tjpGibInput) { this.elements.tjpGibInput.value = tjpParam.trim(); } const adminIdParam = urlParams.get('adminId') || urlParams.get('id'); if (adminIdParam && this.elements.adminIdInput) { this.elements.adminIdInput.value = adminIdParam.trim(); } } catch { } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } public override async disconnected(): Promise { const lc = `${this.lc}[${this.disconnected.name}]`; this.clearCountdown(); } private showError(title: string, message: string): void { const els = this.elements; if (!els) { return; } let targetToScroll: HTMLElement | undefined; // 1. Top error container if (els.errorContainer) { if (els.errorTitle) { els.errorTitle.textContent = title; } if (els.errorMessage) { els.errorMessage.textContent = message; } els.errorContainer.classList.remove('hidden'); targetToScroll = els.errorContainer; } // 2. Inline decision error container (directly by the submit button) if (els.decisionErrorContainer) { if (els.decisionErrorTitle) { els.decisionErrorTitle.textContent = title; } if (els.decisionErrorMessage) { els.decisionErrorMessage.textContent = message; } els.decisionErrorContainer.classList.remove('hidden'); targetToScroll = els.decisionErrorContainer; } if (targetToScroll) { try { targetToScroll.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch { } } } private hideError(): void { this.elements?.errorContainer?.classList.add('hidden'); this.elements?.decisionErrorContainer?.classList.add('hidden'); } private clearCountdown(): void { if (this.countdownInterval) { clearInterval(this.countdownInterval); this.countdownInterval = undefined; } } private startCountdown(remainingMs: number, totalLeaseMs: number = 180000): void { this.clearCountdown(); this.countdownRemainingMs = remainingMs; this.totalLeaseMs = totalLeaseMs > 0 ? totalLeaseMs : 180000; this.countdownExpiresTimestampMs = Date.now() + remainingMs; this.updateCountdownDisplay(); this.countdownInterval = setInterval(() => { const now = Date.now(); const leftMs = Math.max(0, this.countdownExpiresTimestampMs - now); this.countdownRemainingMs = leftMs; this.updateCountdownDisplay(); if (leftMs <= 0) { this.clearCountdown(); this.handleCountdownExpired(); } }, 1000); } private updateCountdownDisplay(): void { const els = this.elements; if (!els?.countdownTimer) { return; } const totalSec = Math.floor(this.countdownRemainingMs / 1000); const mins = Math.floor(totalSec / 60); const secs = totalSec % 60; const formatted = `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`; els.countdownTimer.textContent = formatted; const ratio = this.totalLeaseMs > 0 ? (this.countdownRemainingMs / this.totalLeaseMs) : 0; const pct = Math.max(0, Math.min(100, ratio * 100)); if (els.countdownProgressFill) { els.countdownProgressFill.style.width = `${pct}%`; } // Determine phase and emoji icon let phase = 'phase-green'; let icon = '⏱️'; if (this.countdownRemainingMs <= 15000 || ratio <= 0.08) { phase = 'phase-critical'; icon = '🚨'; } else if (this.countdownRemainingMs <= 35000 || ratio <= 0.20) { phase = 'phase-red'; icon = '🔥'; } else if (this.countdownRemainingMs <= 75000 || ratio <= 0.45) { phase = 'phase-yellow'; icon = '⚠️'; } else { phase = 'phase-green'; icon = '⏱️'; } if (els.countdownBanner) { els.countdownBanner.classList.remove('phase-green', 'phase-yellow', 'phase-red', 'phase-critical', 'phase-expired', 'phase-resolved'); els.countdownBanner.classList.add(phase); } if (els.countdownIcon) { els.countdownIcon.textContent = icon; } } private handleCountdownExpired(): void { const els = this.elements; if (els?.countdownTimer) { els.countdownTimer.textContent = 'EXPIRED'; els.countdownTimer.classList.add('expired'); } if (els?.countdownIcon) { els.countdownIcon.textContent = '⛔'; } if (els?.countdownProgressFill) { els.countdownProgressFill.style.width = '0%'; } if (els?.countdownBanner) { els.countdownBanner.classList.remove('phase-green', 'phase-yellow', 'phase-red', 'phase-critical', 'phase-resolved'); els.countdownBanner.classList.add('phase-expired'); } if (els?.btnSubmitDecision) { els.btnSubmitDecision.disabled = true; } this.showError('Challenge Expired', 'The active exposure countdown lease has expired. This challenge has been automatically burned and can no longer be resolved.'); } private handleCountdownResolved(): void { this.clearCountdown(); const els = this.elements; if (!els) { return; } if (els.countdownBanner) { els.countdownBanner.classList.remove('phase-green', 'phase-yellow', 'phase-red', 'phase-critical', 'phase-expired'); els.countdownBanner.classList.add('phase-resolved'); } if (els.countdownTimer) { els.countdownTimer.textContent = 'RESOLVED'; els.countdownTimer.classList.remove('expired'); } if (els.countdownIcon) { els.countdownIcon.textContent = '🔒'; } if (els.countdownProgressFill) { els.countdownProgressFill.style.width = '0%'; } } /** * Handles fetching the challenge keystone from the server. */ private async handleFetchChallenge(): Promise { const lc = `${this.lc}[${this.handleFetchChallenge.name}]`; try { this.hideError(); this.clearCountdown(); const adminId = this.elements?.adminIdInput?.value.trim() || this.cachedAdminId; const adminPassword = this.elements?.adminPasswordInput?.value.trim() || this.cachedAdminPassword; const tjpGib = this.elements?.tjpGibInput?.value.trim() || this.activeTjpGib; if (!adminId) { this.showError('Authentication Required', 'Please enter your Admin ID.'); this.elements?.adminIdInput?.focus(); return; } if (!adminPassword) { this.showError('Authentication Required', 'Please enter your Admin Master Secret / Password.'); this.elements?.adminPasswordInput?.focus(); return; } if (!tjpGib) { this.showError('Missing Input', 'Please enter or paste the 64-character Keystone TJP Gib.'); this.elements?.tjpGibInput?.focus(); return; } // Show fetch spinner this.elements?.fetchSpinner?.classList.remove('hidden'); if (this.elements?.btnFetchChallenge) { this.elements.btnFetchChallenge.disabled = true; } const response = await fetch(`/api/admin/review/${encodeURIComponent(tjpGib)}`, { method: 'GET', headers: { 'x-admin-id': adminId, 'x-admin-password': adminPassword, 'Accept': 'application/json', }, }); const data = await response.json().catch(() => ({})); if (!response.ok || !data.success) { const status = response.status; const errorMsg = data.error || (status === 401 ? 'Invalid admin credentials' : status === 404 ? 'Challenge keystone not found' : 'Failed to retrieve challenge'); this.showError(`Error (${status})`, errorMsg); this.elements?.challengeSection?.classList.add('hidden'); return; } // Challenge successfully retrieved this.activeTjpGib = tjpGib; this.activeGenesisKeystone = data.genesisKeystone; this.activeMetadata = data.metadata || data; this.cachedAdminId = adminId; this.cachedAdminPassword = adminPassword; // Disable Challenge Verification section once retrieved if (this.elements?.lookupSection) { this.elements.lookupSection.classList.add('disabled'); } if (this.elements?.adminIdInput) { this.elements.adminIdInput.disabled = true; } if (this.elements?.adminPasswordInput) { this.elements.adminPasswordInput.disabled = true; } if (this.elements?.tjpGibInput) { this.elements.tjpGibInput.disabled = true; } if (this.elements?.btnFetchChallenge) { this.elements.btnFetchChallenge.disabled = true; } if (this.elements?.btnToggleTjpMask) { this.elements.btnToggleTjpMask.disabled = true; } this.renderChallengeDetails(data); } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`); this.showError('Load Error', emsg); } finally { this.elements?.fetchSpinner?.classList.add('hidden'); if (this.elements?.btnFetchChallenge && !this.activeGenesisKeystone && !this.elements?.lookupSection?.classList.contains('disabled')) { this.elements.btnFetchChallenge.disabled = false; } } } /** * Renders retrieved challenge information, countdown, pools, and applicant details. */ private renderChallengeDetails(data: any): void { const { genesisKeystone, wasAlreadyViewed, countdownRemainingMs } = data; const metadata = data.metadata || data; const isLocked = Boolean(data.locked); const isResolved = metadata?.status === 'approved' || metadata?.status === 'rejected'; const els = this.elements; if (!els) { return; } // 1. Reveal challenge card els.challengeSection?.classList.remove('hidden'); els.resolutionSuccessContainer?.classList.add('hidden'); // 2. Concurrency Lockout vs Prior Inspection Warning if (isLocked) { els.alertLocked?.classList.remove('hidden'); if (els.lockedAdminId) { els.lockedAdminId.textContent = data.activeAdminId || 'unknown'; } if (els.lockedTime) { const startedAt = data.activeExposureStartedAtIso || metadata?.firstViewedAtIso; els.lockedTime.textContent = startedAt ? new Date(startedAt).toLocaleString() : 'recently'; } els.alertAlreadyViewed?.classList.add('hidden'); els.decisionForm?.classList.add('hidden'); } else { els.alertLocked?.classList.add('hidden'); if (!isResolved) { els.decisionForm?.classList.remove('hidden'); } // Warning banner for prior inspection (ONLY shown when still unresolved and viewing admin is active reviewer) if (wasAlreadyViewed && !isResolved) { els.alertAlreadyViewed?.classList.remove('hidden'); if (els.alreadyViewedText && metadata.firstViewedAtIso) { const viewedDate = new Date(metadata.firstViewedAtIso).toLocaleString(); els.alreadyViewedText.textContent = `ATTENTION: This challenge was previously inspected on ${viewedDate}. The active exposure burn countdown is currently ticking!`; } } else { els.alertAlreadyViewed?.classList.add('hidden'); } } // 3. Countdown timer if (isResolved) { this.handleCountdownResolved(); } else if (typeof countdownRemainingMs === 'number') { const totalLeaseMs = metadata?.exposureTimeoutMs || data.exposureTimeoutMs || 180000; this.startCountdown(countdownRemainingMs, totalLeaseMs); } // 4. Metadata summary if (els.metaScope) { els.metaScope.textContent = metadata.scope || '--'; } if (els.metaTarget) { els.metaTarget.textContent = metadata.targetIdentifier || '--'; } if (els.metaStatus) { const displayStatus = isLocked ? 'IN REVIEW' : (metadata.status || 'PENDING'); els.metaStatus.textContent = displayStatus.toUpperCase(); els.metaStatus.className = `summary-badge status-${isLocked ? 'pending' : (metadata.status || 'pending')}`; } if (els.metaCreatedAt) { els.metaCreatedAt.textContent = metadata.createdAtIso ? new Date(metadata.createdAtIso).toLocaleString() : '--'; } // 5. Applicant details (for exception-keys scope) const applicant = metadata.applicantDetails || genesisKeystone?.data?.applicantDetails; if (applicant && els.applicantDetailsBox) { els.applicantDetailsBox.classList.remove('hidden'); if (els.appHandle) { els.appHandle.textContent = applicant.siteUsername ? `@${applicant.siteUsername}` : '--'; } if (els.appEmail) { els.appEmail.textContent = applicant.email || '--'; } if (els.appName) { els.appName.textContent = applicant.applicantName || 'Anonymous'; } if (els.appProject) { els.appProject.textContent = applicant.projectTitle || '--'; } if (els.appProfile) { els.appProfile.textContent = applicant.profileUrl || 'None provided'; } if (els.appReason) { els.appReason.textContent = applicant.reason || '--'; } } else { els.applicantDetailsBox?.classList.add('hidden'); } // 6. Raw genesis keystone code if (els.rawKeystoneCode) { if (genesisKeystone) { els.rawKeystoneCode.textContent = JSON.stringify(genesisKeystone.data, null, 2); } else if (isLocked) { els.rawKeystoneCode.textContent = 'Keystone data withheld: Challenge currently under active review by another administrator.'; } else { els.rawKeystoneCode.textContent = '--'; } } // 6b. Raw evolved (solved) keystone code (if resolved) const solvedKeystone = data.solvedKeystone; if (solvedKeystone && els.rawSolvedDetails && els.rawSolvedCode) { els.rawSolvedCode.textContent = JSON.stringify(solvedKeystone.data || solvedKeystone, null, 2); els.rawSolvedDetails.classList.remove('hidden'); } else { els.rawSolvedDetails?.classList.add('hidden'); } // 6c. Audit event history log this.renderAuditLog(metadata?.history); // 7. Dynamic challenge choice pools (rendered only when active and not locked) if (!isLocked && !isResolved && genesisKeystone) { const rawPools = genesisKeystone?.data?.challengePools; if (!rawPools || (Array.isArray(rawPools) && rawPools.length === 0)) { throw new Error(`Invalid challenge keystone: challengePools is empty (E: 83920192837465019283746501928374)`); } const poolsList: any[] = Array.isArray(rawPools) ? rawPools : Object.values(rawPools); const poolIds: string[] = poolsList.map((p: any) => typeof p === 'string' ? p : p?.id).filter(Boolean); if (poolIds.length === 0) { throw new Error(`Invalid challenge keystone: no valid pool IDs found in challengePools (E: 72819203948571625340192837465019)`); } if (els.poolsContainer) { els.poolsContainer.innerHTML = ''; poolIds.forEach((poolId, idx) => { const isAccept = poolId === POOL_ID_ACCEPT; const isReject = poolId === POOL_ID_REJECT; const isDefault = isAccept || idx === 0; const card = document.createElement('label'); card.className = `pool-card pool-${poolId} ${isDefault ? (isAccept ? 'selected-accept' : 'selected-reject') : ''}`; const radio = document.createElement('input'); radio.type = 'radio'; radio.name = 'admin-choice-pool'; radio.value = poolId; radio.checked = isDefault; radio.addEventListener('change', () => { const allCards = els.poolsContainer?.querySelectorAll('.pool-card'); allCards?.forEach(c => c.classList.remove('selected-accept', 'selected-reject')); if (isAccept) { card.classList.add('selected-accept'); } else if (isReject) { card.classList.add('selected-reject'); } }); const labelSpan = document.createElement('span'); labelSpan.className = 'pool-name'; labelSpan.textContent = isAccept ? '✅ Approve (Accept)' : isReject ? '❌ Reject' : `Choice: ${poolId}`; card.appendChild(radio); card.appendChild(labelSpan); els.poolsContainer?.appendChild(card); }); } } // 8. If already resolved, disable submission if (isResolved) { if (els.decisionForm) { els.decisionForm.classList.add('hidden'); } this.handleCountdownResolved(); this.showSuccess(`Challenge is already resolved with status: ${metadata.status.toUpperCase()}`, metadata.status === 'approved'); } else if (!isLocked) { if (els.btnSubmitDecision) { els.btnSubmitDecision.disabled = false; } } } private showSuccess(msg: string, isApproval: boolean = false): void { const els = this.elements; if (!els) { return; } els.resolutionSuccessContainer?.classList.remove('hidden'); if (els.resolutionStatusText) { els.resolutionStatusText.textContent = msg; } if (isApproval && els.emailDispatchedBox) { els.emailDispatchedBox.classList.remove('hidden'); } else { els.emailDispatchedBox?.classList.add('hidden'); } } private renderAuditLog(history?: any[]): void { const els = this.elements; if (!els?.auditLogContainer) { return; } els.auditLogContainer.innerHTML = ''; if (!history || history.length === 0) { const empty = document.createElement('div'); empty.className = 'audit-log-empty'; empty.textContent = 'No recorded audit events.'; els.auditLogContainer.appendChild(empty); return; } history.forEach(item => { const row = document.createElement('div'); const eventClass = (item.event || '').replace(/_/g, '-').toLowerCase(); row.className = `audit-log-entry event-${eventClass}`; const mainDiv = document.createElement('div'); mainDiv.className = 'audit-entry-main'; const leftDiv = document.createElement('div'); leftDiv.className = 'audit-entry-left'; const nameSpan = document.createElement('span'); nameSpan.className = 'audit-event-name'; nameSpan.textContent = (item.event || 'event').replace(/_/g, ' '); const adminId = item.details?.adminId; leftDiv.appendChild(nameSpan); if (adminId) { const adminSpan = document.createElement('span'); adminSpan.className = 'audit-admin-id'; adminSpan.textContent = `@${adminId}`; leftDiv.appendChild(adminSpan); } const timeSpan = document.createElement('span'); timeSpan.className = 'audit-timestamp'; timeSpan.textContent = item.timestampIso ? new Date(item.timestampIso).toLocaleString() : '--'; mainDiv.appendChild(leftDiv); mainDiv.appendChild(timeSpan); row.appendChild(mainDiv); const detailNotes = item.details?.resolutionNotes || (item.details?.status ? `Status: ${item.details.status}` : undefined); if (detailNotes) { const detailsDiv = document.createElement('div'); detailsDiv.className = 'audit-details-text'; detailsDiv.textContent = detailNotes; row.appendChild(detailsDiv); } els.auditLogContainer?.appendChild(row); }); } /** * Solves the selected challenge pool using the admin master secret and submits to the server. */ private async handleSubmitDecision(): Promise { const lc = `${this.lc}[${this.handleSubmitDecision.name}]`; try { this.hideError(); if (!this.activeGenesisKeystone || !this.activeTjpGib) { this.showError('State Error', 'No active challenge is loaded.'); return; } const adminId = this.cachedAdminId || this.elements?.adminIdInput?.value.trim(); const adminPassword = this.cachedAdminPassword || this.elements?.adminPasswordInput?.value.trim(); if (!adminId) { this.showError('Authentication Required', 'Please enter your Admin ID.'); this.elements?.adminIdInput?.focus(); return; } if (!adminPassword) { this.showError('Authentication Required', 'Please enter your Admin Master Secret.'); this.elements?.adminPasswordInput?.focus(); return; } // Find selected pool const selectedRadio = this.elements?.poolsContainer?.querySelector('input[name="admin-choice-pool"]:checked') as HTMLInputElement; const poolId = selectedRadio?.value; if (!poolId) { this.showError('Selection Required', 'Please select a choice pool (e.g. Approve or Reject).'); return; } const resolutionNotes = this.elements?.resolutionNotesInput?.value.trim() || undefined; // Show submit spinner this.elements?.submitSpinner?.classList.remove('hidden'); if (this.elements?.btnSubmitDecision) { this.elements.btnSubmitDecision.disabled = true; } // 1. Solve challenge keystone on client using derived cryptographic proof console.log(`${lc} Solving challenge keystone for pool '${poolId}'...`); const solvedKeystone = await solveAdminChallengeKeystone({ genesisKeystone: this.activeGenesisKeystone, masterSecret: adminPassword, poolId, resolutionNotes, }); // 2. Submit resolution to server console.log(`${lc} Submitting solved keystone resolution to server...`); const response = await fetch('/api/admin/review/submit', { method: 'POST', headers: { 'x-admin-id': adminId, 'x-admin-password': adminPassword, 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ tjpGib: this.activeTjpGib, solvedKeystone, resolutionNotes, }), }); const data = await response.json().catch(() => ({})); if (!response.ok || !data.success) { const emsg = data.error || `Server returned error (${response.status})`; this.showError(`Submission Failed (${response.status})`, emsg); if (this.elements?.btnSubmitDecision) { this.elements.btnSubmitDecision.disabled = false; } return; } // Resolution success! this.handleCountdownResolved(); if (this.elements?.alertAlreadyViewed) { this.elements.alertAlreadyViewed.classList.add('hidden'); } if (this.elements?.decisionForm) { this.elements.decisionForm.classList.add('hidden'); } if (this.elements?.metaStatus) { this.elements.metaStatus.textContent = (data.status || 'RESOLVED').toUpperCase(); this.elements.metaStatus.className = `summary-badge status-${data.status || 'approved'}`; } // Reveal solved keystone in accordion if (this.elements?.rawSolvedDetails && this.elements?.rawSolvedCode && solvedKeystone) { this.elements.rawSolvedCode.textContent = JSON.stringify(solvedKeystone.data || solvedKeystone, null, 2); this.elements.rawSolvedDetails.classList.remove('hidden'); } const isApproval = data.status === 'approved'; // Append resolution event to audit log and refresh view if (this.activeMetadata?.history) { this.activeMetadata.history.push({ timestampIso: new Date().toISOString(), event: isApproval ? 'approved' : 'rejected', details: { adminId, poolId, resolutionNotes }, }); this.renderAuditLog(this.activeMetadata.history); } const successMsg = `Challenge successfully resolved with status: ${String(data.status).toUpperCase()}. Audit record persisted to chronological storage.`; this.showSuccess(successMsg, isApproval); } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`); this.showError('Submission Error', emsg); if (this.elements?.btnSubmitDecision) { this.elements.btnSubmitDecision.disabled = false; } } finally { this.elements?.submitSpinner?.classList.add('hidden'); } } }