feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
/ * *
* Persistence + runner for aggregate reports .
*
* Spec : docs / ticket - analyzer - phase2 - spec . md → Sections D . 4 – D . 6
* /
import postgresClient from '@/lib/services/postgres-client' ;
import {
type AggregateFingerprint ,
type AggregateReduceResponse ,
type AggregateReportStatus ,
type StageExecutionRecord ,
} from '@/lib/types/analyzer' ;
import { getITGlueClient } from '@/lib/services/itglue-client' ;
import { runAggregateReduceStage } from './stages/aggregate-reduce' ;
interface AggregateReportRow {
id : string ;
generated_by_user_id : string | null ;
generated_at : Date ;
filter_criteria : unknown ;
analysis_ids : string [ ] ;
ticket_count : number ;
include_itglue_context : boolean ;
report_title : string | null ;
category_distribution : Record < string , number > | null ;
client_distribution : Record < string , number > | null ;
resolution_path_distribution : Record < string , number > | null ;
root_cause_distribution : Record < string , number > | null ;
date_range_actual : { earliest : string | null ; latest : string | null } | null ;
documentation_gaps : unknown ;
process_gaps : unknown ;
client_patterns : unknown ;
recurrence_clusters : unknown ;
systemic_observations : unknown ;
recommended_actions : unknown ;
narrative_summary : string | null ;
executive_summary : string | null ;
total_input_tokens : number | null ;
total_output_tokens : number | null ;
estimated_cost_usd : string | null ;
model_used : string | null ;
itglue_context_included : boolean | null ;
status : AggregateReportStatus ;
error_message : string | null ;
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
expected_ticket_numbers : string [ ] | null ;
triggered_by_ticket_number : string | null ;
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
}
export interface AggregateReportSummary {
id : string ;
generatedByUserId : string | null ;
generatedAt : string ;
filterCriteria : unknown ;
analysisIds : string [ ] ;
ticketCount : number ;
includeItglueContext : boolean ;
reportTitle : string | null ;
status : AggregateReportStatus ;
errorMessage : string | null ;
// SQL outputs
categoryDistribution : Record < string , number > | null ;
clientDistribution : Record < string , number > | null ;
resolutionPathDistribution : Record < string , number > | null ;
rootCauseDistribution : Record < string , number > | null ;
dateRangeActual : { earliest : string | null ; latest : string | null } | null ;
// LLM outputs
documentationGaps : unknown ;
processGaps : unknown ;
clientPatterns : unknown ;
recurrenceClusters : unknown ;
systemicObservations : unknown ;
recommendedActions : unknown ;
narrativeSummary : string | null ;
executiveSummary : string | null ;
// Cost
totalInputTokens : number | null ;
totalOutputTokens : number | null ;
estimatedCostUsd : number | null ;
modelUsed : string | null ;
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
// Bundle (Phase 3)
expectedTicketNumbers : string [ ] | null ;
triggeredByTicketNumber : string | null ;
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
}
function rowToSummary ( r : AggregateReportRow ) : AggregateReportSummary {
return {
id : r.id ,
generatedByUserId : r.generated_by_user_id ,
generatedAt : r.generated_at.toISOString ( ) ,
filterCriteria : r.filter_criteria ,
analysisIds : r.analysis_ids ,
ticketCount : r.ticket_count ,
includeItglueContext : r.include_itglue_context ,
reportTitle : r.report_title ,
status : r.status ,
errorMessage : r.error_message ,
categoryDistribution : r.category_distribution ,
clientDistribution : r.client_distribution ,
resolutionPathDistribution : r.resolution_path_distribution ,
rootCauseDistribution : r.root_cause_distribution ,
dateRangeActual : r.date_range_actual ,
documentationGaps : r.documentation_gaps ,
processGaps : r.process_gaps ,
clientPatterns : r.client_patterns ,
recurrenceClusters : r.recurrence_clusters ,
systemicObservations : r.systemic_observations ,
recommendedActions : r.recommended_actions ,
narrativeSummary : r.narrative_summary ,
executiveSummary : r.executive_summary ,
totalInputTokens : r.total_input_tokens ,
totalOutputTokens : r.total_output_tokens ,
estimatedCostUsd : r.estimated_cost_usd === null ? null : Number ( r . estimated_cost_usd ) ,
modelUsed : r.model_used ,
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
expectedTicketNumbers : r.expected_ticket_numbers ,
triggeredByTicketNumber : r.triggered_by_ticket_number ,
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
} ;
}
const REPORT_SELECT = `
id : : text AS id ,
generated_by_user_id , generated_at ,
filter_criteria , analysis_ids : : text [ ] AS analysis_ids ,
ticket_count , include_itglue_context , report_title ,
category_distribution , client_distribution , resolution_path_distribution ,
root_cause_distribution , date_range_actual ,
documentation_gaps , process_gaps , client_patterns , recurrence_clusters ,
systemic_observations , recommended_actions ,
narrative_summary , executive_summary ,
total_input_tokens , total_output_tokens ,
estimated_cost_usd : : text AS estimated_cost_usd ,
model_used , itglue_context_included ,
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
status , error_message ,
expected_ticket_numbers , triggered_by_ticket_number
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
` ;
export interface CreateAggregateReportInput {
generatedByUserId : string | null ;
filterCriteria : unknown ;
analysisIds : string [ ] ;
ticketCount : number ;
includeItglueContext : boolean ;
reportTitle : string | null ;
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
/ * *
* Bundle mode ( Phase 3 ) : when set , the report is created in the
* 'pending_analyses' state and the worker will transition it to 'pending'
* once every expected ticket has a complete analysis . Leave undefined for
* the legacy manual - multi - select flow .
* /
expectedTicketNumbers? : string [ ] ;
triggeredByTicketNumber? : string ;
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
}
export async function createAggregateReport (
input : CreateAggregateReportInput
) : Promise < { id : string } > {
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
const isBundle =
Array . isArray ( input . expectedTicketNumbers ) &&
input . expectedTicketNumbers . length > 0 ;
const initialStatus = isBundle ? 'pending_analyses' : 'pending' ;
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
const res = await postgresClient . query < { id : string } > (
` INSERT INTO analyzer_aggregate_reports
( generated_by_user_id , filter_criteria , analysis_ids ,
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
ticket_count , include_itglue_context , report_title , status ,
expected_ticket_numbers , triggered_by_ticket_number )
VALUES ( $1 , $2 : : jsonb , $3 : : uuid [ ] , $4 , $5 , $6 , $7 , $8 : : text [ ] , $9 )
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
RETURNING id : : text AS id ` ,
[
input . generatedByUserId ,
JSON . stringify ( input . filterCriteria ) ,
input . analysisIds ,
input . ticketCount ,
input . includeItglueContext ,
input . reportTitle ,
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
initialStatus ,
input . expectedTicketNumbers ? ? null ,
input . triggeredByTicketNumber ? ? null ,
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
]
) ;
return { id : res.rows [ 0 ] . id } ;
}
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
/ * *
* Worker chain - trigger .
*
* Called after a single - ticket analysis completes successfully . For each
* pending_analyses report waiting on this ticket : append the analysis_id ( if
* not already present ) , and if all expected tickets now have a complete
* analysis , transition status = 'pending' and fire runAggregateReport .
*
* Idempotent : safe to invoke multiple times for the same analysis ( the
* deduplicating UPDATE skips no - ops ; the status transition is gated on the
* full set being present so the second call is a no - op ) .
* /
export async function chainTriggerForCompletedAnalysis (
ticketNumber : string ,
analysisId : string
) : Promise < { readyReportIds : string [ ] ; touchedReportIds : string [ ] } > {
const res = await postgresClient . query < {
id : string ;
expected_ticket_numbers : string [ ] ;
analysis_ids : string [ ] ;
} > (
` SELECT id::text AS id,
expected_ticket_numbers ,
analysis_ids : : text [ ] AS analysis_ids
FROM analyzer_aggregate_reports
WHERE status = 'pending_analyses'
AND expected_ticket_numbers @ > ARRAY [ $1 ] : : text [ ] ` ,
[ ticketNumber ]
) ;
const touched : string [ ] = [ ] ;
const ready : string [ ] = [ ] ;
for ( const r of res . rows ) {
if ( ! r . analysis_ids . includes ( analysisId ) ) {
await postgresClient . query (
` UPDATE analyzer_aggregate_reports
SET analysis_ids = analysis_ids || $2 : : uuid
WHERE id = $1
AND NOT ( analysis_ids @ > ARRAY [ $2 : : uuid ] ) ` ,
[ r . id , analysisId ]
) ;
touched . push ( r . id ) ;
}
// Re-check whether the full set is now satisfied: every expected ticket
// must have at least one complete analysis whose id is in analysis_ids.
// Reads the latest analysis_ids (the UPDATE above isn't reflected in the
// copy we loaded earlier).
const ready_check = await postgresClient . query < { satisfied : boolean } > (
` SELECT (
( SELECT COUNT ( DISTINCT aa . ticket_number )
FROM analyzer_analyses aa
JOIN analyzer_aggregate_reports r ON r . id = $1
WHERE aa . id = ANY ( r . analysis_ids )
AND aa . status = 'complete'
AND aa . ticket_number = ANY ( r . expected_ticket_numbers ) )
=
( SELECT array_length ( expected_ticket_numbers , 1 )
FROM analyzer_aggregate_reports WHERE id = $1 )
) AS satisfied ` ,
[ r . id ]
) ;
if ( ready_check . rows [ 0 ] ? . satisfied ) {
const transition = await postgresClient . query < { id : string } > (
` UPDATE analyzer_aggregate_reports
SET status = 'pending'
WHERE id = $1
AND status = 'pending_analyses'
RETURNING id : : text AS id ` ,
[ r . id ]
) ;
if ( transition . rowCount && transition . rowCount > 0 ) {
ready . push ( r . id ) ;
}
}
}
return { readyReportIds : ready , touchedReportIds : touched } ;
}
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
export async function getAggregateReport (
id : string
) : Promise < AggregateReportSummary | null > {
const res = await postgresClient . query < AggregateReportRow > (
` SELECT ${ REPORT_SELECT } FROM analyzer_aggregate_reports WHERE id = $ 1 ` ,
[ id ]
) ;
if ( res . rowCount === 0 ) return null ;
return rowToSummary ( res . rows [ 0 ] ) ;
}
export async function listAggregateReports ( opts : {
limit? : number ;
offset? : number ;
generatedByUserId? : string ;
} ) : Promise < AggregateReportSummary [ ] > {
const limit = Math . min ( opts . limit ? ? 50 , 200 ) ;
const offset = opts . offset ? ? 0 ;
const params : unknown [ ] = [ limit , offset ] ;
let userClause = '' ;
if ( opts . generatedByUserId ) {
params . push ( opts . generatedByUserId ) ;
userClause = ` WHERE generated_by_user_id = $ ${ params . length } ` ;
}
const res = await postgresClient . query < AggregateReportRow > (
` SELECT ${ REPORT_SELECT }
FROM analyzer_aggregate_reports
$ { userClause }
ORDER BY generated_at DESC
LIMIT $1 OFFSET $2 ` ,
params
) ;
return res . rows . map ( rowToSummary ) ;
}
interface FingerprintRow {
id : string ;
ticket_number : string ;
aggregate_fingerprint : AggregateFingerprint ;
triggered_at : Date ;
}
async function loadFingerprints (
analysisIds : string [ ]
) : Promise < { ticket_number : string ; fingerprint : AggregateFingerprint ; triggered_at : Date } [ ] > {
if ( analysisIds . length === 0 ) return [ ] ;
const res = await postgresClient . query < FingerprintRow > (
` SELECT id::text AS id, ticket_number, aggregate_fingerprint, triggered_at
FROM analyzer_analyses
WHERE id = ANY ( $1 : : uuid [ ] )
AND aggregate_fingerprint IS NOT NULL
ORDER BY ticket_number , analysis_version DESC ` ,
[ analysisIds ]
) ;
return res . rows . map ( ( r ) = > ( {
ticket_number : r.ticket_number ,
fingerprint : r.aggregate_fingerprint ,
triggered_at : r.triggered_at ,
} ) ) ;
}
function bucketCount ( items : string [ ] ) : Record < string , number > {
const out : Record < string , number > = { } ;
for ( const i of items ) out [ i ] = ( out [ i ] ? ? 0 ) + 1 ;
return out ;
}
async function fetchITGlueDocTitles (
clientNames : string [ ]
) : Promise < { client_name : string ; doc_titles : string [ ] } [ ] > {
let client ;
try {
client = getITGlueClient ( ) ;
} catch {
return [ ] ; // not configured — caller should fall back gracefully
}
const result : { client_name : string ; doc_titles : string [ ] } [ ] = [ ] ;
for ( const name of clientNames ) {
try {
const org = await client . findOrganizationByName ( name ) ;
if ( ! org ) continue ;
2026-04-29 14:48:53 -04:00
const docs = await client . getFlexibleAssetsForOrganization ( org . id ) ;
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
const titles = docs
. map ( ( d ) = > ( d as { name? : string } ) . name )
. filter ( ( t ) : t is string = > typeof t === 'string' )
. slice ( 0 , 50 ) ;
result . push ( { client_name : name , doc_titles : titles } ) ;
} catch {
// Tolerate per-client failures.
}
}
return result ;
}
const ITGLUE_DOC_TITLE_CAP = 200 ;
export async function bulkInsertReportStageExecutions (
reportId : string ,
records : StageExecutionRecord [ ]
) : Promise < void > {
if ( records . length === 0 ) return ;
const values : unknown [ ] = [ reportId ] ;
const tuples : string [ ] = [ ] ;
for ( const r of records ) {
const base = values . length ;
values . push (
r . stage ,
r . stage_order ,
r . model_id ,
JSON . stringify ( r . input_payload ? ? { } ) ,
JSON . stringify ( r . output_payload ? ? { } ) ,
r . input_tokens ,
r . output_tokens ,
r . latency_ms ,
r . started_at ,
r . completed_at ,
r . error_message
) ;
tuples . push (
` ( $ 1, $ ${ base + 1 } , $ ${ base + 2 } , $ ${ base + 3 } , $ ${ base + 4 } ::jsonb, ` +
` $ ${ base + 5 } ::jsonb, $ ${ base + 6 } , $ ${ base + 7 } , $ ${ base + 8 } , ` +
` $ ${ base + 9 } , $ ${ base + 10 } , $ ${ base + 11 } ) `
) ;
}
await postgresClient . query (
` INSERT INTO analyzer_stage_executions
( aggregate_report_id , stage , stage_order , model_id ,
input_payload , output_payload ,
input_tokens , output_tokens , latency_ms ,
started_at , completed_at , error_message )
VALUES $ { tuples . join ( ', ' ) } ` ,
values
) ;
}
/ * *
* Fire - and - forget runner . Intended to be invoked from the POST endpoint with
* ` void runAggregateReport(id) ` — the route returns immediately , this updates
* the row when work completes ( or fails ) .
* /
export async function runAggregateReport ( reportId : string ) : Promise < void > {
const stageRecords : StageExecutionRecord [ ] = [ ] ;
try {
await postgresClient . query (
` UPDATE analyzer_aggregate_reports SET status = 'running' WHERE id = $ 1 ` ,
[ reportId ]
) ;
const report = await getAggregateReport ( reportId ) ;
if ( ! report ) throw new Error ( 'report row vanished' ) ;
// ── Step 1: load fingerprints + compute SQL distributions ──
const sqlStart = new Date ( ) ;
const fingerprints = await loadFingerprints ( report . analysisIds ) ;
if ( fingerprints . length === 0 ) {
throw new Error ( 'no analyses with fingerprints found for the given IDs' ) ;
}
const categories = bucketCount ( fingerprints . map ( ( f ) = > f . fingerprint . category ) ) ;
const clients = bucketCount ( fingerprints . map ( ( f ) = > f . fingerprint . client_name ) ) ;
const resolutionPaths = bucketCount (
fingerprints . map ( ( f ) = > f . fingerprint . resolution_path )
) ;
const rootCauses = bucketCount (
fingerprints . map ( ( f ) = > f . fingerprint . root_cause_class )
) ;
const dates = fingerprints . map ( ( f ) = > f . triggered_at . getTime ( ) ) ;
const dateRange = {
earliest : new Date ( Math . min ( . . . dates ) ) . toISOString ( ) ,
latest : new Date ( Math . max ( . . . dates ) ) . toISOString ( ) ,
} ;
const sqlEnd = new Date ( ) ;
stageRecords . push ( {
stage : 'analyze' , // Reusing 'analyze' since CHECK constraint enumerates only stage names; a future migration could add 'aggregate_sql' / 'aggregate_reduce'.
stage_order : 1 ,
model_id : null ,
input_payload : { analysis_ids : report.analysisIds } ,
output_payload : {
category_distribution : categories ,
client_distribution : clients ,
resolution_path_distribution : resolutionPaths ,
root_cause_distribution : rootCauses ,
date_range_actual : dateRange ,
fingerprint_count : fingerprints.length ,
} ,
input_tokens : null ,
output_tokens : null ,
latency_ms : sqlEnd.getTime ( ) - sqlStart . getTime ( ) ,
started_at : sqlStart ,
completed_at : sqlEnd ,
error_message : null ,
} ) ;
// Persist partial results immediately so UI can show distributions.
await postgresClient . query (
` UPDATE analyzer_aggregate_reports
SET category_distribution = $2 : : jsonb ,
client_distribution = $3 : : jsonb ,
resolution_path_distribution = $4 : : jsonb ,
root_cause_distribution = $5 : : jsonb ,
date_range_actual = $6 : : jsonb
WHERE id = $1 ` ,
[
reportId ,
JSON . stringify ( categories ) ,
JSON . stringify ( clients ) ,
JSON . stringify ( resolutionPaths ) ,
JSON . stringify ( rootCauses ) ,
JSON . stringify ( dateRange ) ,
]
) ;
// ── Step 2: IT Glue context (optional) ──
let itglueDocTitles : { client_name : string ; doc_titles : string [ ] } [ ] | undefined ;
let itglueIncluded = false ;
if ( report . includeItglueContext ) {
const uniqueClients = Array . from (
new Set ( fingerprints . map ( ( f ) = > f . fingerprint . client_name ) )
) ;
const itglueStart = new Date ( ) ;
itglueDocTitles = await fetchITGlueDocTitles ( uniqueClients ) ;
// Cap to spec total (200 doc titles across all clients).
let remaining = ITGLUE_DOC_TITLE_CAP ;
itglueDocTitles = itglueDocTitles . map ( ( c ) = > {
if ( remaining <= 0 ) return { client_name : c.client_name , doc_titles : [ ] } ;
const titles = c . doc_titles . slice ( 0 , remaining ) ;
remaining -= titles . length ;
return { client_name : c.client_name , doc_titles : titles } ;
} ) ;
itglueIncluded = itglueDocTitles . some ( ( c ) = > c . doc_titles . length > 0 ) ;
const itglueEnd = new Date ( ) ;
stageRecords . push ( {
stage : 'itglue' ,
stage_order : 2 ,
model_id : null ,
input_payload : { client_count : uniqueClients.length } ,
output_payload : { doc_count : itglueDocTitles.reduce ( ( a , c ) = > a + c . doc_titles . length , 0 ) } ,
input_tokens : null ,
output_tokens : null ,
latency_ms : itglueEnd.getTime ( ) - itglueStart . getTime ( ) ,
started_at : itglueStart ,
completed_at : itglueEnd ,
error_message : null ,
} ) ;
}
// ── Step 3: reduce LLM call ──
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
// Honour the provider the bundle was created with. Manual aggregate reports
// (no provider in filter_criteria) default to anthropic.
const reduceProvider : 'anthropic' | 'openrouter' =
( report . filterCriteria as { provider? : string } | null ) ? . provider === 'openrouter'
? 'openrouter'
: 'anthropic' ;
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
const reduceStart = new Date ( ) ;
let reduceResult ;
try {
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
reduceResult = await runAggregateReduceStage (
{
distributions : {
category_distribution : categories ,
client_distribution : clients ,
resolution_path_distribution : resolutionPaths ,
root_cause_distribution : rootCauses ,
date_range_actual : dateRange ,
} ,
fingerprints : fingerprints.map ( ( f ) = > ( {
ticket_number : f.ticket_number ,
fingerprint : f.fingerprint ,
} ) ) ,
itglue_doc_titles : itglueDocTitles ,
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
} ,
feat: RMM Overshell, IT Glue audit/write-back, LogLift, link-aware bundles, dashboard overhaul
- RMM Overshell (migration 077): admin page, dispatch UI, executor/worker, target
resolver, script registry (AD/DHCP/DNS/event-log/services/software/network/loglift)
- LogLift evidence pipeline (migration 078): upload webhook, B2 storage client,
receiver/matcher, EventLogCollector PowerShell script
- IT Glue audit + write-back (migrations 075, 076): asset-audit runner, ticket
xrefs, applications/configurations browse pages + apply/revert/audit endpoints
- Link-aware analyzer bundles (migration 073) + provider toggle (migration 074):
link-discovery service, OpenRouter LLM provider, related-tickets/itglue-suggestion
panels, analyze-bundle endpoint
- Endpoint data model + device-link reconciliation (migrations 079, 080): conflicts
admin page, reconciler service, resolve endpoints
- Dashboard overhaul: integration-health service + alerts, overview/health endpoints
- Permissions: add itglue + rmm scopes; middleware: public /api/rmm/loglift route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 07:13:18 -04:00
{ provider : reduceProvider }
) ;
feat(analyzer): Phase 2 — full stage persistence, fingerprints, aggregate reports, cost guards
Eight sub-phases per docs/ticket-analyzer-phase2-spec.md:
2.1 Schema (migration 070): analyzer_stage_executions table; source_snapshot,
aggregate_fingerprint, fingerprint_generated_at columns on analyzer_analyses.
model_traces marked LEGACY (kept for back-compat).
2.2 Every pipeline stage records a row to analyzer_stage_executions, success
or failure. Worker persists a status='failed' analyzer_analyses row when
the pipeline throws so partial stage records have a parent. Pipeline
exposes raw triage/sonnet/opus responses for downstream stages.
2.3 Stage 3 prompt updated with markdown formatting rules + banned filler
phrases. Added react-markdown + remark-gfm + @tailwindcss/typography.
New <AnalysisMarkdown> component replaces <ProseText>; coerces stray
headers to bold paragraphs.
2.4 Stage 6 fingerprint (Haiku) runs after persistence, failure-tolerant.
scripts/backfill-fingerprints.ts reconstructs Stage 6 input from the
legacy model_traces blob.
2.5 Browse UI rebuild at /analyzer/tickets: multi-select for client/issue/
queue/status/priority/assignee, sticky filter bar, active-filter chips,
bulk selection persisted via localStorage, "Analyze N selected" +
"Generate aggregate report" actions. New <MultiSelect> primitive.
Staleness uses last_activity_date > completed_at heuristic per spec C.1.
2.6 Aggregate reports (migration 071): runner is fire-and-forget, persists
SQL distributions immediately so UI shows partial state during the
Sonnet reduce call. Three endpoints, three pages (/analyzer/reports[/new
/:id]). IT Glue context fetcher capped at 200 doc titles.
2.7 Cost guards (migration 072): per-request $5 confirmation, soft-warn at
$20/day, hard-block at $50/day with ANALYZER_DAILY_COST_OVERRIDE_USERS
override. Every gating decision audited.
2.8 Runbook + build notes updated.
128 vitest tests passing, tsc clean. Migrations 070/071/072 idempotent
(IF NOT EXISTS). model_traces double-write retained — drop in a future
migration once aggregate reports have soaked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:00:22 -04:00
} catch ( err ) {
const reduceEnd = new Date ( ) ;
stageRecords . push ( {
stage : 'analyze' ,
stage_order : 3 ,
model_id : null ,
input_payload : { fingerprint_count : fingerprints.length } ,
output_payload : { } ,
input_tokens : null ,
output_tokens : null ,
latency_ms : reduceEnd.getTime ( ) - reduceStart . getTime ( ) ,
started_at : reduceStart ,
completed_at : reduceEnd ,
error_message : err instanceof Error ? err.message : String ( err ) ,
} ) ;
throw err ;
}
const reduceEnd = new Date ( ) ;
stageRecords . push ( {
stage : 'analyze' ,
stage_order : 3 ,
model_id : reduceResult.model_used ,
input_payload : { fingerprint_count : fingerprints.length } ,
output_payload : reduceResult.data ,
input_tokens : reduceResult.usage.input_tokens ,
output_tokens : reduceResult.usage.output_tokens ,
latency_ms : reduceEnd.getTime ( ) - reduceStart . getTime ( ) ,
started_at : reduceStart ,
completed_at : reduceEnd ,
error_message : null ,
} ) ;
// ── Step 4: persist outputs ──
await postgresClient . query (
` UPDATE analyzer_aggregate_reports
SET documentation_gaps = $2 : : jsonb ,
process_gaps = $3 : : jsonb ,
client_patterns = $4 : : jsonb ,
recurrence_clusters = $5 : : jsonb ,
systemic_observations = $6 : : jsonb ,
recommended_actions = $7 : : jsonb ,
narrative_summary = $8 ,
executive_summary = $9 ,
total_input_tokens = $10 ,
total_output_tokens = $11 ,
estimated_cost_usd = $12 ,
model_used = $13 ,
itglue_context_included = $14 ,
status = 'complete'
WHERE id = $1 ` ,
[
reportId ,
JSON . stringify ( reduceResult . data . documentation_gaps ) ,
JSON . stringify ( reduceResult . data . process_gaps ) ,
JSON . stringify ( reduceResult . data . client_patterns ) ,
JSON . stringify ( reduceResult . data . recurrence_clusters ) ,
JSON . stringify ( reduceResult . data . systemic_observations ) ,
JSON . stringify ( reduceResult . data . recommended_actions ) ,
reduceResult . data . narrative_summary ,
reduceResult . data . executive_summary ,
reduceResult . usage . input_tokens ,
reduceResult . usage . output_tokens ,
reduceResult . estimated_cost_usd ,
reduceResult . model_used ,
itglueIncluded ,
]
) ;
await bulkInsertReportStageExecutions ( reportId , stageRecords ) ;
} catch ( err ) {
const message = err instanceof Error ? err.message : String ( err ) ;
console . error ( ` [ANALYZER-REPORT] runAggregateReport ${ reportId } failed: ` , message ) ;
await postgresClient
. query (
` UPDATE analyzer_aggregate_reports
SET status = 'failed' , error_message = $2
WHERE id = $1 ` ,
[ reportId , message ]
)
. catch ( ( ) = > { } ) ;
if ( stageRecords . length > 0 ) {
await bulkInsertReportStageExecutions ( reportId , stageRecords ) . catch ( ( ) = > { } ) ;
}
}
}