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>
This commit is contained in:
lorentz 2026-05-03 07:13:18 -04:00
parent 378e68ad8a
commit 1112a06afe
132 changed files with 21352 additions and 743 deletions

View file

@ -213,6 +213,9 @@ export const PersistedAnalysis = z.object({
filteredNoiseCount: z.number().int().nonnegative(),
errorMessage: z.string().nullable(),
/** Phase 3: which LLM provider produced this analysis. */
provider: z.enum(['anthropic', 'openrouter']).default('anthropic'),
});
export type PersistedAnalysis = z.infer<typeof PersistedAnalysis>;
@ -440,6 +443,7 @@ export type AggregateReduceResponse = z.infer<typeof AggregateReduceResponse>;
export const AggregateReportStatus = z.enum([
'pending',
'pending_analyses',
'running',
'complete',
'failed',
@ -450,8 +454,12 @@ export type AggregateReportStatus = z.infer<typeof AggregateReportStatus>;
// API request bodies.
// =============================================================================
export const ProviderEnum = z.enum(['anthropic', 'openrouter']);
export type ProviderEnum = z.infer<typeof ProviderEnum>;
export const AnalyzeTicketRequest = z.object({
force: z.boolean().optional().default(false),
provider: ProviderEnum.optional().default('anthropic'),
});
export type AnalyzeTicketRequest = z.infer<typeof AnalyzeTicketRequest>;
@ -461,6 +469,112 @@ export const ShareAnalysisRequest = z.object({
});
export type ShareAnalysisRequest = z.infer<typeof ShareAnalysisRequest>;
// =============================================================================
// Phase 3 — link discovery & bundle analysis.
// =============================================================================
export const LinkConfidence = z.enum(['high', 'medium']);
export type LinkConfidence = z.infer<typeof LinkConfidence>;
export const LinkSource = z.enum([
'related_tickets_section',
'description_mention',
'note_mention',
'problem_ticket_id',
'llm_suggested',
]);
export type LinkSource = z.infer<typeof LinkSource>;
export const TicketRef = z.object({
ticket_number: z.string(),
title: z.string().nullable(),
status_label: z.string().nullable(),
last_activity_date: z.string().datetime({ offset: true }).nullable(),
source: LinkSource,
confidence: LinkConfidence,
reason: z.string().nullable(),
});
export type TicketRef = z.infer<typeof TicketRef>;
export const DiscoveredLinks = z.object({
explicit: z.array(TicketRef),
suggested: z.array(TicketRef),
isProblemTicket: z.boolean(),
problemTicketSignals: z.array(z.string()),
});
export type DiscoveredLinks = z.infer<typeof DiscoveredLinks>;
export const BundleAnalyzeRequest = z.object({
linkedTicketNumbers: z.array(z.string()).max(50),
includeItglueContext: z.boolean().optional().default(true),
reportTitle: z.string().max(200).nullable().optional(),
confirmedCost: z.boolean().optional().default(false),
provider: ProviderEnum.optional().default('anthropic'),
});
export type BundleAnalyzeRequest = z.infer<typeof BundleAnalyzeRequest>;
export const SuggestLinksRequest = z.object({
includeSuggested: z.boolean().optional().default(true),
});
export type SuggestLinksRequest = z.infer<typeof SuggestLinksRequest>;
// =============================================================================
// Phase 4 — IT Glue asset audit (LLM-driven gap analysis vs ticket history).
// =============================================================================
export const AuditConfidence = z.enum(['high', 'medium', 'low']);
export type AuditConfidence = z.infer<typeof AuditConfidence>;
export const AssetAuditFieldGap = z.object({
field_name: z.string(),
why_missing_matters: z.string(),
suggested_value: z.string().nullable(),
evidence_ticket_numbers: z.array(z.string()),
confidence: AuditConfidence,
});
export type AssetAuditFieldGap = z.infer<typeof AssetAuditFieldGap>;
export const AssetAuditNotePromotion = z.object({
quoted_note_text: z.string(),
target_field: z.string(),
suggested_value: z.string(),
confidence: AuditConfidence,
});
export type AssetAuditNotePromotion = z.infer<typeof AssetAuditNotePromotion>;
export const AssetAuditContradiction = z.object({
description: z.string(),
evidence: z.string(),
});
export type AssetAuditContradiction = z.infer<typeof AssetAuditContradiction>;
export const AssetAuditResponse = z.object({
field_gaps: z.array(AssetAuditFieldGap),
notes_promotions: z.array(AssetAuditNotePromotion),
contradictions: z.array(AssetAuditContradiction),
overall_score: z.number().min(0).max(1),
});
export type AssetAuditResponse = z.infer<typeof AssetAuditResponse>;
export const RunAssetAuditRequest = z.object({
provider: ProviderEnum.optional().default('anthropic'),
});
export type RunAssetAuditRequest = z.infer<typeof RunAssetAuditRequest>;
export const ApplyAssetSuggestionRequest = z.object({
auditId: z.string().uuid(),
fieldName: z.string(),
suggestedValue: z.unknown(),
/** ticket_numbers + gap description for the source_evidence row. */
sourceEvidence: z
.object({
ticket_numbers: z.array(z.string()).optional(),
gap_description: z.string().optional(),
})
.optional(),
});
export type ApplyAssetSuggestionRequest = z.infer<typeof ApplyAssetSuggestionRequest>;
// =============================================================================
// Internal pipeline payload — passed between stages, NOT a wire format.
// =============================================================================