// : assemble le tableau de bord. // // Au montage, il charge l'état initial via REST (`/jobs`, `/reviews`), puis // s'abonne au flux global `/events` pour tenir les listes à jour en direct. // La sélection d'un job ouvre son flux fin via . import { endpoints, escapeHtml, fetchJobs, fetchReviews, relativeTime, subscribe, } from "./api.js"; import "./theme-toggle.js"; import "./job-list.js"; import "./job-detail.js"; class Dashboard extends HTMLElement { connectedCallback() { this._jobs = { pending: [], processing: [] }; this._reviews = []; this._selectedId = null; this.render(); this.refresh(); this.connect(); } disconnectedCallback() { this._source?.close(); if (this._refreshTimer) clearTimeout(this._refreshTimer); } // ---- Chargement et flux ------------------------------------------------- async refresh() { try { const [jobs, reviews] = await Promise.all([fetchJobs(), fetchReviews()]); this._jobs = { pending: jobs.pending ?? [], processing: jobs.processing ?? [], }; this._reviews = reviews.reviews ?? []; this.update(); } catch (error) { console.error("failed to refresh the dashboard", error); } } connect() { this._source = subscribe(endpoints.lifecycle, { onLifecycle: (event) => this.applyLifecycle(event), // En cas de gap (évènements ratés), on recharge l'état depuis le REST. onGap: () => this.refresh(), }); } // Recharge la liste des reviews peu après une fin de job, pour récupérer les // champs que l'évènement ne porte pas (markdown, coût, modèle). scheduleRefresh() { if (this._refreshTimer) clearTimeout(this._refreshTimer); this._refreshTimer = setTimeout(() => this.refresh(), 400); } applyLifecycle(event) { switch (event.type) { case "queued": if (!this._jobs.pending.some((job) => job.id === event.id)) { this._jobs.pending.push(this.jobFromEvent(event)); } break; case "started": { // On garde le job déjà connu s'il existe, pour conserver les champs que // l'évènement `started` ne porte pas (event_id, created_at). const known = this._jobs.pending.find((job) => job.id === event.id); this.removeJob(event.id); this._jobs.processing.push(known ?? this.jobFromEvent(event)); break; } case "review_done": case "failed": this.removeJob(event.id); this.scheduleRefresh(); break; case "skipped": this.removeJob(event.id); break; default: return; } this.update(); } removeJob(id) { this._jobs.pending = this._jobs.pending.filter((job) => job.id !== id); this._jobs.processing = this._jobs.processing.filter((job) => job.id !== id); } // L'évènement de cycle de vie porte assez de champs pour l'affichage ; le // rafraîchissement REST fournira le reste (created_at, payload…). jobFromEvent(event) { return { id: event.id, event_id: event.event_id, repo: event.repo, pr: event.pr, created_at: event.created_at, }; } // ---- Rendu -------------------------------------------------------------- render() { this.innerHTML = `

Live operations

Review dashboard

Pending jobs, work in progress, and completed reviews — kept in sync over SSE.

Pending

0

In progress

0

Activity

Completed reviews

0
`; this._pending = this.querySelector("#pending"); this._processing = this.querySelector("#processing"); this._detail = this.querySelector("#detail"); this._reviewsEl = this.querySelector("#reviews"); this.querySelector("#refresh").addEventListener("click", () => this.refresh()); this.addEventListener("job-select", (event) => this.select(event.detail.id)); this.update(); } update() { this._pending.jobs = this._jobs.pending; this._pending.selected = this._selectedId; this._processing.jobs = this._jobs.processing; this._processing.selected = this._selectedId; this.querySelector("#pending-count").textContent = String(this._jobs.pending.length); this.querySelector("#processing-count").textContent = String(this._jobs.processing.length); this.querySelector("#reviews-count").textContent = String(this._reviews.length); this._reviewsEl.innerHTML = this.reviewsHtml(); } select(id) { this._selectedId = id; this._detail.jobId = id; this.update(); this._detail.scrollIntoView({ behavior: "smooth", block: "nearest" }); } reviewsHtml() { if (this._reviews.length === 0) { return `

No review completed yet.

`; } const rows = [...this._reviews] .sort((a, b) => String(b.completed_at ?? "").localeCompare(String(a.completed_at ?? ""))) .map( (review) => ` ${review.ok ? "done" : "failed"} ${escapeHtml(review.repo)}#${review.pr} ${review.comments} ${review.duration_ms} ms ${escapeHtml(review.model ?? "—")} ${relativeTime(review.completed_at)} `, ) .join(""); return ` ${rows}
StatusPull requestCommentsDurationModelCompleted
`; } } customElements.define("steep-dashboard", Dashboard);