/**
 * Dashboard handler for the orchestrator.
 *
 * Responds to dashboard.* and run.* WS messages from Platform by querying the local
 * execution_jobs and execution_steps tables and reading logs from LogStorage.
 *
 * Supported messages:
 * - dashboard.run.detail: returns jobs with nested steps for a run
 * - dashboard.step.logs: returns log lines for a specific step
 * - dashboard.payload: returns the original webhook payload for a run
 * - run.rerun.request: re-runs a completed workflow run
 * - run.cancel.request: cancels a running workflow run
 */
import { type Kysely } from 'kysely';
import { type ColdStore } from '@kici-dev/shared';
import type { DashboardRunDetailRequest, DashboardRunsListRequest, DashboardRunsListResponse, DashboardRunsFiltersRequest, DashboardRunsFiltersResponse, DashboardSourcesListRequest, DashboardSourcesListResponse, DashboardStepLogsRequest, DashboardPayloadRequest, DashboardOrchLogsRequest, DashboardEventLogListRequest, DashboardEventLogDetailRequest, DashboardEventLogPayloadStreamRequest, DashboardAccessLogListRequest, DashboardEventDlqListRequest, DashboardEventDlqCountRequest, DashboardEventDlqRetryRequest, DashboardEventDlqDiscardRequest, RunRerunRequest, RunCancelRequest, ManualScheduleRequest } from '@kici-dev/engine';
import type { Database } from '../db/types.js';
import type { LogStorage } from '../reporting/log-storage.js';
import type { AccessLogWriter } from '../audit/access-log.js';
import type { EventStore } from '../events/event-store.js';
interface DashboardHandlerDeps {
    db: Kysely<Database>;
    logStorage: LogStorage;
    /** Send a response message back to Platform over the WS connection. */
    send: (msg: unknown) => void;
    /** This orchestrator's instance ID, included in job detail responses. */
    orchestratorId?: string;
    /** Access log writer — records one row per read / mutation with actor attribution. */
    accessLog?: AccessLogWriter;
    /** Org ID for access_log rows (null when the orchestrator isn't org-scoped). */
    orgId?: string | null;
    /** Routing key for access_log rows (null when not run-scoped). */
    routingKey?: string | null;
    /**
     * Long-lived cold-store handle for read fallback. When set, run /
     * job / step lookups fall through to S3 if the row has aged out of
     * PG. `null` means the orchestrator is running without cold-store
     * configured — handler then returns its pre-Phase-C behavior.
     */
    coldStore?: ColdStore | null;
    /**
     * Callback for handling re-run requests.
     * Returns { newRunId } on success or throws on failure.
     *
     * Phase F — `routingKey` is forwarded from the Platform (read from
     * Platform's denormalized `execution_runs.routing_key`) so the
     * orchestrator can probe its cold-store under the right tenant
     * prefix when the run row is missing from PG. Optional for back-
     * compat with mixed deploys; absent means the orchestrator falls
     * back to the legacy "no PG row → throw" path.
     */
    onRerun: (runId: string, triggeredBy: string | null, routingKey?: string) => Promise<{
        newRunId: string;
    }>;
    /**
     * Callback for handling cancel requests.
     * Returns { cancelledJobs } on success or throws on failure.
     * The force flag indicates whether to force-cancel (SIGKILL, skip hooks).
     */
    onCancel: (runId: string, cancelledBy: string | null, force?: boolean) => Promise<{
        cancelledJobs: number;
    }>;
    /**
     * Callback for handling manual schedule trigger requests.
     * Returns { newRunId } on success or throws on failure.
     */
    onManualSchedule: (registrationId: string, triggeredBy: string | null) => Promise<{
        newRunId: string;
    }>;
    /**
     * Event store for the per-org DLQ surface. Optional — when absent the
     * `handleEventDlq*` methods reply with a structured `error` so the
     * dashboard renders an empty-state without crashing. In real
     * deployments the store is wired by `bootstrapOrchestrator`.
     */
    eventStore?: EventStore | null;
}
export declare class DashboardHandler {
    private readonly db;
    private readonly logStorage;
    private readonly send;
    private readonly orchestratorId;
    private readonly accessLog;
    private orgId;
    private routingKey;
    private readonly onRerun;
    private readonly onCancel;
    private readonly onManualSchedule;
    private readonly coldStore;
    private readonly eventStore;
    constructor(deps: DashboardHandlerDeps);
    /**
     * Update the bound orgId + routingKey. Called from server.ts after resolving
     * the single tenant org from the `sources` / `generic_webhook_sources` table.
     *
     * NOTE: this binding is the **fallback** for `recordAccess`. Run-targeted
     * handlers resolve the run-owning org per-request via `resolveOrgForRun`;
     * the bound pair is consulted only when the per-target lookup yields no
     * result (e.g. cold-archived run, deleted source). On a multi-tenant
     * orchestrator the bound pair is non-deterministic (server.ts:937-955
     * picks the first row from a `LIMIT 1` query with no `ORDER BY`), so it
     * MUST NOT be the source of truth for `access_log.org_id`.
     */
    setOrgContext(orgId: string | null, routingKey: string | null): void;
    /**
     * Resolve the run-owning org from `execution_runs.routing_key` plus a
     * sources/generic_webhook_sources lookup on that routing key. Returns
     * `null` when the run row is missing (e.g. cold-archived) so the caller
     * can fall back to the handler-bound context.
     *
     * Returns `{ orgId, routingKey }` where either field can be null:
     * - `routingKey` is non-null whenever the PG run row was found, even if
     *   the source row has since been deleted (best-effort attribution).
     * - `orgId` is null only when the routing key has no matching source row.
     */
    private resolveOrgForRun;
    /**
     * Resolve the org owning a routing key by consulting both source tables
     * (GitHub-app `sources` first, then `generic_webhook_sources`). Used
     * directly for `handleRerunRequest` (which gets a `routingKey` hint from
     * Platform — Phase F denormalization) and indirectly via
     * `resolveOrgForRun`.
     */
    private resolveOrgForRoutingKey;
    /**
     * Resolve the org from a `workflow_registrations` row. Returns null when
     * the registration is missing (caller falls back to bound context).
     */
    private resolveOrgForRegistration;
    /**
     * Apply a per-target resolution result, falling back to the handler-bound
     * orgId / routingKey when the resolution is null or its individual fields
     * are null. This is the merge point that makes the bound context a
     * fallback rather than the source of truth.
     */
    private contextOrFallback;
    /**
     * Defense-in-depth dashboard-write policy gate for the DLQ handlers.
     * Returns true when allowed; false (with a `denied` access_log row and
     * an `operation_disabled` envelope on the wire) when the orch policy
     * has the operation switched off.
     */
    private enforcePolicy;
    /**
     * Write an access_log row for a handler invocation. The caller resolves
     * the run-owning org via `resolveOrgForRun` (or the registration / event-
     * log equivalents) and threads it in here so each row carries the org
     * that owns the target — not the handler-bound (multi-tenant: non-
     * deterministic) pair. Best-effort; the writer swallows failures.
     */
    private recordAccess;
    /**
     * Handle a dashboard.run.detail request.
     * Queries execution_jobs and execution_steps for the given runId,
     * builds a nested job/step tree, and sends the response.
     */
    handleRunDetail(msg: DashboardRunDetailRequest): Promise<void>;
    /**
     * Resolve every routing key owned by an org by unioning both source
     * tables. The orchestrator is single-org but multi-routing-key: one org
     * owns one GitHub-app source plus N generic sources, each with its own
     * `routing_key`. `execution_runs` has no `org_id` column — org ownership
     * is expressed purely through `routing_key → sources.customer_id` /
     * `generic_webhook_sources.customer_id`, so scoping a run query to an org
     * means scoping it to the union of that org's routing keys.
     *
     * Predicates mirror the org-context resolver in `server.ts` and
     * `resolveOrgForRoutingKey` above:
     * - `sources`: `customer_id = orgId` excluding the `__default__` sentinel.
     * - `generic_webhook_sources`: `customer_id = orgId` and not soft-deleted
     *   (`deleted_at is null`).
     */
    private resolveOrgRoutingKeys;
    /**
     * Resolve a per-routing-key source identity map for a set of routing keys,
     * unioning both source tables. Uses the identical name/subtype/provider
     * derivation as `handleSourcesList` so a run summary's `source` block
     * matches what the sources page shows for the same routing key. Routing
     * keys with no live source row are simply absent from the map (the caller
     * falls back to the run's own provider).
     */
    private resolveSourceIdentities;
    /**
     * Look up the page's jobs in one query to derive per-run `jobCount` plus the
     * compile-job markers (`hadCompileJob` / `compileJobId`). The compile job is
     * the synthetic `__build__*` job KiCI inserts for the compile phase — the
     * same definition the Platform run-list route uses
     * (`job_name LIKE '__build__%'`). Returns a per-run aggregate keyed by
     * `run_id`; runs with no job rows are absent (the caller omits the fields).
     */
    private resolveRunJobAggregates;
    /**
     * Handle a dashboard.runs.list request.
     *
     * Returns a page of run summaries from `execution_runs`, scoped to ALL
     * routing keys owned by this orchestrator's bound org (`this.orgId`). The
     * orchestrator is single-org but multi-routing-key, and the bound
     * `this.routingKey` is just ONE of the org's keys (resolved
     * non-deterministically at startup), so filtering on it alone would drop
     * runs that arrived under a sibling routing key. We resolve the org's full
     * routing-key set from `sources` + `generic_webhook_sources` and filter on
     * the union. Attribution still uses the handler-bound `orgId` /
     * `routingKey` directly — the same inline-ctx pattern as the org-scoped
     * event-log / access-log handlers.
     *
     * Used by the operator console (`support-read` break-glass) and the
     * dashboard's run-list view. The access_log row carries the wire actor
     * (including the platform_operator reason) so the customer can audit any
     * operator read.
     */
    handleRunsList(msg: DashboardRunsListRequest): Promise<DashboardRunsListResponse>;
    /**
     * Handle a dashboard.runs.filters request.
     *
     * Returns the distinct filter-option values the customer runs page renders
     * in its filter controls — statuses / workflows / branches / repositories /
     * triggerTypes / sources — scoped to ALL routing keys owned by this
     * orchestrator's bound org (same scoping as `handleRunsList`). Distinct
     * values are derived in-memory from a single column projection over
     * `execution_runs`; `sources` reuses `resolveSourceIdentities` so the list
     * matches what the sources page shows for the same routing key.
     *
     * `triggerTypes` is populated from `execution_runs.provider` — the
     * orchestrator has no dedicated trigger-event column, so `provider`
     * (`github` / `generic` / `internal`) is the closest available
     * discriminator the customer filter can offer.
     *
     * Attribution uses the handler-bound `orgId` / `routingKey` directly — the
     * same inline-ctx pattern as `handleRunsList`. The access_log row carries
     * the wire actor (including the platform_operator reason) so the customer
     * can audit any operator read.
     */
    handleRunsFilters(msg: DashboardRunsFiltersRequest): Promise<DashboardRunsFiltersResponse>;
    /**
     * Handle a dashboard.sources.list request.
     *
     * Returns the org's webhook source summaries by unioning both source
     * tables (GitHub-app `sources` + live `generic_webhook_sources`), scoped
     * to this orchestrator's bound org (`this.orgId`). Source counts are tiny
     * so the page is returned unpaginated (`msg.limit` is ignored). Secret-
     * bearing columns (`config`, `git_config`, `verification_config`) are
     * never projected — `git_config` is read only to derive the subtype.
     *
     * Attribution uses the handler-bound `orgId` / `routingKey` directly —
     * the same inline-ctx pattern as `handleRunsList`. The access_log row
     * carries the wire actor so the customer can audit any operator read.
     */
    handleSourcesList(msg: DashboardSourcesListRequest): Promise<DashboardSourcesListResponse>;
    /**
     * Handle a dashboard.step.logs request.
     * Looks up the step's log_path and reads content from LogStorage.
     */
    handleStepLogs(msg: DashboardStepLogsRequest): Promise<void>;
    /**
     * Handle a dashboard.payload request.
     * Reads the webhook payload from log storage for the given runId.
     */
    handlePayload(msg: DashboardPayloadRequest): Promise<void>;
    /**
     * Handle a dashboard.orch.logs request.
     * Reads orchestration + provisioning log files from LogStorage for the given run/job.
     * Provisioning logs are returned with phase "provisioning" so the dashboard can
     * render them in a separate collapsible section.
     */
    handleOrchLogs(msg: DashboardOrchLogsRequest): Promise<void>;
    /**
     * Handle a run.rerun.request.
     * Delegates to the onRerun callback which invokes handleRerun from the rerun module.
     *
     * Resolution: prefer `msg.routingKey` (Phase F denormalization hint from
     * Platform — `execution_runs.routing_key` mirror). The hint is the ONLY
     * org context we have when the run row has been cold-archived out of PG,
     * so consult sources/generic_webhook_sources directly. If the hint is
     * absent, fall back to `resolveOrgForRun` against the warm PG row.
     */
    handleRerunRequest(msg: RunRerunRequest): Promise<void>;
    /**
     * Handle a run.cancel.request.
     * Delegates to the onCancel callback which sends job.cancel to agents.
     */
    handleCancelRequest(msg: RunCancelRequest): Promise<void>;
    /**
     * Handle a dashboard.event-log.list request.
     *
     * Returns a paginated page of inbound webhook delivery rows owned by this
     * orchestrator (`event_log` table) for the given org. Pagination uses an
     * opaque cursor (received_at + id) so concurrent inserts don't shift pages.
     *
     * Filters: routingKey, event, status, fromTimestamp/toTimestamp,
     * deliveryId substring. All optional.
     */
    handleEventLogList(msg: DashboardEventLogListRequest): Promise<void>;
    /**
     * Handle a dashboard.access-log.list request.
     *
     * Returns a paginated page of access_log rows for the given org. Used by
     * the dashboard "Data access" tab to show who read what (and when).
     *
     * Every filter is optional; the orchestrator scopes on orgId then applies
     * each supplied filter independently. Records its own access_log row
     * under action='access_log.list.read' so there's a meta-audit of who
     * looked at the audit log.
     */
    handleAccessLogList(msg: DashboardAccessLogListRequest): Promise<void>;
    /**
     * Handle a dashboard.event-log.detail request.
     *
     * Returns the metadata row only — the webhook body bytes stream over a
     * separate chunked-WS path (`handleEventLogPayloadStream` below) so
     * Platform never holds the full body in memory and the dashboard can
     * render progress as bytes arrive. When `payload_omitted=true` the row's
     * `payloadOmitted`/`payloadOmittedReason` fields tell the dashboard to
     * skip issuing a stream request.
     */
    handleEventLogDetail(msg: DashboardEventLogDetailRequest): Promise<void>;
    /**
     * Handle a dashboard.event-log.payload.stream request.
     *
     * Reads the gzipped body from LogStorage, decompresses it, slices the
     * decompressed bytes into 64 KiB chunks, base64-encodes each, and emits a
     * sequence of `dashboard.event-log.payload.chunk` messages. The first
     * chunk carries `totalBytes`; the last chunk carries `isLast=true`.
     *
     * The single audit row (`event_log.payload.read`) is written at the start
     * of the stream — outcome `allowed` when the row exists and the body
     * begins streaming, `not_found` / `denied` when the request resolves to
     * an empty/error stream. Per-chunk audits would inflate the access-log
     * volume without adding attribution detail.
     *
     * Pacing: a `setImmediate` yield between sends keeps the WS connection
     * from saturating. For a 5 MB cap (the wishlist's worst case) this means
     * ~80 yields per stream — negligible.
     */
    handleEventLogPayloadStream(msg: DashboardEventLogPayloadStreamRequest): Promise<void>;
    /**
     * Handle a dashboard.event-dlq.list request.
     *
     * Returns a paginated page of DLQ rows (events whose dispatch attempts
     * exhausted the retry budget). The page is keyed off `dlq_at DESC` and
     * paginates with a single `before` ISO cursor.
     *
     * Records one `event_dlq.list.read` access_log entry using the user
     * actor on the wire — mirrors the HTTP admin route's audit shape but
     * with the calling user's identity instead of the bearer-token role.
     */
    handleEventDlqList(msg: DashboardEventDlqListRequest): Promise<void>;
    /**
     * Handle a dashboard.event-dlq.count request.
     *
     * Returns the DLQ depth for the sidebar badge. Polled ~30s by the
     * dashboard. No access_log row — the count surface is intentionally
     * un-audited (it's a frequent badge poll, not a triage action).
     */
    handleEventDlqCount(msg: DashboardEventDlqCountRequest): Promise<void>;
    /**
     * Handle a dashboard.event-dlq.retry request.
     *
     * Clears the DLQ flag on the row and issues a `pg_notify` so a healthy
     * node picks the event up immediately rather than waiting for the
     * leader-only retry scanner's next tick. Notify failure is non-fatal —
     * the scanner will catch up.
     */
    handleEventDlqRetry(msg: DashboardEventDlqRetryRequest): Promise<void>;
    /**
     * Handle a dashboard.event-dlq.discard request.
     *
     * Permanently deletes the DLQ row. No retry, no archive — used when the
     * payload is corrupt or the routing target has been removed.
     */
    handleEventDlqDiscard(msg: DashboardEventDlqDiscardRequest): Promise<void>;
    /**
     * Handle a run.manual_schedule.request.
     * Delegates to the onManualSchedule callback which invokes handleManualSchedule.
     */
    handleManualScheduleRequest(msg: ManualScheduleRequest): Promise<void>;
    /**
     * Phase C cold-store fallback: fetch the run + its jobs + its steps
     * from S3 when the PG copy has been archived. Bounded scan: stops at
     * the warm cutoff (older entries don't exist in cold-store yet by
     * construction). Each row is filtered by `run_id` because chunks
     * span multiple runs.
     */
    private fetchArchivedRunDetail;
}
export {};
//# sourceMappingURL=handler.d.ts.map