- 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>
604 lines
25 KiB
Markdown
604 lines
25 KiB
Markdown
# Feature: IT Glue Asset Audit & Documentation Write-back
|
||
|
||
**Status:** shipped (Phases 4 + 4.1 of the AI Ticket Analyzer)
|
||
**Migrations:** `075_itglue_audit.sql`, `076_itglue_ticket_xrefs.sql`
|
||
**Build notes:** `docs/wulf-pulse-ticket-analyzer-build-notes.md` → Phase 4 + 4.1
|
||
**Operator runbook:** `docs/wulf-pulse-ticket-analyzer-runbook.md` → "IT Glue asset audits"
|
||
|
||
---
|
||
|
||
## What it is, in one paragraph
|
||
|
||
For each IT Glue **flexible-asset Application record** *and* **Configuration
|
||
record** (server, workstation, network device), Pulse runs an LLM audit
|
||
that compares the record's current contents against (a) the field schema
|
||
with hints, (b) other well-filled records at the same client, (c) recent
|
||
ticket history that mentions the asset. It surfaces **field gaps** ("Wulf
|
||
Application Champion is empty — Jake Hammel is the de-facto SME per
|
||
T20260502.0033"), **note promotions** (free text in Notes that belongs in a
|
||
structured field), and **contradictions** (Notes say "2-3 VMs" but
|
||
Application-on-Device tags only 1). Admins can apply a suggestion with one
|
||
click — Pulse PATCHes IT Glue and records every change with full
|
||
before/after diff and revert capability.
|
||
|
||
Two complementary entry points:
|
||
|
||
- **Asset-first** (Phase 4) — admin browses lowest-scoring Application or
|
||
Configuration records and runs an audit against all-time history. Useful
|
||
for periodic backlog sweeps.
|
||
- **Ticket-first** (Phase 4.1) — every analyzed ticket gets a "Check IT
|
||
Glue documentation" button on its analysis page. Click → matched IT Glue
|
||
records appear → audit per record uses *just this ticket's evidence*.
|
||
Findings frame as "what did this ticket teach us that the documentation
|
||
doesn't say?" Drives a forward-only **cross-reference index**
|
||
(`itglue_ticket_xrefs`) of which tickets referenced or updated which
|
||
documentation, which is also the lookup index for a future RAG
|
||
automation.
|
||
|
||
---
|
||
|
||
## Why we built it
|
||
|
||
The analyzer already extracts `documentation_gaps_observed` per ticket
|
||
(Phase 2.4 fingerprint). Until Phase 4 nothing acted on the signal. The
|
||
catalyzing example was T20260502.0033 (Hynes — tags not printing from
|
||
Simple Shop Floor), resolved by Collin + Jake identifying a stopped Windows
|
||
service on MISYS-SQL processing BarTender scan-folder text files. The IT
|
||
Glue Application record `MISYS 6.3` had **5/17 fields filled**, missing:
|
||
|
||
- Wulf Application Champion (Jake is the SME, but only by tribal knowledge)
|
||
- Vendor Maintenance/Support (escalation contact "Steve Cianflone" buried
|
||
in free-text Notes)
|
||
- The integration architecture (Simple Shop Floor → MISYS-SQL → BarTender
|
||
scan folder)
|
||
- The named services on each VM
|
||
|
||
The next tech who hits this issue would re-discover everything. Phase 4
|
||
turns the analyzer's passive "documentation_gaps_observed" output into an
|
||
actionable backlog with a write-back path.
|
||
|
||
---
|
||
|
||
## User-facing flows
|
||
|
||
### List view — `/analyzer/itglue/applications`
|
||
|
||
Shows every Application record across all clients, sorted by **lowest audit
|
||
score first** (so unaudited and worst-scored assets bubble up). Each row:
|
||
asset name, client, populated-field count, last-audit timestamp +
|
||
provider, score badge.
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────────────────┐
|
||
│ MISYS 6.3 ▌ 55% │
|
||
│ Hynes Industries · 5 fields populated · last audited today (Claude)│
|
||
└────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Detail view — `/analyzer/itglue/applications/[id]`
|
||
|
||
Three regions:
|
||
|
||
1. **Header** — asset name, client, score badge, "Open in IT Glue" link, the
|
||
`<ProviderToggle>` (Claude vs DeepSeek), and a primary **Run audit** button.
|
||
2. **Audit findings** (visible after a run) —
|
||
- **Field gaps** — severity-toned cards (red/amber/blue by confidence),
|
||
each with: field name, why-it-matters, suggested value (when the LLM
|
||
has evidence), evidence ticket links, and an **Apply** button.
|
||
- **Promote from Notes** — quoted substring → target field → suggested
|
||
structured value, with **Apply**.
|
||
- **Contradictions** — pure observations (no Apply, by design).
|
||
3. **Current fields** — every field rendered in IT Glue's order; populated
|
||
shown normally, empty shown muted with the field's hint inline as
|
||
guidance.
|
||
4. **Write history** — every Apply/Revert for this asset, with inline
|
||
before/after diffs and a **Revert** button on committed writes.
|
||
5. **Audit history** — score over time across runs.
|
||
|
||
### Admin view — `/admin/itglue-writes`
|
||
|
||
Cross-asset write log, status filters (pending / committed / failed /
|
||
reverted), full diff per row, ordered most-recent-first.
|
||
|
||
---
|
||
|
||
## Permissions
|
||
|
||
| Action | Permission |
|
||
|---|---|
|
||
| Read audit / View asset detail | `requireAuth()` (any signed-in user) |
|
||
| Run a fresh audit | `requireAuth()` |
|
||
| Apply a suggestion (PATCH IT Glue) | `requirePermission('itglue', 'write')` |
|
||
| Revert a previous write | `requirePermission('itglue', 'write')` |
|
||
| `/admin/itglue-writes` cross-asset feed | `requirePermission('admin', 'access')` |
|
||
|
||
`itglue.write` is granted to `admin` and `super-admin` roles only (see
|
||
`lib/permissions.ts`). The Apply / Revert buttons render on the page for
|
||
non-admins but are **disabled** with a tooltip explaining why.
|
||
|
||
---
|
||
|
||
## Pipeline
|
||
|
||
Single LLM call, provider-aware via `stageModelsFor(provider).deep_analysis`
|
||
(Claude Sonnet for Anthropic; DeepSeek V4 Pro for OpenRouter).
|
||
|
||
### Inputs (six context arms)
|
||
|
||
1. **Asset snapshot** — the asset's current `traits` JSONB, redacted via
|
||
`lib/services/analyzer/itglue-redact.ts` to strip any password/secret/
|
||
key/token/credential-keyed values.
|
||
2. **Field schema with hints** — every field on the asset type, in IT
|
||
Glue's display order, with the `hint` text IT Glue surfaces in its own
|
||
editor (e.g. *"Tag any application servers or Devices it's installed
|
||
on"*).
|
||
3. **Peer exemplars from same client** — top 5 most-filled flexible
|
||
assets of the same type at the same client (ranked by populated trait
|
||
keys), redacted.
|
||
4. **Best-in-class peers across all clients** — top 3 most-filled
|
||
instances of the same asset type globally, redacted.
|
||
5. **Per-field fill-rate stats** — the % of (a) this client's assets and
|
||
(b) all assets of this type that have field X populated. Pure SQL
|
||
aggregate; no token cost. Lets the LLM rank gaps by "unusual to be
|
||
missing" vs "usually missing anyway."
|
||
6. **Ticket evidence** — up to 20 most-recent complete analyses for the
|
||
client whose summary or fingerprint mentions the asset name, with
|
||
their `aggregate_fingerprint` payload included in full.
|
||
|
||
### Output schema (`AssetAuditResponse` in `lib/types/analyzer.ts`)
|
||
|
||
```ts
|
||
{
|
||
field_gaps: Array<{
|
||
field_name: string, // must exist on the asset type
|
||
why_missing_matters: string,
|
||
suggested_value: string | null, // null if no concrete evidence
|
||
evidence_ticket_numbers: string[],
|
||
confidence: 'high' | 'medium' | 'low',
|
||
}>,
|
||
notes_promotions: Array<{
|
||
quoted_note_text: string, // exact substring of Notes
|
||
target_field: string,
|
||
suggested_value: string,
|
||
confidence: 'high' | 'medium' | 'low',
|
||
}>,
|
||
contradictions: Array<{
|
||
description: string,
|
||
evidence: string,
|
||
}>,
|
||
overall_score: number, // 0..1 self-rated completeness
|
||
}
|
||
```
|
||
|
||
### System-prompt rules (highlights)
|
||
|
||
- Never invent a field name not present in the schema.
|
||
- Never suggest a value you cannot point to evidence for — use `null`
|
||
instead.
|
||
- Don't suggest password/secret/key/token/credential-shaped fields. (The
|
||
Apply endpoint also blocks these defensively.)
|
||
- Use fill-rate stats: a gap that's empty here but populated >80%
|
||
elsewhere is a stronger gap than one that's empty 80% of the time
|
||
globally.
|
||
|
||
### Pipeline files
|
||
|
||
- `lib/services/analyzer/asset-audit/data-builder.ts` — collects all six
|
||
inputs.
|
||
- `lib/services/analyzer/asset-audit/prompt.ts` — system prompt + payload
|
||
builder (with 80KB cap; trims peer_global first, oldest tickets next).
|
||
- `lib/services/analyzer/asset-audit/runner.ts` — single `callLLMStage`
|
||
call, persistence on success or failure.
|
||
- `lib/services/analyzer/asset-audit/persistence.ts` — typed read/write of
|
||
both DB tables.
|
||
|
||
---
|
||
|
||
## Data model
|
||
|
||
### `itglue_asset_audits` (one row per audit run)
|
||
|
||
| Column | Notes |
|
||
|---|---|
|
||
| `id` UUID | PK |
|
||
| `asset_type` TEXT | Currently `'flexible_asset'` only |
|
||
| `asset_id` BIGINT | IT Glue resource id |
|
||
| `asset_type_id`, `organization_id` BIGINT | Denormalized for fast filters |
|
||
| `generated_by_user_id` TEXT | FK `user(id)`, nullable on user delete |
|
||
| `generated_at` TIMESTAMPTZ | |
|
||
| `provider`, `model_used` | Which LLM produced the analysis |
|
||
| `asset_snapshot` JSONB | Redacted traits at audit time |
|
||
| `ticket_count` INT | How many fingerprints fed in |
|
||
| `field_gaps`, `notes_promotions`, `contradictions` JSONB | LLM output |
|
||
| `overall_score` NUMERIC(3,2) | |
|
||
| `estimated_cost_usd`, `total_input_tokens`, `total_output_tokens` | Cost telemetry |
|
||
| `status` | `pending` / `running` / `complete` / `failed` |
|
||
| `error_message` TEXT | |
|
||
|
||
### `itglue_writes` (one row per write attempt)
|
||
|
||
| Column | Notes |
|
||
|---|---|
|
||
| `id` UUID | PK |
|
||
| `audit_id` UUID | FK to the audit that prompted the change (nullable) |
|
||
| `asset_type`, `asset_id` | What was written |
|
||
| `field_name` TEXT | Human field name (mapped to trait key on apply) |
|
||
| `before_value`, `after_value` JSONB | Pre/post diff |
|
||
| `performed_by_user_id` TEXT | FK `user(id)` |
|
||
| `performed_at` TIMESTAMPTZ | |
|
||
| `status` | `pending` → `committed` / `failed` / `reverted` |
|
||
| `itglue_response` JSONB | Raw API response for forensics |
|
||
| `error_message` TEXT | |
|
||
| `source_evidence` JSONB | `{ ticket_numbers, gap_description }` or `{ reverts_write_id }` |
|
||
|
||
### Generic `audit_log` (existing — also written for every change)
|
||
|
||
`audit.log()` is called on every successful Apply/Revert with action
|
||
`itglue.write` or `itglue.revert`, resource `flexible_asset`, resourceId =
|
||
asset_id, and details = `{ field_name, before, after, audit_id }`. Surfaces
|
||
in `/admin/audit-log` next to every other admin action.
|
||
|
||
---
|
||
|
||
## API contract
|
||
|
||
All routes under `/api/analyzer/itglue/`.
|
||
|
||
### `GET /applications`
|
||
List all Application records joined to their latest audit.
|
||
Returns: `{ applications: [{ id, name, organizationId, organizationName, traitCount, latestAudit }] }`.
|
||
|
||
### `GET /applications/[id]`
|
||
Asset detail + field schema for rendering.
|
||
Returns: `{ asset, fields }`.
|
||
|
||
### `GET /applications/[id]/audit?history=1`
|
||
Latest audit (and optionally history of last 20).
|
||
Returns: `{ audit: AssetAuditRow | null, history?: AssetAuditRow[] }`.
|
||
|
||
### `POST /applications/[id]/audit`
|
||
Body: `{ provider?: 'anthropic' | 'openrouter' }`. Runs a fresh audit.
|
||
Cost-guard records `action='itglue_audit'` in `analyzer_cost_audit`.
|
||
Returns: `{ audit: AssetAuditRow }`.
|
||
|
||
### `POST /applications/[id]/apply` *(admin)*
|
||
Body: `{ auditId, fieldName, suggestedValue, sourceEvidence? }`.
|
||
Inserts pending `itglue_writes` row → calls `updateFlexibleAsset` on the IT
|
||
Glue client → marks committed/failed → refreshes the local mirror →
|
||
writes generic `audit_log`.
|
||
Returns: `{ writeId, status: 'committed', asset }` or 502 on IT Glue error.
|
||
|
||
### `POST /applications/[id]/revert/[writeId]` *(admin)*
|
||
Re-applies the original `before_value`. Inserts a new write row with
|
||
swapped before/after; original row → `status='reverted'`.
|
||
Returns: `{ writeId, revertedWriteId, status: 'committed', asset }`.
|
||
|
||
### `GET /applications/[id]/writes`
|
||
Per-asset write history.
|
||
Returns: `{ writes: AssetWriteRow[] }`.
|
||
|
||
### `GET /writes` *(admin)*
|
||
Cross-asset write log with status filter.
|
||
Query: `?status=committed&limit=100&offset=0`.
|
||
Returns: `{ writes: AssetWriteRow[] }`.
|
||
|
||
---
|
||
|
||
## Safeguards
|
||
|
||
### Three-layer credential refusal
|
||
|
||
1. **Prompt** — system prompt instructs the LLM not to suggest password,
|
||
secret, key, token, or credential fields.
|
||
2. **Redaction** — `redact()` from `lib/services/analyzer/itglue-redact.ts`
|
||
strips matching keys from the asset snapshot, peer traits, and all
|
||
payloads before they reach the LLM.
|
||
3. **Endpoint** — `POST /apply` regex-blocks any `field_name` matching
|
||
`/(password|secret|key|token|credential)/i` and returns 400 even if a
|
||
compromised payload made it through the first two layers.
|
||
|
||
### Three-layer audit trail
|
||
|
||
| Layer | Captures | Retention |
|
||
|---|---|---|
|
||
| `itglue_asset_audits` | Every audit run with full LLM context (asset snapshot, model used, cost, ticket count) | Forever |
|
||
| `itglue_writes` | Every PATCH attempt with before/after diff, status (pending → committed \| failed \| reverted), audit provenance, raw IT Glue API response, source_evidence | Forever |
|
||
| `audit_log` (generic) | One row per Apply/Revert in the format admins are used to | Per existing policy |
|
||
|
||
### Revert chain semantics
|
||
|
||
A revert produces a **new** `itglue_writes` row whose `before_value` and
|
||
`after_value` are swapped from the original. The new row carries
|
||
`source_evidence = { reverts_write_id }`. The original row's status flips
|
||
to `'reverted'`. To trace any state, walk the `audit_id` and `reverts_write_id`
|
||
graph — it's always complete.
|
||
|
||
### Per-record sync after every write
|
||
|
||
`refreshFlexibleAssetById()` runs after a successful PATCH (or a
|
||
successful revert) so `itg_flexible_assets.traits` reflects the change
|
||
immediately. The route also returns the freshly-PATCH'd asset in the
|
||
response so the UI can update without waiting for the mirror.
|
||
|
||
---
|
||
|
||
## Cost & latency
|
||
|
||
| Provider | Per-audit cost (typical) | Per-audit latency |
|
||
|---|---|---|
|
||
| Anthropic (Sonnet) | ~$0.10 | 30–60s |
|
||
| OpenRouter (DeepSeek V4 Pro) | ~$0.01 | 60–180s |
|
||
|
||
Apply is one IT Glue PATCH + one local upsert + one audit_log insert —
|
||
typically < 1s.
|
||
|
||
A full audit + 5 applies on a typical Application record: ~$0.10 (Claude)
|
||
or ~$0.01 (DeepSeek), 3–5 minutes including click-through.
|
||
|
||
---
|
||
|
||
## Limitations & non-goals
|
||
|
||
- **Applications only in v1.** The schema generalizes (`asset_type` is a
|
||
column), but Configurations / Procedures / Domains / Passwords each have
|
||
different prompt nuances. Adding more asset types is straightforward —
|
||
duplicate the data-builder and prompt files, add a new asset detail
|
||
page.
|
||
- **No bulk-apply.** Admin clicks each suggestion individually. If a
|
||
single audit produces 10 gaps that's 10 clicks. Bulk-apply is an easy
|
||
fast-follow once we trust quality.
|
||
- **No two-step approval workflow.** Admin-direct write per the design
|
||
decision; the audit log is the safety net. The existing `approval_requests`
|
||
table from the pipeline engine is available if we later want to require
|
||
tech-proposes / admin-approves.
|
||
- **No auto-create of new asset records.** Apply only updates existing
|
||
assets. If an audit reveals a totally missing record, the suggestion is
|
||
shown but Apply is disabled with an explainer; admins create the stub
|
||
manually in IT Glue.
|
||
- **No inline editor for suggested values.** Admin sees the LLM's
|
||
suggestion verbatim; if they want to tweak, they edit the value in IT
|
||
Glue afterward. v2 could add an inline editor.
|
||
- **No password / secret / key / token / credential writes through this
|
||
surface — ever.** Refused at three layers (above).
|
||
|
||
---
|
||
|
||
## Inspecting state (operational SQL)
|
||
|
||
### Audits, lowest score first
|
||
|
||
```sql
|
||
SELECT a.asset_id,
|
||
fa.name AS application_name, fa.organization_name,
|
||
a.overall_score,
|
||
jsonb_array_length(a.field_gaps) AS gap_count,
|
||
jsonb_array_length(a.notes_promotions) AS promo_count,
|
||
a.provider, a.estimated_cost_usd,
|
||
a.generated_at
|
||
FROM itglue_asset_audits a
|
||
JOIN itg_flexible_assets fa ON fa.id = a.asset_id::bigint
|
||
WHERE a.status = 'complete'
|
||
ORDER BY a.generated_at DESC, a.overall_score ASC;
|
||
```
|
||
|
||
### Writes in the last 7 days
|
||
|
||
```sql
|
||
SELECT performed_at, field_name, status,
|
||
before_value, after_value,
|
||
performed_by_user_id, audit_id
|
||
FROM itglue_writes
|
||
WHERE performed_at >= NOW() - INTERVAL '7 days'
|
||
ORDER BY performed_at DESC;
|
||
```
|
||
|
||
### Find what's still waiting on which write
|
||
|
||
```sql
|
||
SELECT id, field_name, before_value, after_value,
|
||
source_evidence ->> 'reverts_write_id' AS reverts_id,
|
||
status, performed_at
|
||
FROM itglue_writes
|
||
WHERE asset_id = '17096940'
|
||
ORDER BY performed_at;
|
||
```
|
||
|
||
### Audit cost-guard decisions
|
||
|
||
```sql
|
||
SELECT created_at, user_id, action, estimated_cost,
|
||
decision, decision_reason
|
||
FROM analyzer_cost_audit
|
||
WHERE action = 'itglue_audit'
|
||
ORDER BY created_at DESC
|
||
LIMIT 50;
|
||
```
|
||
|
||
---
|
||
|
||
## How a typical session looks
|
||
|
||
1. Tech opens `/analyzer/itglue/applications`. MISYS 6.3 is at the top with
|
||
no audit yet.
|
||
2. Tech clicks the row → lands on the detail page. They see 12/17 fields
|
||
are empty.
|
||
3. Tech clicks **Run audit** (DeepSeek selected for cost). 60–180s later,
|
||
the audit panel populates: 4 high-confidence field gaps, 1 notes
|
||
promotion, 1 contradiction, score 0.55.
|
||
4. Tech (admin) clicks **Apply** on `Wulf Application Champion` →
|
||
suggested value "Jake Hammel". A PATCH lands on IT Glue, the local
|
||
mirror refreshes, the field shows the new value, a write row appears
|
||
in the History section.
|
||
5. Tech clicks **Apply** on the Vendor Maintenance/Support promotion
|
||
("Steve Cianflone" extracted from Notes).
|
||
6. The next time someone opens this asset in IT Glue, the documentation
|
||
reflects what tickets have been telling us all along.
|
||
|
||
---
|
||
|
||
## Files (quick reference)
|
||
|
||
**Schema**: `migrations/075_itglue_audit.sql`
|
||
|
||
**Pipeline**: `lib/services/analyzer/asset-audit/`
|
||
- `data-builder.ts`, `prompt.ts`, `runner.ts`, `persistence.ts`, `runner.test.ts`
|
||
|
||
**API**: `app/api/analyzer/itglue/applications/`
|
||
- `route.ts` (list)
|
||
- `[id]/route.ts` (detail)
|
||
- `[id]/audit/route.ts` (GET/POST audit)
|
||
- `[id]/apply/route.ts` (admin write)
|
||
- `[id]/revert/[writeId]/route.ts` (admin revert)
|
||
- `[id]/writes/route.ts` (per-asset history)
|
||
|
||
Plus `app/api/analyzer/itglue/writes/route.ts` (admin cross-asset feed).
|
||
|
||
**UI**:
|
||
- `app/analyzer/itglue/applications/page.tsx` (list)
|
||
- `app/analyzer/itglue/applications/[id]/page.tsx` (detail)
|
||
- `app/admin/itglue-writes/page.tsx` (admin cross-asset)
|
||
|
||
**Modified**:
|
||
- `lib/services/itglue-client.ts` — `updateFlexibleAsset`, `refreshFlexibleAsset`,
|
||
`getRawSingle`, `isITGlueConfigured`, internal `patch`
|
||
- `lib/services/itglue-sync-service.ts` — `refreshFlexibleAssetById`
|
||
- `lib/permissions.ts` — `itglue: ['read', 'write']`
|
||
- `lib/types/analyzer.ts` — `AssetAuditResponse` schema + request types
|
||
- `components/navigation/app-navigation.tsx` — nav entry
|
||
|
||
---
|
||
|
||
## Phase 4.1 additions
|
||
|
||
### Ticket-first capture flow
|
||
|
||
Trigger: opt-in. The user clicks **"Check IT Glue documentation"** on an
|
||
analysis detail page (`/analyzer/analysis/[id]`).
|
||
|
||
Pipeline:
|
||
1. `GET /api/analyzer/analyses/[id]/itglue-suggestions` — runs
|
||
`matchAssetsForAnalysis(id)` and returns matched flexible_assets +
|
||
configurations (top 5 each by score) along with any existing
|
||
ticket-scoped audits.
|
||
2. User clicks **"Audit for this ticket"** on a matched asset → `POST` to
|
||
the same endpoint with `{ assetType, assetId, provider }`.
|
||
3. The runner builds context with `ticketScopeAnalysisId` set, so ticket
|
||
evidence is exactly the one analysis the user clicked from. The system
|
||
prompt picks up a ticket-scoped suffix instructing the LLM to frame
|
||
findings as "what *this ticket* taught us."
|
||
4. The audit row is persisted with `triggered_by_ticket_number` +
|
||
`triggered_by_analysis_id` populated.
|
||
5. Apply works from the same per-asset routes as the asset-first flow.
|
||
When the audit is ticket-scoped, the resulting `itglue_writes` row also
|
||
carries `triggered_by_ticket_number`, and the apply path inserts an
|
||
xref row with `relationship='updated'`.
|
||
|
||
Asset matching is loose: substring + word-boundary match between the
|
||
ticket's `aggregate_fingerprint.{applications_involved, device_classes,
|
||
vendors_involved}` and `itg_flexible_assets.name` (Application type only)
|
||
+ `itg_configurations.{name, hostname}` for the same client. Score 3
|
||
(exact) > 2 (word-boundary) > 1 (substring). Top 5 per kind.
|
||
|
||
### Configuration support
|
||
|
||
Same audit/apply/revert pattern as Applications, with these differences:
|
||
|
||
- **Flat schema** — Configurations have ~17 editable top-level columns
|
||
rather than a `traits` JSONB blob. The data-builder synthesizes a
|
||
trait-style map for prompt consistency, then the apply route maps
|
||
field name → IT Glue dash-case attribute (`primary_ip` →
|
||
`'primary-ip'`).
|
||
- **Hand-curated field hints** — IT Glue Configurations don't expose a
|
||
`_fields` table; the 16 hand-written hints live in
|
||
`lib/services/analyzer/asset-audit/data-builder.ts`
|
||
(`CONFIGURATION_FIELDS` constant). Each hint is the per-field
|
||
documentation the LLM sees.
|
||
- **Configuration-flavored prompt** — focuses on hostname/FQDN
|
||
consistency, OS version currency, named services capture (in
|
||
`operating_system_notes`), IP/MAC hygiene, contact ownership.
|
||
- **Column allowlist on apply** — even though the audit's output schema
|
||
doesn't restrict `field_name`, the configuration apply route refuses
|
||
anything outside `name | hostname | primary_ip | mac_address |
|
||
serial_number | asset_tag | position | notes |
|
||
operating_system_notes`. FK-shaped fields (manufacturer_id, model_id,
|
||
operating_system_id, contact_id, location_id) are read-only via this
|
||
surface for v1.
|
||
- **Per-record sync helper** — `refreshConfigurationById(id)` mirrors
|
||
`refreshFlexibleAssetById(id)`; called after every successful Apply.
|
||
|
||
### Cross-reference index — `itglue_ticket_xrefs`
|
||
|
||
```
|
||
ticket_number | analysis_id | asset_type | asset_id | relationship | source | confidence | details | created_at
|
||
```
|
||
|
||
Three relationship types:
|
||
|
||
| Relationship | Meaning | Source |
|
||
|---|---|---|
|
||
| `referenced` | The analyzer cited this asset/doc when analyzing the ticket. Comes from `analyzer_analyses.itglue_docs_referenced`. | `analyzer_referenced` |
|
||
| `updated` | A ticket-driven audit produced a write on this asset. | `audit_write` |
|
||
| `should_have_referenced` | Reserved — gap text suggests we should have found this asset/doc but didn't. Not auto-populated yet. | `manual` (future) |
|
||
|
||
Populated by:
|
||
- **Post-analysis hook** in `lib/services/analyzer/worker.ts` —
|
||
`insertReferencedXrefsFromAnalysis` runs after every successful
|
||
analysis insertion. Maps each `ITGlueDocReference.doc_type` →
|
||
asset_type. Best-effort.
|
||
- **Post-apply hook** in the apply routes — `insertUpdatedXref` runs
|
||
after a successful PATCH if the audit was ticket-scoped. Best-effort.
|
||
- **No backfill** — table fills forward. The unique index
|
||
`(ticket_number, analysis_id, asset_type, asset_id, relationship,
|
||
source)` makes ingestion idempotent.
|
||
|
||
Two views consume it:
|
||
|
||
1. **Asset detail page** — "Tickets that touched this asset" section
|
||
under the audit panel, with sub-sections for `Referenced by` (with
|
||
the LLM's relevance reason) and `Updated by` (with the field name and
|
||
write history link).
|
||
2. **`GET /api/analyzer/tickets/[ticketNumber]/itglue-xrefs`** —
|
||
ticket-side view; available for future ticket-page surfacing.
|
||
|
||
This table is also the lookup index a future RAG automation will use:
|
||
given a new ticket's entities, fetch historically-referenced docs as
|
||
warm candidates for the analyzer's IT Glue retrieval stage.
|
||
|
||
### Ticket-linkage columns
|
||
|
||
Both audit and write tables carry denormalized ticket linkage:
|
||
|
||
- `itglue_asset_audits.triggered_by_ticket_number` (and `triggered_by_analysis_id`)
|
||
— set on ticket-scoped audits. Null on asset-first audits.
|
||
- `itglue_writes.triggered_by_ticket_number` — copied forward from the
|
||
audit on Apply, so "every write a given ticket drove" is a one-query
|
||
lookup. Null when the audit was asset-first.
|
||
|
||
### Files added in 4.1
|
||
|
||
- `migrations/076_itglue_ticket_xrefs.sql`
|
||
- `lib/services/analyzer/asset-audit/xrefs.ts`
|
||
- `lib/services/analyzer/asset-audit/asset-matcher.ts`
|
||
- `app/api/analyzer/analyses/[id]/itglue-suggestions/route.ts`
|
||
- Configuration parallel route tree under `app/api/analyzer/itglue/configurations/`
|
||
- xref endpoints: `app/api/analyzer/itglue/applications/[id]/xrefs/route.ts`,
|
||
`app/api/analyzer/itglue/configurations/[id]/xrefs/route.ts`,
|
||
`app/api/analyzer/tickets/[ticketNumber]/itglue-xrefs/route.ts`
|
||
- `app/analyzer/itglue/configurations/page.tsx` (list)
|
||
- `app/analyzer/itglue/configurations/[id]/page.tsx` (detail)
|
||
- `components/analyzer/itglue-suggestions-panel.tsx`
|
||
|
||
### Files modified in 4.1
|
||
|
||
- `lib/services/itglue-client.ts` — `updateConfiguration`, `refreshConfiguration`
|
||
- `lib/services/itglue-sync-service.ts` — `refreshConfigurationById`
|
||
- `lib/services/analyzer/asset-audit/data-builder.ts` — assetType dispatch + ticket-scope mode + Configuration field schema
|
||
- `lib/services/analyzer/asset-audit/prompt.ts` — Configuration prompt + ticket-scoped suffix + assetType-aware payload
|
||
- `lib/services/analyzer/asset-audit/runner.ts` — accepts `assetType` and `ticketScopeAnalysisId`; persists triggered_by_*
|
||
- `lib/services/analyzer/asset-audit/persistence.ts` — asset_type union extended; `getLatestTicketScopedAudit`; `createPendingWrite` accepts asset_type + ticket linkage
|
||
- `lib/services/analyzer/worker.ts` — post-analysis xref ingestion hook
|
||
- `app/api/analyzer/itglue/applications/[id]/apply/route.ts` — ticket linkage + xref insert on Apply
|
||
- `app/api/analyzer/itglue/applications/[id]/revert/[writeId]/route.ts` — asset_type now passed to createPendingWrite
|
||
- `app/analyzer/analysis/[id]/page.tsx` — `<ItglueSuggestionsPanel/>` rendered
|
||
- `app/analyzer/itglue/applications/[id]/page.tsx` — "Tickets that touched this asset" section
|
||
- `components/navigation/app-navigation.tsx` — split entries
|