import type { DiagEvent } from "@vincentt-xr/harness/events"; import type { TunnelState, UAClass } from "@vincentt-xr/harness/tunnel"; import type { EventTags } from "../relay/server.js"; import type { PreviewEndReason } from "./runner.js"; export interface PreviewUiDeps { print: (line: string) => void; now: () => number; /** The forward target, named identically in the start block and the 502. */ targetUrl: string; color: boolean; /** Filter the stream to one tester (`--tester`) — one tab, across refreshes. */ filterTester?: string; /** Filter the stream to one connection (`--viewer`). */ filterViewer?: string; } /** * PreviewUi owns every line after the mint. It is a pure sink — it takes events * and prints strings — so the whole transcript is assertable without a terminal, * a socket or a phone. */ export declare class PreviewUi { private readonly deps; private readonly roster; private readonly perf; private readonly p; private readonly startedAt; /** L18 prints ONCE PER STRETCH of failure, not once per occurrence. */ private originDown; /** L21's soft warning prints once per preview. */ private egressWarned; /** L20b's 30-minute warning prints once. */ private hardMaxWarned; private tunnelState; private ended; constructor(deps: PreviewUiDeps); get connectedCount(): number; get totalConnections(): number; private line; private lines; /** * A CONNECTION arrived. Records it and prints NOTHING: one tab opens two * connections under HTTP/1.1, so a line here is what made one QR scan read as * two records. The creator's line comes from `testerSeen`. * * The tunnel client fires onViewer on EVERY OPEN frame, not once per * connection, so the roster's idempotence is what keeps a phone that requests * forty assets from announcing itself forty times. */ viewerSeen(label: string, uaClass: UAClass): void; viewerGone(label: string): void; /** * The harness introduced itself on a connection. This is what prints the * creator's connect line — not `viewerSeen`, which now stays silent because * one tab opens two connections and a line per connection is the defect this * fixes. */ testerSeen(label: string, tester: string): void; /** * The tester cap, once per refused session. It names the number so the creator * can act on it, and says "this connection" rather than naming a tester — * because there is none, and inventing one is the silent merge this design * exists to prevent. */ capReached(): void; events(evs: DiagEvent[], tags: EventTags, opts?: { attribute?: boolean; }): void; /** * L18. Recovery is announced by the 200 that follows and by NOTHING else — a * "recovered!" line would be a second interruption for a non-event. */ originError(): void; originRecovered(): void; /** L19. The tunnel's own state is what the creator's network failure looks like. */ tunnel(state: TunnelState): void; /** L21's soft threshold. Printed once; the preview KEEPS SERVING. */ egressSoft(): void; /** * L20b's 30-minute warning. `expiresAt` is the minted ceiling and a heartbeat * never extends it, so this fires exactly once per preview. */ checkHardMax(expiresAt: string): void; /** The creator's own Ctrl-C. Prints the stopping line, then the ending. */ stopping(): void; stopped(): void; /** Every ending the creator did NOT cause. One first line per cause. */ endedBy(reason: PreviewEndReason, orgName: string): void; following(label: string): void; notFollowing(label: string): void; notFollowingTester(tester: string): void; /** Live testers — what `stopping`/`stopped` should be counting. */ get testerCount(): number; } export declare function endingLines(reason: PreviewEndReason, orgName: string): string[]; /** `21:04` — what a creator plans a demo around, in their own local time. */ export declare function clockTime(d: Date): string;