| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- /**
- * Framework-free boot page and failure report. It remains available when a
- * client plugin fails because React arrives only with the UI renderer.
- * @module @deepseek-ai/dsh-client-web/src/boot-page
- */
- import type { LoaderEntryState } from './loader-status.ts'
- import css from './boot-page.module.css'
- /** Create a div with one module class and optional text. */
- function div(className: string | undefined, text?: string): HTMLDivElement {
- const el = document.createElement('div')
- el.className = className ?? ''
- if (text !== undefined) el.textContent = text
- return el
- }
- /** Kernel-owned page mounted below the application's root element. */
- export class BootPage {
- private readonly root: HTMLDivElement
- private readonly card: HTMLDivElement
- private readonly wordmark: HTMLDivElement
- private readonly spinner: HTMLDivElement
- private readonly hint: HTMLDivElement
- private readonly states = new Map<string, LoaderEntryState>()
- private readonly active = new Set<string>()
- private total = 0
- private failure: string | undefined
- /**
- * Build and attach the boot page.
- * @param container - Application mount point.
- */
- constructor(container: HTMLElement) {
- this.root = div(css.boot)
- this.root.dataset.dshBoot = ''
- this.card = div(css.card)
- this.wordmark = div(css.wordmark, 'HARNESS')
- this.spinner = div(css.spinner)
- this.spinner.dataset.dshBootSpinner = ''
- this.hint = div(css.hint, 'Loading plugins…')
- this.card.append(this.wordmark, this.spinner, this.hint)
- this.root.append(this.card)
- container.append(this.root)
- this.updateProgress()
- }
- /**
- * Set the number of loader entries represented by the progress arc.
- * @param total - Complete boot roster size.
- */
- setTotal(total: number): void {
- this.total = total
- this.updateProgress()
- }
- /**
- * Project one loader entry's fiber state.
- * @param id - Loader entry name.
- * @param state - Projected fiber state.
- */
- setState(id: string, state: LoaderEntryState): void {
- this.states.set(id, state)
- if (state === 'active') this.active.add(id)
- this.updateProgress()
- this.render()
- }
- /**
- * Display the boot failure report.
- * @param message - Failure report text.
- */
- fail(message: string): void {
- this.failure = message
- this.render()
- }
- /** Detach the page before or after the UI renderer takes the mount point. */
- dispose(): void {
- this.root.remove()
- }
- /** Redraw the state-dependent content below the wordmark. */
- private render(): void {
- const failed = [...this.states].filter(([, state]) => state === 'failed').map(([id]) => id)
- if (this.failure === undefined && failed.length === 0) {
- if (this.spinner.parentElement !== this.card) {
- this.card.replaceChildren(this.wordmark, this.spinner, this.hint)
- }
- return
- }
- const report = div(css.failed)
- report.append(div(css.failedTitle, 'Failed to load plugins'))
- for (const id of failed) report.append(div(css.failedItem, id))
- if (this.failure !== undefined) report.append(div(css.failedItem, this.failure))
- this.card.replaceChildren(this.wordmark, report)
- }
- /** Grow the rotating arc monotonically as loader entries activate. */
- private updateProgress(): void {
- const ratio = this.total === 0 ? 0 : Math.min(this.active.size / this.total, 1)
- this.spinner.style.setProperty('--dsh-boot-arc', `${String(Math.round(72 + ratio * 216))}deg`)
- }
- }
|