Called by the inline Script Builder tab on Submit. Writes
ai_drafted_script + ai_drafted_parameters to the fix without stamping
applied_at (a draft is not an application — that's §5 of the Phase 9
spec). Bumps state_version so Resolve/Escalate preview bundles
regenerate.
409 on terminal fix status. 404 on wrong session. 422 on empty script.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /script-builder/sessions now supports origin='pilot_inline':
- Requires ai_session_id; validates it against current user ownership.
- Get-or-create: returns existing row for (user, ai_session_id) pair.
- Partial unique index on the DB backs the invariant; races resolve to
the single winner row.
list_sessions + count_user_sessions default-scope to origin='standalone'
so inline scratch sessions don't pollute the /script-builder dashboard
or count against the 5-session cap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #3 from phase-8-review-issues.md. 'Not yet' on the AI-confirming
banner was a local-state hide; the proposal re-surfaced on the next
refreshSessionDerived call.
Two-part fix:
- PATCH /outcome now clears ai_outcome_proposal on any terminal action
(engineer has taken a decision; stale AI proposal is moot).
- New DELETE /ai-sessions/:sid/suggested-fixes/:fid/ai-outcome-proposal
endpoint for explicit 'Not yet' rejection. Does not touch status
or state_version — pure UI state.
Frontend handleRejectAIProposal now calls the DELETE and setActiveFix
with the server response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #2 from phase-8-review-issues.md. Apply was client-side-only via
a bannerApplied flag. Refresh / chat reselect / multi-tab would drop
Verifying state back to Proposed.
- New POST /ai-sessions/{sid}/suggested-fixes/{fid}/apply stamps
applied_at without changing status (still 'proposed'). Idempotent
if already stamped; 409 if fix is past proposed (a terminal outcome
was already recorded).
- Bumps state_version so resolve/escalate preview bundles reflect that
the fix has entered verifying.
- Frontend handleApplyFix calls the endpoint and uses the returned
applied_at directly. bannerApplied client flag is removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #1 from phase-8-review-issues.md. Cache invalidation alone isn't
enough — previews were also omitting outcome fields from the LLM bundle,
so a fresh regenerate still couldn't distinguish proposed / failed /
partial / success.
- PATCH /outcome now bumps ai_sessions.state_version (matches
record_decision's existing pattern).
- Resolution-note + escalation-package bundles now include status,
applied_at, verified_at, partial_notes, failure_reason on the active fix.
- Generator prompts prescribe outcome-aware phrasing (closure language
for success; what-we've-tried + next-steps for failed/partial).
- New end-to-end test asserts the regenerated preview reflects the
recorded outcome, not just that the cache key changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tells the AI when + how to emit the [FIX_OUTCOME] marker that Task 4's
parser consumes. Placeholder-only per the anti-parrot pattern — no
literal UUIDs, outcomes, or reasons that could leak into unrelated
sessions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The AI emits [FIX_OUTCOME] when the engineer indicates in chat that a
prior suggested fix worked, didn't work, or was partially applied. The
marker writes to session_suggested_fixes.ai_outcome_proposal (JSONB),
which the frontend surfaces as a "confirm outcome?" banner. The status
column is only updated when the engineer clicks confirm (via PATCH
/outcome endpoint from Task 3).
Placeholder-only system prompt wiring comes in Task 5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records engineer-reported outcome (applied_success|applied_failed|
applied_partial|dismissed). Enforces transition rules (partial → success/
failed allowed; terminal outcomes return 409) and notes requirements
(applied_partial requires notes).
Sets verified_at on success/failure, stamps applied_at if not already
set (handles the case where the AI [FIX_OUTCOME] marker fires before
the engineer clicks Apply).
Also fixes pre-existing test-infrastructure bug: network_diagram.py used
bare string server_default="'[]'" for JSONB columns, which asyncpg
rejects during test schema creation. Changed to text("'[]'::jsonb") to
match the pattern used by script_template.py.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the loop on the Phase 5 "Run now, templatize after resolve" path.
After a session resolves, drafts queued by the three-option dialog surface
as a modal that lets the engineer review the AI-proposed parameterization
and either save as a reusable team template or skip. A "don't ask again"
toggle writes to account_settings.preferences so the next resolve won't
pop the modal.
Backend:
- /api/v1/draft-templates:
* GET — list account drafts (pending_only default true; pass false for
audit view including accepted/rejected)
* GET /{id} — single draft
* POST /{id}/accept — promotes to a new script_templates row with
source_session_id / source_user_id / source_ticket_ref populated
(drives the Script Library "generated from CW #X · resolved by Y"
provenance chip). Draft flips to status=accepted,
promoted_template_id set, resolved_at stamped. 409 on re-accept /
already-rejected. 400 on unknown category_id.
* POST /{id}/reject — flips to status=rejected. 409 on re-reject.
- /api/v1/accounts/me/preferences (GET/PATCH) — thin wrapper over
AccountSettings.get_setting/set_setting. PATCH merges keys into the
JSONB column, preserving existing keys the client didn't touch.
Used by the "Don't ask again for this team" checkbox
(templatize_prompt_enabled=false) and, forward-looking, by
cw_resolved_status_id / cw_escalated_status_id from Phase 4.
- 13 tests: list filter, accept with/without edited_body, provenance
copy-through, reject, 409 on re-accept / re-reject, 400 on unknown
category, prefs round-trip with merge semantics.
Frontend:
- src/components/pilot/script/TemplatizePrompt.tsx — modal showing the
drafted script with proposed parameters in the Phase 5
ParameterizationPreview, editable name/category/description, an
individual-parameter remove button, and the "don't ask again" opt-out.
Accept posts to /draft-templates/{id}/accept + optionally PATCHes
preferences. Skip posts /reject.
- src/api/draftTemplates.ts — typed client plus accountPreferencesApi.
- AssistantChatPage: after a successful Resolve (external OR local),
fetches preferences + pending drafts for the session and queues the
modal one draft at a time. Escalate does not trigger this flow.
- Sidebar: Scripts nav shows the pending-draft count as a badge. Fetched
independently of the main sidebar stats so endpoint flakes don't
break the rest of the sidebar.
Verified live 2026-04-22: seed two drafts → GET sees both pending →
accept draft A (template created, provenance CW #99123 populated) →
reject draft B → pending count drops → PATCH opt-out → GET confirms
persistence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "AI parrots example content from system prompt" bug bit us twice in
one day across two different prompt sites. Patching individual prompts
is treating the symptom; this commit makes the rule structural.
Audit + sanitize:
- assistant_chat_service.ASSISTANT_SYSTEM_PROMPT — already cleaned in
prior commits, but the [FORK] schema still had literal "Brief reason"
/ "Short name" / "One sentence" placeholders. Replaced with
<angle-bracket> placeholders. Anti-parrot rule itself rewritten to
describe the failure mode abstractly instead of naming "jsmith" so
the rule no longer trips the guardrail (and so the model doesn't
see "jsmith" as a token at all).
- ai_chat_service.py — removed three concrete-example offenders:
"Get-Service ADSync" command literal, the "DC01 server_name" intake
form payload (in two places), and the inline interview demos using
"Azure AD Sync failures" / "Exchange Online mailbox migration".
Replaced with technology-neutral schema descriptions.
- ai_tree_generator_service.BRANCH_DETAIL_SYSTEM_PROMPT — replaced the
fully-fleshed DNS troubleshooting tree (with literal Dnscache /
ipconfig / google.com / Start-Service) with a placeholder schema
showing only ID-linkage shape.
- kb_conversion_service.PROCEDURAL_SYSTEM_PROMPT — replaced the worked
Server Manager + DC01 example payload with a placeholder schema.
Guardrail (tests/test_prompt_anti_parrot.py):
- Imports every module under app/services/ and app/core/ and walks
every uppercase string constant ending in _PROMPT, _SCHEMA,
_PROTOCOL, _FORMAT, or _CONTEXT.
- test 1: known-leaked-token list (jsmith, DC01, ADSync, Dnscache,
google.com, "Outlook keeps", "Teams drops") must not appear in any
prompt constant. Add to the list when a new leak shows up in prod —
the list IS the audit trail.
- test 2: marker blocks ([QUESTIONS], [ACTIONS], [SUGGEST_FIX], etc.)
must contain placeholders only. Distinguishes JSON keys (followed
by ':', allowed) from JSON values (followed by ',' / ']' / '}',
must be <placeholder>); allows pipe-separated enum types
(text|password|select) and a small set of fixed enum values
(question, diagnostic_check, decision, action, ...). Verified by
feeding the test a known-bad block — caught it correctly.
Documented the rule in CLAUDE.md → AI / FlowPilot lessons, naming
the test as the enforcement point so future contributors know how to
extend it (add to the known-leaked list when a new leak surfaces).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the SuggestedFix card to an inline panel that handles both cases:
template-matched fixes open the Script Library generator with parameters
pre-filled from session context; un-matched fixes open the three-option
dialog (one_off / draft_template / build_template). The decision endpoint
records the path choice with side effects: draft_template persists a
draft_templates row via a Sonnet-driven TemplateExtractionService;
build_template returns a redirect to the Script Builder; one_off just
records the choice.
Backend:
- TemplateExtractionService: drafts a parameter schema from a concrete
rendered script. Conservative by default ("prefer fewer parameters").
Round-trip-validates that templated_body only references declared
parameters; missing-key mismatch falls back to the original script
with no params. LLM/parse failures fall back identically — the
engineer can still create a draft and refine in the post-resolve
prompt (Phase 6).
- /suggested-fixes/{fix_id}/decision side effects:
* one_off → returns rendered_script (engineer's edited version or the
fix's ai_drafted_script verbatim)
* draft_template → same + creates draft_templates row with extracted
params, returns draft_template_id
* build_template → returns redirect_path=/scripts/builder?from_session=
&fix= so the frontend can navigate to the builder pre-loaded
- 400 when a non-template fix has no ai_drafted_script (template-matched
fixes take the dedicated /scripts/generate path, not this endpoint).
- 12 tests: TemplateExtractionService parse + fallback paths, all four
decision branches, edited_script override, missing-script 400.
Frontend:
- src/components/pilot/script/{TemplateMatchPanel, NoTemplateDialog,
ParameterizationPreview}.tsx — inline panels rendered in the task
lane's bottom slot when the engineer clicks a SuggestedFix card.
- TemplateMatchPanel: loads template via /scripts/templates/{id},
pre-fills params from fix.ai_drafted_parameters with cyan "from
session" tags, generates via existing /scripts/generate (already
bumps state_version on ai_session_id from Phase 3). 404 falls back
with a clear message instead of erroring.
- NoTemplateDialog: shows the AI-drafted script with proposed parameter
values highlighted in amber via ParameterizationPreview; three option
cards with the middle (draft_template) flagged Recommended; inline
edit on the script body before deciding.
- SuggestedFix card now clickable: onActivate toggles the inline panel.
- AssistantChatPage: scriptPanelOpen state + handleScriptDecision that
navigates on build_template and toasts on the other paths. Active fix
changes auto-close the panel so engineers don't act on stale state.
- Cmd+K → "Open inline Script Generator" palette entry surfaces only on
/pilot/:id routes; fires a window event the chat page subscribes to.
No Resolve shortcut added per Section 14 decision (browser ⌘R conflict).
Verified 2026-04-22 against the dev stack:
- one_off / draft_template / build_template all return the right shape
with real Sonnet TemplateExtractionService for the draft path.
- Conservative extraction confirmed: cmdkey + Restart-Process script
yielded zero proposed parameters as intended.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the preview popover's Confirm & post action to ConnectWise (and,
via the provider pattern, any future PSA). Adds the parallel Escalate
flow with the handoff-oriented five-section markdown. Sessions without a
linked PSA ticket resolve/escalate locally — markdown stored, status
flipped, nothing posted externally.
Backend:
- EscalationPackageGeneratorService: Sonnet, five sections (Problem /
What we've confirmed / What we've tried / Current hypothesis /
Suggested next steps). Shares the preview_cache with a separate KIND
so Resolve and Escalate previews for the same state coexist.
- PSAWritebackService: post_resolution_note (RESOLUTION note type,
customer-visible), post_escalation_package (INTERNAL_ANALYSIS,
handoff for the next engineer only), transition_ticket_status with
mandatory re-fetch verification. PSAStatusVerificationError surfaces
loudly when CW silently rejects a status change — the
ConnectWise anti-pattern CLAUDE.md flags.
- Endpoints:
* POST /ai-sessions/{id}/escalation-package/preview
* POST /ai-sessions/{id}/resolution-note/post
* POST /ai-sessions/{id}/escalation-package/post
Outcomes: "resolved" / "escalated" with external_id + verified status,
"resolved_local" / "escalated_local" when no PSA linked.
- Target CW status IDs live in account_settings.preferences
(cw_resolved_status_id, cw_escalated_status_id). When unset, the post
proceeds without a status transition — response includes a
status_transition_skipped_reason rather than silently erroring.
- 7 tests: local-only path, PSA happy path with verified transition,
status verification failure → 502, skipped transition when
unconfigured, 409 on already-resolved re-post, escalate parallel path,
internal-analysis note type enforced.
Frontend:
- ResolutionNotePreview now kind-parameterized ('resolve' | 'escalate')
with inline edit + Confirm & post. Preview loads from the matching
backend endpoint; posting calls the matching endpoint; outcome toast
surfaces the verified CW status or the local-only result.
- AssistantChatPage: previewKind state replaces previewOpen; two toggle
buttons (Preview Resolve note / Escalate instead) in the lane's bottom
slot. handleConfirmPost dispatches by kind.
Verified 2026-04-22:
- Local-only Resolve + Escalate round-trip against the dev stack.
- Live Sonnet escalation-package preview; cache hit on repeat call
with no state change (separate cache kind from resolution-note).
- PSA post + status-verification paths covered by mocked-provider pytest
cases. Live CW round-trip pending a test CW instance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the AI-proposed resolution path and the inline preview of the
markdown that will be posted to the customer ticket on Resolve. The
preview is keyed on (session_id, ai_sessions.state_version) so back-to-
back fetches against unchanged state hit an in-process cache instead
of paying for a Sonnet call.
Backend:
- preview_cache: in-process LRU keyed on (kind, session_id, state_version).
No TTL — state_version is the source of truth. Soft-cap 5000 entries.
- unified_chat_service: [SUGGEST_FIX] parser (last-block-wins, JSON
payload, confidence clamped 0-100), supersession persistence (sets
superseded_at on prior active row), atomic state_version bump.
- ResolutionNoteGeneratorService: pulls session, facts, active fix, and
redacted script_generations into a structured input bundle for Sonnet;
produces the four-section markdown (Problem / What we confirmed /
Root cause / Resolution). Sensitive script parameters redacted via
ScriptTemplateEngine.redact_sensitive driven by the template's
parameters_schema.
- /api/v1/ai-sessions/{id}/suggested-fixes/active — 200 with the active
fix or 404.
- /api/v1/ai-sessions/{id}/suggested-fixes/{fix_id}/decision — records
one_off / draft_template / build_template / dismissed; dismiss
supersedes; bumps state_version. 409 on dismissing an already-
superseded fix.
- /api/v1/ai-sessions/{id}/resolution-note/preview — generates or returns
cached markdown; from_cache flag in payload signals cache hit.
- scripts.py POST /generate now bumps state_version on the linked
ai_session_id when present (third source of preview-cache invalidation
per Section 5.5).
- ASSISTANT_SYSTEM_PROMPT documents [SUGGEST_FIX] (when to/not to emit,
format, supersession semantics).
- 12 tests covering the parser (well-formed, last-wins, malformed,
confidence clamping), supersession + state_version invariant, all
decision branches, preview cache hit-on-no-change + miss-after-write.
Frontend:
- src/components/pilot/sections/SuggestedFix.tsx — amber-accented card
with confidence badge; dismiss action wired to the decision endpoint.
- src/components/pilot/ResolutionNotePreview.tsx — popover with refresh,
loading state, cached/fresh indicator, ticket-ref display.
- src/api/sessionSuggestedFixes.ts — typed client; getActive normalizes
404 to null so callers don't have to special-case.
- TaskLane gains suggestedFixSlot + bottomSlot props (rendered after
Diagnostic Checks; bottomSlot anchors the Resolve action).
- AssistantChatPage: refreshSessionDerived helper batches fact + fix
refresh; fact mutations and chat sends both schedule a 500ms-debounced
preview refresh per the Section 5.5 spec.
Verified end-to-end against the dev stack with a real Sonnet call:
- /active 404 → fact create → preview generates four-section markdown
grounded only in provided facts → second preview call hits cache
(from_cache=true, no LLM call) → fact write 2 → cache miss, regenerates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the load-bearing structural feature of the FlowPilot migration: a
"What we know" panel that holds confirmed facts for a session, fed by AI
[PROMOTE] markers and engineer-added notes. Facts feed the resolution
note preview (Phase 3) and survive across turns via stable UUIDs assigned
to pending_task_lane items.
Backend:
- FactSynthesisService: create/update/soft-delete facts with atomic
state_version bumps; LLM-backed synthesize_from_question/check on the
fact_synthesis (Haiku) action tier per Section 6.6.
- /api/v1/ai-sessions/{id}/facts CRUD + /facts/promote (proposed_text or
via synthesis). PATCH returns 403 for question/diagnostic_check facts
(edit the source item instead, Section 7.3).
- unified_chat_service: [PROMOTE] marker parser (JSON-block per Section
8.1 spec drift note), stable-UUID assignment for pending_task_lane
questions/actions preserved by exact text/label match across turns.
- ASSISTANT_SYSTEM_PROMPT: documents [PROMOTE] format, when to/not to
emit, hallucination guardrails, source_ref handling.
- 17 tests covering parser, stable IDs, service validation, CRUD,
editability rule, both promote modes, 422 null-synthesis path,
state_version invariant.
Frontend:
- src/components/pilot/sections/{WhatWeKnow,WhatWeKnowItem,AddNoteButton}
— green-gradient section above Questions, dashed-circle check, inline
edit/delete gated by the server's editable flag.
- TaskLane gains a whatWeKnowSlot prop (existing assistant/ folder kept
per the doc's "rename is opportunistic" guidance).
- AssistantChatPage fetches facts on selectChat and refetches after each
chat send (so [PROMOTE]-synthesized facts appear immediately); auto-
opens the lane when facts exist.
Verification: end-to-end smoke against the local docker stack confirms
all five endpoints (list/create/patch/delete/promote) plus the 403
editability rule. pytest suite verifies the same with mocked LLM. Live
[PROMOTE] flow remains untested until used in the UI — the marker shape
is covered by parser tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add device_types table with system seed data
Creates DeviceType SQLAlchemy model and migration 073 that provisions the
device_types table with 28 system-seeded device types across 7 categories
(network, compute, storage, cloud, endpoint, infrastructure, security).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add network_diagrams table
Create NetworkDiagram SQLAlchemy model with JSONB nodes/edges, team-scoped with client/asset metadata, and Alembic migration 074.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add Pydantic schemas for device types and network diagrams
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add device types CRUD router
Adds GET/POST/PUT/DELETE endpoints at /device-types with team-scoped access. System types are read-only; custom types are scoped to the creating team.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add AI generation service for network diagrams
Adds network_diagram_ai_service.py with generate_diagram() function that
calls the AI provider to convert plain-English network descriptions into
structured DiagramNode/DiagramEdge data. Registers the action in
ACTION_MODEL_MAP as a standard-tier route.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add network diagrams CRUD + AI generate + export/import router
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add TypeScript types for network diagrams
Adds all interfaces for network diagrams and device types including
DiagramNode, DiagramEdge, DeviceProperties, NetworkDiagramResponse,
AI generate request/response, import/export shapes, and list item types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add frontend API clients for device types and network diagrams
Adds deviceTypesApi (list, create, update, remove) and networkDiagramsApi
(list, get, create, update, archive, duplicate, exportJson, importJson,
aiGenerate, listClients) following the existing apiClient module pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add device registry, DeviceNode, ConnectionEdge for React Flow
Creates the React Flow building blocks for the network diagram editor:
device type registry with icon/color mappings, DeviceNode component with
status indicators and connection handles, ConnectionEdge with per-type
styling, and nodeTypes/edgeTypes registration maps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add DeviceToolbar panel with search, categories, drag-drop, custom type creation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add PropertiesPanel for node and edge property editing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add AIAssistPanel with replace and merge modes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add NetworkCanvas wrapper and DiagramHeader components
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add DiagramEditor page assembling all panels with auto-save and AI generation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add Network Diagrams list page with search, client filter, import
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add Network Maps to sidebar navigation and router
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve TypeScript errors in DeviceToolbar and DiagramEditor
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve stale selection bug in network diagram PropertiesPanel
Selection state now stores IDs and derives objects from live arrays,
so edits in PropertiesPanel inputs reflect immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add React Flow UI foundation components for network diagrams
BaseNode (structured node shell with header/content/footer slots),
BaseHandle (styled connection handle), LabeledHandle (handle with
port label), NodeStatusIndicator (status border effect),
NodeTooltip (hover details via NodeToolbar).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add LabeledGroupNode and AnimatedSvgEdge components
GroupNode for subnet/VLAN/site grouping with positioned label badge.
AnimatedSvgEdge for traffic flow visualization with animated SVG
shape along edge path. Both registered in type maps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: DeviceNode uses BaseNode, BaseHandle, StatusIndicator, Tooltip
Replaces hand-rolled node layout with composable React Flow UI
components. Status is now a border effect instead of a dot.
Hover tooltip shows hostname, IP, vendor, role, notes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add grouping toolbar items and traffic flow toggle
DeviceToolbar gets Subnet/VLAN/Site/DMZ grouping section with
drag-drop. PropertiesPanel gets Show Traffic toggle that switches
edges between connection and animated types. DiagramEditor handles
both device and group node drops.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address code review findings for React Flow UI integration
- Use screenToFlowPosition() for drop coordinates (fixes zoom/pan bug)
- Remove duplicate selection border from DeviceNode (BaseNode handles it)
- Add w-full to GroupNode for proper container sizing
- Remove unused 'selected' destructuring from DeviceNode
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add ISP icon to network diagram device registry
Globe icon with accent color, under cloud category.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: improve drag-and-drop feel in network diagram editor
Grip icons on draggable toolbar items, press effect on drag start,
dashed border overlay with 'Drop to add' text when dragging over canvas.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add ContextMenu component for network diagram editor
Charcoal-styled context menu with action factories for node
and canvas variants. Viewport-clamped positioning, auto-dismiss
on click outside, escape, or scroll.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add useCanvasShortcuts hook for copy/paste/duplicate
Keyboard shortcuts with preventDefault and input guard.
Clipboard stores nodes with relative positions and edge indices.
Paste computes canvas center via screenToFlowPosition.
Duplicate offsets +30px. Supports both device and group nodes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: wire context menu and keyboard shortcuts into diagram editor
Right-click context menus for nodes (copy/duplicate/delete) and
canvas (paste/select-all/fit-view). Right-click selects the node
per spec. serializeNodes now handles group nodes correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: context menu dismisses on pane click, ISP in toolbar
Context menu now closes when clicking anywhere on the canvas via
onPaneClick prop. ISP device added as built-in toolbar item under
Internet section so it's always available without a database entry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: backend code review fixes for network diagrams
- Replace legacy Optional imports with modern str | None syntax
- Type JSONB columns as Mapped[list[dict[str, Any]]]
- Escape SQL LIKE wildcards (%, _) in diagram search
- Type DiagramNode.position as Position(x, y) Pydantic model
- Wrap AI response parsing in KeyError handler for clean 422 errors
- Remove unused Optional/TYPE_CHECKING imports from schemas/models
- Extract _get_available_slugs helper to DRY duplicate queries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: network diagram editor UX — straight edges, snap-to-grid, ISP in Cloud, group resize
- Straight edges: replace SmoothStepEdge with BaseEdge + getStraightPath so
connections draw direct diagonal lines instead of orthogonal bent paths
- Snap-to-grid: add snapToGrid/snapGrid=[20,20] to NetworkCanvas so nodes
align consistently when dragged
- ISP in Cloud: remove standalone "Internet" sidebar section, inject ISP into
the Cloud category loop with search support and correct item count
- Group node resize: add NodeResizer to GroupNode (subnet/VLAN/site/DMZ),
handles visible when selected; dimensions saved/restored correctly on
reload (also fixes group node load bug where type was always 'device')
- DiagramNode type: add nodeType and style optional fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: network diagram team_id guard + multi-style edge routing
Backend:
- Guard create_diagram with 422 if current_user.team_id is None (prevents
NOT NULL constraint crash for accounts not yet assigned to a team)
- Add routing field to DiagramEdge schema (straight/curved/step)
Frontend:
- ConnectionEdge now supports straight (default), curved (bezier), and
step (smooth-step) routing per-edge via routing field in edge data
- PropertiesPanel Connection section gets a Line Style toggle:
Straight | Curved | Step buttons, active state highlights in accent
- handleEdgeUpdate and serializeEdges now propagate the routing field
- DiagramEdge type gets optional routing field
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: network diagrams UX overhaul — icons, empty canvas, properties panel
- Colorize: semantic category colors for all device types (network=blue,
security=orange, compute=emerald, endpoint=amber, storage=violet,
cloud=cyan, infra=steel); better icons (Router, ShieldAlert, Boxes,
Package, Gauge, PlugZap, Video, Radio); MiniMap uses category colors
- Onboard: centered AI generate prompt on empty canvas with 5 MSP-specific
example chips, ⌘↵ shortcut, spinner; AIAssistPanel only shown with nodes
- Arrange: properties panel — status badge grid at top, fields grouped into
Network (IP/Subnet/VLAN) and Hardware (Hostname/Vendor/Model/Role) sections
- Delight: segmented topology color bar on listing cards; backend returns
category_counts via single extra query on list endpoint
- Harden: real PNG export via html-to-image + getNodesBounds/getViewportForBounds
- Polish: ChevronDown replaces unicode ▾, click-outside for client filter,
consistent spinner in empty prompt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: drop changelog noise from network extraction
* fix: align network map builder with account isolation
* feat: add manual create option for network maps
* feat: make manual network map creation easier to discover
* fix(network-maps): address design critique — harden, normalize, clarify, polish
- Archive: two-step inline confirm in card dropdown menu
- Delete Device/Edge: two-step inline confirm in PropertiesPanel footer
- Context menu Delete: floating confirm bar instead of immediate deletion
- AI Generate New: two-step confirm when replacing existing diagram nodes
- DiagramHeader: show 'Unsaved changes' in amber when isDirty and not saving
- deviceRegistry: SECURITY_COLOR #f97316 → #f87171 (deprecated ember orange removed)
- CanvasEmptyPrompt: remove backdrop-blur (design system violation)
- CanvasEmptyPrompt: remove redundant 'Skip AI' bottom button (duplicate of Build manually card)
- CanvasEmptyPrompt: rounded-xl/rounded-2xl → rounded-lg, border-2 → border
- Topology bar: h-1 → h-2 + native tooltip with category breakdown
- AIAssistPanel: replace pulse-dot loading with spinner (consistent with rest of feature)
- ContextMenu: add shadow-lg (consistent with other dropdowns)
- DeviceNode tooltip: Position.Bottom → Position.Top (avoids canvas-edge clipping)
- CanvasEmptyPrompt: raise ⌘↵ hint from /50 opacity to full text-muted-foreground
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(network-maps): bring to front / send to back layering for nodes
Three entry points for z-index control:
- Right-click context menu: Bring to Front / Send to Back with ] / [ shortcuts, separated by dividers from copy/delete groups
- Properties panel: Layer row with Bring Front + Send Back buttons, tooltip shows keyboard shortcut
- Keyboard: ] brings selected node(s) to front, [ sends to back (skips when input focused)
Context menu also gains divider support (dividerBefore flag) for visual grouping.
Layering handlers use max/min zIndex across all nodes so repeated presses always stack correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: swap switch icon from Layers → Network (Lucide)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: icon size picker (S/M/L) on device nodes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: drag-to-resize device nodes + BrickWallFire for firewall
- NodeResizer on DeviceNode (same pattern as group nodes); icon scales
proportionally with node width, clamped 16–60px
- Removes S/M/L static picker — resize is now direct manipulation
- firewall: ShieldAlert → BrickWallFire
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: trigger Railway rebuild
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing hero_001.jpg to git (was untracked, broke Railway deploy)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: ShieldAlert still referenced in CATEGORY_DEFAULTS after icon swap
Removed ShieldAlert from imports when swapping firewall icon to BrickWallFire
but left it in CATEGORY_DEFAULTS — runtime crash, device toolbar empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(network): proportional node resize with locked aspect ratio
Nodes grew into rectangles because NodeResizer had no aspect ratio
constraint, minWidth != minHeight, and icon/text only scaled from width.
- DeviceNode: add keepAspectRatio + equal minWidth/minHeight (80×80),
maxWidth/maxHeight (280×280), scale icon and label/IP font sizes from
Math.min(width, height) so all content grows uniformly
- DiagramEditor: set explicit 120×120 style on dropped device nodes so
React Flow has a definite starting size for aspect ratio calculation
- DiagramEditor: persist device node style (width/height) in
serializeNodes and restore it on load so size survives save/reload
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): suppress ESLint errors in network diagram components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- test_rls_isolation: add pytestmark for module-scoped event loop to fix
"Future attached to a different loop" with pytest-asyncio 0.23 + asyncpg
module-scoped fixtures
- test_admin_categories_global: global categories use PLATFORM_ACCOUNT_ID
not NULL; update stale assertion
- test_permissions_account: with RLS, cross-tenant tree access returns 404
(invisible) not 403 (forbidden) — update to match actual behavior
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both tables have no account_id column — they are globally readable
by all authenticated users and must not have RLS policies.
Also removes the corresponding test cases that assumed these tables
had account_id-based policies.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- trees.py: change account_id=current_user.account_id →
account_id=tree.account_id so super-admin cross-account shares land in
the tree's tenant where RLS will see them.
- migration a05e1a1bea7c: fix backfill to join tree_shares → trees instead
of tree_shares → users(created_by). Same logic: historical shares belong
to the tree's tenant.
- test_tree_sharing.py: add test_share_account_id_matches_tree_not_actor
to assert share.account_id == tree.account_id after POST /share; also
add missing account_id to all direct TreeShare(...) constructors in
existing tests.
- test_phase1_migrations.py: remove team_id= from TargetList constructor
(column dropped in Phase 3).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P3-A: Add account_id to audit_logs model + migration (backfill via user_id →
users.account_id). log_audit() gains optional account_id param with fallback
SELECT to avoid churn across 40 call sites.
P3-B: Add account_id to tree_shares model + migration (backfill via created_by
→ users.account_id). TreeShare constructor updated in trees.py.
P3-C: Enable RLS on 6 remaining tables: step_ratings, step_usage_log,
target_lists, session_shares, audit_logs, tree_shares.
P3-D: Drop team_id from target_lists — endpoint, schema, and model now use
account_id as the sole isolation key.
P3-E: Append Phase 3 RLS isolation tests for all 6 tables.
test_target_lists.py: fix cross-account test to use Account model (not Team)
and set account_id on new User.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'medium' is not a valid value for ck_ai_sessions_confidence_tier.
Valid values are 'guided' | 'exploring' | 'discovery'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Service layer (production code):
- branch_manager: set account_id on SessionBranch (root + fork) and ForkPoint
from session.account_id; load session in create_fork for this purpose
- handoff_manager: set account_id on SessionHandoff from session.account_id
- ai_suggestions endpoint: set account_id on AISuggestion from current_user
- steps endpoint (/feedback): set account_id on StepRating from current_user
- ratings endpoint: set account_id on StepRating from current_user
Test infrastructure:
- conftest.py: seed PLATFORM_ACCOUNT_ID (00000000-...-0001) account after
Base.metadata.create_all so global categories and gallery items have a valid FK
- test_rls_isolation: add _ensure_rls_schema fixture that runs
'alembic upgrade head' before module tests — previous function-scoped
test_db fixtures drop the schema, leaving the RLS tests with no tables
- test_branding: create Account before User in helper functions
- test_admin_gallery: set account_id=PLATFORM_ACCOUNT_ID on Tree/ScriptTemplate
- test_public_templates: set account_id=PLATFORM_ACCOUNT_ID on Tree,
ScriptTemplate, TreeCategory
- test_resolution_outputs: set account_id=session.account_id on
SessionResolutionOutput
- test_analytics_phase5: set account_id on PsaPostLog
- test_draft_trees: replace account_id=None with PLATFORM_ACCOUNT_ID in
migration default test (NOT NULL now enforced)
- test_maintenance_schedules: set account_id on other_tree
- test_save_session_as_tree: set account_id on all 5 Session() constructors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All previously-nullable account_id columns are now NOT NULL.
tree_embeddings and feedback backfilled before constraint applied.
Global content assigned to platform sentinel account (00000000-...-0001)
in preceding migration.
Tables updated: users, trees, tree_categories, tree_tags,
step_categories, step_library, tree_embeddings, feedback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates template_trees and platform_steps (no account_id, no RLS).
Migrates is_default=TRUE trees and public steps into them.
Creates sentinel platform account (00000000-...-0001) for global
tree_categories, tree_tags, step_categories, step_library, and
is_default trees — clearing all NULL account_id rows in those tables
as prerequisite for Group 9 SET NOT NULL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Zero rows in production — this is a schema-only migration in practice.
team_id kept for app code compatibility. Drop deferred to later cleanup.
Backfill: team_id → team admin user → account_id; fallback: created_by.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
team_id is kept in all three tables — drop deferred until app code
is fully migrated off team_id references.
Tables: script_builder_sessions, script_templates, script_generations
Backfill: user_id/created_by → users.account_id
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
psa_post_log: backfill via psa_connection, fallback to posted_by user
psa_member_mappings: backfill via psa_connection
notification_logs: backfill via notification_config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backfill from rater/user's account_id (not the step's account_id).
This is an explicit design decision — step rating data is attributed
to the account that performed the rating.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: add tenant data isolation design spec
Complete architecture plan for multi-tenant data isolation across
all layers (PostgreSQL RLS, application-layer filtering, schema
migration, testing strategy, and phased rollout checklist).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: add background job isolation policy to tenant isolation spec
Documents policy for all 5 existing background jobs:
- Knowledge Flywheel and PSA Retry flagged for account_id threading
- Chat Retention already follows correct pattern (model for others)
- Maintenance Schedule Firing needs account_id in queries + Session creation
- AI Conversation Expiry approved as cross-tenant with justification
Adds approved cross-tenant query registry and Phase 2 checklist items.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: add tenant isolation Phase 0 implementation plan
8 tasks covering: CRITICAL copilot hotfix, tenant_filter() helper,
get_tenant_context dependency, analytics/category/AI session gap fixes,
full UUID endpoint audit, TargetList dead code audit, teams orphan
check, and CI grep check for missing tenant filters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add tenant_filter() helper and get_tenant_context dependency
tenant_filter(model, account_id) is the canonical app-layer tenant
scoping expression. Every query on a tenant table must use it.
build_tree_access_filter and build_step_visibility_filter updated
to call tenant_filter() internally for the account_id match.
get_tenant_context is a FastAPI dependency that returns account_id
or raises 403 if the user has no account — prevents raw access to
current_user.account_id and centralises the null check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: scope analytics/flows/{tree_id} to requesting account
Any authenticated user could read flow analytics (session counts,
completion rates, CSAT) for any tree UUID. Now returns 404 if the
tree doesn't belong to the requesting account.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: scope category tree_count to requesting account
tree_count on GET /categories/{id} was including trees from all
accounts, leaking cross-tenant row counts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: restrict AI session search to current user only
Search endpoint used OR(user_id, account_id), exposing other users'
problem_summary and problem_domain within the same account. Sessions
are user-scoped only — cross-user access requires explicit escalation
or sharing. List and search endpoints now behave consistently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add ownership check and 404 responses to ai-sessions endpoints
Cross-tenant isolation audit found:
- retry-psa-push had NO ownership check (CRITICAL) — any user could retry any session's PSA push
- save_task_lane used db.get() without ownership filter, returned 403 revealing existence
- get_session returned 403 instead of 404 for unauthorized access
- stream_documentation returned 403 instead of 404
All now use query-level user_id filtering and return 404 to avoid revealing existence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: return 404 instead of 403 for cross-tenant session access
All session endpoints (get, update, complete, scratchpad, variables, export,
ticket-link) now return 404 instead of 403 when a user tries to access
another user's session. This prevents confirming existence of resources
across tenant boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: return 404 instead of 403 for cross-tenant tree access
get_tree and update_tree now return 404 when a user cannot access a tree
(private tree from another account). Prevents confirming resource existence
across tenant boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: return 404 instead of 403 for cross-tenant step access
get_step_or_404 now returns 404 when can_view_step or can_edit_step fails,
preventing confirmation of step existence across tenant boundaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: return 404 instead of 403 for cross-tenant upload access
get_upload_url and delete_upload now return 404 when the upload belongs to
a different account/user, preventing resource existence confirmation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: return 404 instead of 403 for cross-tenant share access
revoke_share and create_share now return 404 when the caller is not the
owner, preventing resource existence confirmation across users.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: return 404 instead of 403 for cross-team tree access in maintenance schedules
_get_tree_or_403 now returns 404 when the user's team does not match,
preventing confirmation of tree existence across teams.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: return 404 instead of 403 for cross-account tag access
get_tag now returns 404 for account-specific tags that belong to another
account, preventing resource existence confirmation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: return 404 instead of 403 for cross-account step category access
get_step_category now returns 404 for account-specific categories that
belong to another account, preventing resource existence confirmation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: add cross-tenant isolation tests for Task 6 UUID audit
Tests cover:
- Tree GET/PUT returns 404 for cross-account access
- Session GET returns 404 for cross-user access
- AI session GET returns 404 for cross-user access
- AI session retry-psa-push requires ownership
- Upload URL returns 404 for cross-account access
- Share revoke returns 404 for cross-user access
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: return 404 (not 403) for get_documentation cross-user access; add missing Task 6 tests
get_documentation was revealing session existence via 403. Added pre-check
query filtering by session_id AND user_id before calling the engine.
Also add cross-tenant isolation tests for steps, tags, step_categories,
and maintenance_schedules endpoints fixed in Task 6 (TDD was skipped).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address Task 6 quality review — rename helper, restore 403 for intra-account, add docs test
- Rename _get_tree_or_403 → _get_tree_or_404 in maintenance_schedules.py
(function now raises 404, old name was misleading)
- Restore HTTP 403 for intra-account permission failures in update_tree:
same-account users who can see a tree but can't edit it got 404 (wrong);
only cross-account lookups should return 404 to avoid confirming existence
- Apply same 403/404 distinction to update_tree_visibility
- Add test: get_documentation must return 404 for cross-user session access
- Add comment documenting owner-only design for documentation endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: Task 7+8 — TargetList audit, CI tenant-filter grep check
Task 7: TargetList dead code audit
- Found active code references in 12+ files across backend and frontend
(full CRUD API + frontend page + MaintenanceScheduleSection + BatchLaunchModal)
- Decision: migrate to account_id in Phase 1 (cannot drop)
- DB row count not available from code-server — must verify from VPS SSH
before Phase 1 migration
- Teams orphan check query documented; must run from VPS SSH before Phase 1
- Results documented in spec Section 9
Task 8: CI tenant-filter enforcement check (warn mode)
- Create backend/scripts/check_tenant_filters.py
Scans endpoint and service files for select() on tenant tables without
tenant_filter/account_id/user_id in surrounding context. Currently
reports 109 warnings (Phase 1 backlog). Exits 0 (warn mode).
- Add Check tenant filter enforcement step to backend CI job
Add --fail flag after Phase 1 backlog clears to make it blocking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: record Phase 0 audit results — 0 orphaned teams, 0 target_list rows
Both checks confirmed 2026-04-09 from production DB.
Phase 1 migration is safe to proceed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: add tenant data isolation design spec
Complete architecture plan for multi-tenant data isolation across
all layers (PostgreSQL RLS, application-layer filtering, schema
migration, testing strategy, and phased rollout checklist).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: add background job isolation policy to tenant isolation spec
Documents policy for all 5 existing background jobs:
- Knowledge Flywheel and PSA Retry flagged for account_id threading
- Chat Retention already follows correct pattern (model for others)
- Maintenance Schedule Firing needs account_id in queries + Session creation
- AI Conversation Expiry approved as cross-tenant with justification
Adds approved cross-tenant query registry and Phase 2 checklist items.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: add tenant isolation Phase 0 implementation plan
8 tasks covering: CRITICAL copilot hotfix, tenant_filter() helper,
get_tenant_context dependency, analytics/category/AI session gap fixes,
full UUID endpoint audit, TargetList dead code audit, teams orphan
check, and CI grep check for missing tenant filters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: CRITICAL — scope copilot tree query to current account
A user who knew another account's tree UUID could start a copilot
conversation, causing the tree's full node structure, names, and
descriptions to be sent to the AI as part of the system prompt.
Fix: add account_id (or is_default / visibility='public') filter to
the tree SELECT in copilot_service.start_conversation(). Returns 404
for inaccessible trees. Test added in test_tenant_isolation_p0.py.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add DOCX MIME type to ALLOWED_DOCUMENT_TYPES in storage_service.py
- Add python-docx text extraction in _generate_ai_description
- Extract shared _store_document_content helper for PDF/DOCX
- Add python-docx>=1.1.0 to requirements.txt
- Add tests for docx upload acceptance and document fetch
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PDF uploads were stored in S3 and had text extracted during upload, but
fetch_upload_images() filtered exclusively for image MIME types, so
document content never reached the AI.
- Add fetch_upload_documents() in storage_service.py to retrieve
extracted_content for PDFs and text files
- Update ai_sessions.py chat endpoint to call both fetch_upload_images
and fetch_upload_documents, injecting document text as context
- Add PDF text extraction in _generate_ai_description (pypdf)
- Add pypdf>=4.0.0 to requirements.txt
- Fix test_db teardown to avoid connection pool issues
- Add 5 tests for fetch_upload_documents
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test was inserting a "Team Script" with team_id=NULL (since the
test user has no team), then expecting shared=true to return it.
SQL NULL=NULL is falsy, so the query correctly returned nothing.
Fix: create a team, assign it to the user, then test the filter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- script_template.py: add server_default to ALL NOT NULL columns so
Base.metadata.create_all matches Alembic behavior for raw SQL INSERTs
- test_session_branches_api.py: fork_reason needs 5+ chars ("test" → "testing fork")
- test_scripts.py: engineers CAN create templates (assert 201, not 403)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>