{"uuid": "e250c43d-9c59-4624-8705-fda8a5481de6", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "cve-2026-1234", "type": "seen", "source": "https://gist.github.com/muerte420/1fb8ba6f2443f4d92d5c23c44e7ed029", "content": "\n\n\n\n\nnexusos-psa \u2014 Wiki\n\n\n\n\n*{margin:0;padding:0;box-sizing:border-box}\n:root{\n  --bg:#ffffff;--sidebar-bg:#f8f9fb;--border:#e5e7eb;\n  --text:#1e293b;--text-muted:#64748b;--primary:#2563eb;\n  --primary-soft:#eff6ff;--hover:#f1f5f9;--code-bg:#f1f5f9;\n  --radius:8px;--shadow:0 1px 3px rgba(0,0,0,.08);\n}\nbody{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;\n  line-height:1.65;color:var(--text);background:var(--bg)}\n\n.layout{display:flex;min-height:100vh}\n.sidebar{width:280px;background:var(--sidebar-bg);border-right:1px solid var(--border);\n  position:fixed;top:0;left:0;bottom:0;overflow-y:auto;padding:24px 16px;\n  display:flex;flex-direction:column;z-index:10}\n.content{margin-left:280px;flex:1;padding:48px 64px;max-width:960px}\n\n.sidebar-header{margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--border)}\n.sidebar-title{font-size:16px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px}\n.sidebar-title svg{flex-shrink:0}\n.sidebar-meta{font-size:11px;color:var(--text-muted);margin-top:6px}\n.nav-section{margin-bottom:2px}\n.nav-item{display:block;padding:7px 12px;border-radius:var(--radius);cursor:pointer;\n  font-size:13px;color:var(--text);text-decoration:none;transition:all .15s;\n  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.nav-item:hover{background:var(--hover)}\n.nav-item.active{background:var(--primary-soft);color:var(--primary);font-weight:600}\n.nav-item.overview{font-weight:600;margin-bottom:4px}\n.nav-children{padding-left:14px;border-left:1px solid var(--border);margin-left:12px}\n.nav-group-label{font-size:11px;font-weight:600;color:var(--text-muted);\n  text-transform:uppercase;letter-spacing:.5px;padding:12px 12px 4px;user-select:none}\n.sidebar-footer{margin-top:auto;padding-top:16px;border-top:1px solid var(--border);\n  font-size:11px;color:var(--text-muted);text-align:center}\n\n.content h1{font-size:28px;font-weight:700;margin-bottom:8px;line-height:1.3}\n.content h2{font-size:22px;font-weight:600;margin:32px 0 12px;padding-bottom:6px;border-bottom:1px solid var(--border)}\n.content h3{font-size:17px;font-weight:600;margin:24px 0 8px}\n.content h4{font-size:15px;font-weight:600;margin:20px 0 6px}\n.content p{margin:12px 0}\n.content ul,.content ol{margin:12px 0 12px 24px}\n.content li{margin:4px 0}\n.content a{color:var(--primary);text-decoration:none}\n.content a:hover{text-decoration:underline}\n.content blockquote{border-left:3px solid var(--primary);padding:8px 16px;margin:16px 0;\n  background:var(--primary-soft);border-radius:0 var(--radius) var(--radius) 0;\n  color:var(--text-muted);font-size:14px}\n.content code{font-family:'SF Mono',Consolas,'Courier New',monospace;font-size:13px;\n  background:var(--code-bg);padding:2px 6px;border-radius:4px}\n.content pre{background:#1e293b;color:#e2e8f0;border-radius:var(--radius);padding:16px;\n  overflow-x:auto;margin:16px 0}\n.content pre code{background:none;padding:0;font-size:13px;line-height:1.6;color:inherit}\n.content table{border-collapse:collapse;width:100%;margin:16px 0}\n.content th,.content td{border:1px solid var(--border);padding:8px 12px;text-align:left;font-size:14px}\n.content th{background:var(--sidebar-bg);font-weight:600}\n.content img{max-width:100%;border-radius:var(--radius)}\n.content hr{border:none;border-top:1px solid var(--border);margin:32px 0}\n.content .mermaid{margin:20px 0;text-align:center}\n\n.menu-toggle{display:none;position:fixed;top:12px;left:12px;z-index:20;\n  background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);\n  padding:8px 12px;cursor:pointer;font-size:18px;box-shadow:var(--shadow)}\n@media(max-width:768px){\n  .sidebar{transform:translateX(-100%);transition:transform .2s}\n  .sidebar.open{transform:translateX(0);box-shadow:2px 0 12px rgba(0,0,0,.1)}\n  .content{margin-left:0;padding:24px 20px;padding-top:56px}\n  .menu-toggle{display:block}\n}\n.empty-state{text-align:center;padding:80px 20px;color:var(--text-muted)}\n.empty-state h2{font-size:20px;margin-bottom:8px;border:none}\n\n\n\n\n&#9776;\n\n\n\n\n\n\n\n\n\nnexusos-psa\n\n\n\n\n\n\n\nGenerated by GitNexus\n\n\n\n\n\nLoading\u2026\n\n\n\nvar PAGES = {\"branding\":\"# branding\\n\\n# NEXOS CORE Branding Module\\n\\n## Overview\\n\\nThe **branding** module is a comprehensive brand identity system for NEXOS CORE, a Business Intelligence and AI-powered strategic planning platform. This module contains all brand guidelines, visual assets, design specifications, and reference implementations needed to maintain consistent brand presentation across digital and print channels.\\n\\nThe module serves as the single source of truth for:\\n- Logo specifications and usage rules\\n- Color palette and accessibility standards\\n- Typography guidelines and font pairings\\n- Spacing and layout systems\\n- Content voice and messaging\\n- Implementation examples (landing pages, letterheads, typography samples)\\n\\n## Purpose &amp; Scope\\n\\nNEXOS CORE positions itself as a **premium, enterprise-grade platform** that transforms raw data into actionable business intelligence. The branding module ensures this positioning is communicated consistently through visual design, language, and user experience.\\n\\n**Key brand promise:** *Make IT the afterthought. Make financial success the priority.*\\n\\nThe module is designed for:\\n- **Designers** building interfaces and marketing materials\\n- **Developers** implementing brand standards in code\\n- **Content teams** maintaining voice and messaging consistency\\n- **Marketing** creating campaigns and collateral\\n- **Partners** using the brand in external communications\\n\\n## Core Brand Identity\\n\\n### Brand Pillars\\n\\n| Pillar | Description |\\n|--------|-------------|\\n| **Intelligence** | Data-driven decision making powered by AI |\\n| **Transformation** | Raw data \u2192 actionable insight \u2192 measurable results |\\n| **Growth** | Upward momentum reflecting increased profitability |\\n| **Premium** | Enterprise-grade, boardroom-confident, sophisticated |\\n\\n### Visual Language\\n\\nThe brand uses a **navy + cyan color system** to convey trust, intelligence, and upward momentum:\\n\\n- **Deep Navy** (`#0a1929`): Primary background, core identity, stability\\n- **Bright Cyan** (`#54e0dc`): Primary accent, intelligence, actionable insight\\n- **Teal/Light Cyan** (`#5ddbd9`, `#7eccd3`): Secondary accents, depth, data flows\\n- **Pale Cyan** (`#65e4de`): Light backgrounds, gradients, subtle elements\\n\\n**Color application rule:** 80% Deep Navy, 15% Bright Cyan, 5% secondary accents\\n\\n## Logo System\\n\\n### Design Concept\\n\\nThe NEXOS CORE logo visually represents the brand's core promise:\\n\\n1. **Central Intelligence Hub** \u2014 A focal point (cyan node) gathering intelligence\\n2. **Radiating Data Flows** \u2014 Lines extending outward representing insights flowing to drive decisions\\n3. **Ascending Growth** \u2014 Bar chart with upward momentum showing increased profitability\\n4. **Data to Results Pipeline** \u2014 Clear transformation from raw information to business outcomes\\n\\n### Logo Specifications\\n\\n**File:** `Firefly_give me a version with NEXOS in the same color as CORE and make the CORE same color a 32479.png`  \\n**Dimensions:** 1408 \u00d7 768 pixels (original)  \\n**Format:** PNG with transparency\\n\\n### Logo Variations &amp; Usage\\n\\n| Context | Variation | Minimum Width | Clear Space |\\n|---------|-----------|---------------|-------------|\\n| Website Hero | Full color on dark/white | 400px | 40px all sides |\\n| Favicon | Mark only | 32px | N/A |\\n| Print (A4) | Vector format | 200mm | 50mm all sides |\\n| Email Signature | Constrained | 100px | 15px all sides |\\n| Business Card | Mark + text lockup | 40mm \u00d7 25mm | Bleed edge |\\n\\n### Logo Lockup Rules\\n\\n**Horizontal Layout:**\\n- Mark (left) + text (right)\\n- Minimum 20px gap between mark and text\\n- \\\"NEXOS\\\" in Deep Navy, \\\"CORE\\\" in Bright Cyan\\n\\n**Vertical Layout:**\\n- Mark (top) + text (bottom)\\n- Minimum 15px gap\\n- Same color rules apply\\n\\n**Taglines:**\\n- Primary: \\\"Strategic Business Analysis &amp; Automation\\\" (Light Cyan)\\n- Secondary: \\\"Powered by Intelligently Curated AI\\\" (Light Cyan)\\n\\n### Logo Do's &amp; Don'ts\\n\\n\u2713 **Do:**\\n- Use official logo files only\\n- Maintain aspect ratio and clear space\\n- Use exact colors from COLOR_PALETTE.md\\n- Scale gracefully from 32px to print\\n- Use on solid backgrounds (navy, white, light gray)\\n\\n\u2717 **Don't:**\\n- Rotate, skew, or distort\\n- Change colors or add effects (shadows, glows)\\n- Use outdated versions\\n- Place on cluttered backgrounds without contrast backing\\n- Apply gradients beyond designed color pairs\\n\\n## Color Palette\\n\\n### Primary Colors\\n\\n```\\nDeep Navy Blue\\n  Hex: #0a1929\\n  RGB: 10, 25, 41\\n  Usage: Primary background, text, core identity\\n\\nBright Cyan (Primary Accent)\\n  Hex: #54e0dc\\n  RGB: 84, 224, 220\\n  Usage: CTAs, key metrics, data visualization, interactive elements\\n\\nTeal (Secondary Accent)\\n  Hex: #5ddbd9\\n  RGB: 93, 219, 217\\n  Usage: Hover states, data flows, visual hierarchy\\n\\nLight Cyan\\n  Hex: #7eccd3\\n  RGB: 126, 204, 211\\n  Usage: Borders, subtle elements, disabled states\\n\\nPale Cyan\\n  Hex: #65e4de\\n  RGB: 101, 228, 222\\n  Usage: Backgrounds, gradients, low-contrast accents\\n```\\n\\n### Gradient Definitions\\n\\n**Primary Gradient (Upward Momentum):**\\n- Navy (`#0a1929`) \u2192 Bright Cyan (`#54e0dc`)\\n- Use for buttons, progress indicators, upward momentum visuals\\n\\n**Accent Gradient (Data Flows):**\\n- Light Cyan (`#7eccd3`) \u2192 Pale Cyan (`#65e4de`)\\n- Use for radiating intelligence, data flows\\n\\n### Accessibility &amp; Contrast\\n\\nAll text colors meet **WCAG AA minimum (4.5:1 contrast ratio)**:\\n- Navy text on Cyan backgrounds: \u2713 WCAG AA compliant\\n- White text on Navy: \u2713 WCAG AAA compliant\\n- Use Bright Cyan for primary CTAs; Teal for secondary actions\\n\\n### Dark Mode\\n\\n- Background: `#0a1929` (already deeply saturated)\\n- Primary text: Pale Cyan (`#65e4de`)\\n- Interactive elements: Bright Cyan (`#54e0dc`)\\n\\n## Typography System\\n\\n### Font Stack\\n\\n**Headlines (H1\u2013H3):**\\n- Font: Fraunces, serif\\n- Weight: 700 (Bold)\\n- Letter Spacing: 0.3px\\n- Line Height: 1.2\\n- Color: Deep Navy (`#0a1929`)\\n- Purpose: Editorial, premium, confident \u2014 bold serif conveys sophistication\\n\\n**Body Text:**\\n- Font: Inter, sans-serif\\n- Weight: 400 (Regular)\\n- Size: 16px (web), 11pt (print)\\n- Line Height: 1.6\\n- Color: Deep Navy (`#0a1929`)\\n- Purpose: Clean, modern, readable \u2014 counterbalances bold headlines\\n\\n**UI Text &amp; Labels:**\\n- Font: Inter, sans-serif\\n- Weight: 500 (Medium)\\n- Size: 14px\\n- Letter Spacing: 0.25px\\n- Color: Deep Navy (`#0a1929`)\\n\\n**Code / Monospace:**\\n- Font: \\\"Courier New\\\", monospace\\n- Weight: 400\\n- Size: 12px\\n- Color: Bright Cyan (`#54e0dc`) on navy background\\n\\n### Type Scale\\n\\n```\\nH1: 56px (Fraunces Bold)\\nH2: 48px (Fraunces Bold)\\nH3: 32px (Fraunces Bold)\\nH4: 24px (Fraunces Bold)\\nBody: 16px (Inter Regular)\\nSmall: 14px (Inter Regular)\\nLabel: 12px (Inter Medium)\\n```\\n\\n### Font Pairing Rationale\\n\\nThe **Fraunces (serif) + Inter (sans)** pairing creates intentional visual contrast:\\n\\n- **Fraunces headlines** command attention with editorial confidence\u2014bold, geometric serifs suggest intelligence and premium positioning\\n- **Inter body text** keeps copy accessible and modern, preventing the serif from feeling dated\\n- **Together** they signal \\\"we're designed, deliberate, and distinctive\\\"\u2014not a template site\\n\\n### Fallback Stack\\n\\n```\\nHeadlines: Fraunces \u2192 Georgia \u2192 serif\\nBody: Inter \u2192 Helvetica Neue \u2192 system sans-serif\\n```\\n\\n## Spacing &amp; Grid System\\n\\n### Base Unit: 8px\\n\\nAll spacing follows an 8px grid system for consistency and alignment.\\n\\n| Scale | Value | Usage |\\n|-------|-------|-------|\\n| XS | 4px | Micro spacing (icon padding, small gaps) |\\n| S | 8px | Component spacing (button padding, small margins) |\\n| M | 16px | Default spacing (card margins, section gaps) |\\n| L | 24px | Large spacing (section divisions) |\\n| XL | 32px | Extra large spacing (major layout divisions) |\\n| XXL | 48px | Massive spacing (hero sections, page margins) |\\n\\n### Component Sizing\\n\\n- **Small Button:** 32px height, 8px vertical padding, 16px horizontal padding\\n- **Default Button:** 40px height, 12px vertical padding, 24px horizontal padding\\n- **Large Button:** 48px height, 16px vertical padding, 32px horizontal padding\\n\\n## Visual Elements\\n\\n### Icons\\n\\n- **Style:** Minimal, geometric, stroke-based (not filled)\\n- **Color:** Bright Cyan (`#54e0dc`) for primary; Light Cyan (`#7eccd3`) for secondary\\n- **Stroke Width:** 2px for sizes 24px+; 1.5px for sizes below 24px\\n- **Sizes:** 16px, 24px, 32px, 48px (multiples of 8)\\n\\n### Badges &amp; Status Indicators\\n\\n- **Success:** Bright Cyan background, Deep Navy text\\n- **Warning:** Amber/Gold accent, Deep Navy background\\n- **Error:** Red accent, Deep Navy background\\n- **Pending:** Light Cyan background, Deep Navy text\\n\\n### Cards &amp; Containers\\n\\n- **Background:** Deep Navy with 1px Light Cyan border\\n- **Padding:** 24px (3\u00d7 base unit)\\n- **Shadow:** `0 2px 8px rgba(84, 224, 220, 0.1)` (subtle cyan glow)\\n- **Corner Radius:** 8px (default), 4px (compact)\\n\\n### Data Visualization\\n\\n- **Primary Line/Bar Color:** Bright Cyan (`#54e0dc`)\\n- **Secondary/Comparison:** Teal (`#5ddbd9`)\\n- **Tertiary:** Light Cyan (`#7eccd3`)\\n- **Grid Lines:** Light Cyan at 20% opacity\\n- **Background:** Deep Navy (no additional fill behind charts)\\n\\n## Photography &amp; Imagery\\n\\n### Imagery Style\\n\\n- Modern, clean, professional\\n- Show technology in human context (people using BI insights to make decisions)\\n- Avoid generic \\\"tech stock photos\\\" (no circuit boards, binary code, generic network clouds)\\n- Prefer real data dashboards, strategy sessions, business metrics\\n- Maintain the navy + cyan color temperature in all imagery\\n\\n### Image Filters &amp; Overlays\\n\\n- **Optional:** Subtle cyan tint overlay (Bright Cyan at 5% opacity) to unify brand feel\\n- **Avoid:** Heavy shadows, excessive vignettes, oversaturated colors\\n- **Preferred:** Clean, bright, minimal post-processing\\n\\n## Content &amp; Voice\\n\\n### Brand Voice Characteristics\\n\\n- **Professional &amp; Confident:** Enterprise-grade, not playful or cute\\n- **Results-Driven:** Focus on outcomes and profitability, not process\\n- **Clear &amp; Direct:** No marketing buzzwords, jargon, or fluff\\n- **Strategic &amp; Forward-Thinking:** Suggest growth, transformation, and strategic advantage\\n\\n### Messaging Pillars\\n\\n1. **Intelligence:** AI-powered data transformation\\n2. **Profitability:** Financial success for clients (the real goal)\\n3. **Automation:** Strategic planning and execution powered by intelligence\\n4. **Results:** Measurable, proven increases in client success\\n\\n### Copy Patterns\\n\\n\u2713 **Good:**\\n- \\\"Transform raw data into strategic advantage\\\"\\n- \\\"Intelligence that drives increased profitability\\\"\\n- \\\"Business success powered by AI\\\"\\n\\n\u2717 **Avoid:**\\n- \\\"Cutting-edge solutions in the cloud\\\"\\n- \\\"Next-gen synergistic platforms\\\"\\n- Generic tech clich\u00e9s and buzzwords\\n\\n## Implementation Examples\\n\\nThe module includes four reference implementations demonstrating brand application:\\n\\n### 1. Landing Page (`landing_page_sample.html`)\\n\\nA complete hero-to-footer landing page showing:\\n- Header with navigation\\n- Hero section with logo, headline, and CTAs\\n- Features grid with cards\\n- Value proposition section with benefits list\\n- CTA section\\n- Footer with links\\n\\n**Key patterns:**\\n- Fraunces headlines with Inter body text\\n- Navy background with cyan accents\\n- 8px grid spacing throughout\\n- Responsive grid layouts\\n\\n### 2. Letterhead Samples (3 variations)\\n\\nThree distinct letterhead designs demonstrating different approaches:\\n\\n**Sample 1: Classic Header**\\n- Full-color gradient header with logo\\n- Navy background, cyan divider line\\n- Centered company info\\n- Formal footer with contact details\\n\\n**Sample 2: Minimalist Left Border**\\n- Thin left border accent (gold/tan)\\n- Centered logo and taglines\\n- Clean, minimal header\\n- Light footer background\\n\\n**Sample 3: Formal Centered**\\n- Centered logo and company name\\n- Gold/tan bottom border\\n- Formal letter structure\\n- Professional footer with contact info\\n\\nAll three include:\\n- Proper spacing and margins\\n- Brand colors and typography\\n- Watermark background\\n- Print-ready styling\\n\\n### 3. Typography Samples (4 options)\\n\\nFour different typography pairings to explore alternatives:\\n\\n**Option 1: Clash Display + Work Sans**\\n- Bold, geometric, contemporary aesthetic\\n- High personality, modern feel\\n\\n**Option 2: DM Sans (Single Family)**\\n- Refined, geometric, sophisticated\\n- Subtle personality, cohesive feel\\n\\n**Option 3: IBM Plex Mono + IBM Plex Sans**\\n- Technical, intelligent, premium enterprise\\n- Monospace headlines for technical credibility\\n\\n**Option 4A: Rubik (Sohne Alternative)**\\n- Luxury minimalist aesthetic\\n- Clean, sophisticated, modern\\n\\nEach sample demonstrates the typography in context with full page layouts.\\n\\n### 4. Logo Concepts (2 HTML files)\\n\\n**Logo v1:** Three geometric logo concepts\\n- Ascending Core (hexagonal core with upward momentum)\\n- Rising Core with Wordmark (teal/emerald with OS layers)\\n- Minimalist Core (geometric with ascending chevron)\\n\\n**Logo v2:** Three BI-focused logo concepts\\n- Data to Insight Ascending (data points converging upward)\\n- Intelligence Pyramid (data transformation pyramid)\\n- Intelligence Network (network hub with radiating insights)\\n\\nAll concepts use SVG for scalability and include detailed descriptions of visual metaphors.\\n\\n## Usage Scenarios\\n\\n### Website Hero\\n- Navy background, full-color logo (200mm+)\\n- Primary tagline: \\\"Strategic Business Analysis &amp; Automation\\\" (Bright Cyan)\\n- Secondary tagline: \\\"Powered by Intelligently Curated AI\\\" (Light Cyan)\\n- Headline in H1 (navy), subheading in Pale Cyan\\n\\n### Email Header\\n- Deep Navy background with gradient\\n- Logo (100px width) with \\\"NEXOS CORE\\\" wordmark\\n- Taglines in Light Cyan\\n- Light Cyan divider line below header\\n\\n### Social Media\\n- Square format: Navy background, centered logo, Bright Cyan text overlay\\n- Rectangular: Logo on left, Bright Cyan accent bar on right with key message\\n- All text: Use Pale Cyan on navy for contrast\\n\\n### Business Cards\\n- Front: Logo lockup (40mm), Deep Navy background\\n- Back: Contact info in navy text on light gray background, Bright Cyan accent bar (3mm) on left edge\\n\\n### Presentations\\n- Cover Slide: Navy background, full logo, title in H1 (navy), subtitle in Pale Cyan\\n- Body Slides: Navy sidebar (left), white content area (right), Bright Cyan accents for key points\\n- Data Slides: Navy background, Bright Cyan for bars/lines, navy for grid\\n\\n### Print Collateral\\n- Brochures: Navy cover, white pages, Bright Cyan headers and callouts\\n- Poster: Navy background, large logo (200mm+), Bright Cyan text for key messages\\n- Reports: Navy cover with logo, white pages with navy headings, Bright Cyan for data highlights\\n\\n## File Organization\\n\\n```\\nbranding/\\n\u251c\u2500\u2500 BRAND_GUIDELINES.md          # Master brand guidelines (this file)\\n\u251c\u2500\u2500 COLOR_PALETTE.md             # Detailed color specifications\\n\u251c\u2500\u2500 adobe_firefly_prompt.md      # Logo design brief for Adobe Firefly\\n\u251c\u2500\u2500 landing_page_sample.html     # Full landing page reference\\n\u251c\u2500\u2500 letterhead_sample_1.html     # Classic header letterhead\\n\u251c\u2500\u2500 letterhead_sample_2.html     # Minimalist left border letterhead\\n\u251c\u2500\u2500 letterhead_sample_3.html     # Formal centered letterhead\\n\u251c\u2500\u2500 nexoscore_logo_v1.html       # Logo concept variations (v1)\\n\u251c\u2500\u2500 nexoscore_logo_v2.html       # Logo concept variations (v2)\\n\u251c\u2500\u2500 typography_sample_1.html     # Clash Display + Work Sans\\n\u251c\u2500\u2500 typography_sample_2.html     # DM Sans (single family)\\n\u251c\u2500\u2500 typography_sample_3.html     # IBM Plex Mono + IBM Plex Sans\\n\u2514\u2500\u2500 typography_sample_4a.html    # Rubik (Sohne alternative)\\n```\\n\\n## Brand Evolution &amp; Versioning\\n\\n**Current Version:** 1.3 (as of 2026-05-19)\\n\\nThe brand guidelines follow semantic versioning:\\n- **Major updates** (logo, colors, voice): Require stakeholder approval and version bump\\n- **Minor updates** (spacing refinements, new application examples): Document with date and changelog\\n- **All changes:** Distribute updated guidelines to all teams and design systems\\n\\n### Recent Changes\\n\\n| Version | Date | Change |\\n|---------|------|--------|\\n| 1.3 | 2026-05-19 | Refine primary tagline to \\\"Strategic Business Analysis &amp; Automation\\\" |\\n| 1.2 | 2026-05-19 | Update tagline to \\\"Powered by Intelligently Curated AI\\\" |\\n| 1.1 | 2026-05-19 | Add \\\"Powered by AI\\\" to company tagline; update logo lockup |\\n| 1.0 | 2026-05-19 | Initial brand guidelines |\\n\\n## Integration with Development\\n\\n### Web Implementation\\n\\nWhen implementing NEXOS CORE branding in web projects:\\n\\n1. **Load fonts from Google Fonts or licensed CDN:**\\n   ```html\\n   \\n   ```\\n\\n2. **Use CSS custom properties for colors:**\\n   ```css\\n   :root {\\n     --color-navy: #0a1929;\\n     --color-cyan: #54e0dc;\\n     --color-teal: #5ddbd9;\\n     --color-light-cyan: #7eccd3;\\n     --color-pale-cyan: #65e4de;\\n   }\\n   ```\\n\\n3. **Apply 8px grid spacing:**\\n   ```css\\n   --spacing-xs: 4px;\\n   --spacing-s: 8px;\\n   --spacing-m: 16px;\\n   --spacing-l: 24px;\\n   --spacing-xl: 32px;\\n   --spacing-xxl: 48px;\\n   ```\\n\\n4. **Use semantic color classes:**\\n   ```html\\n   Start Free Trial&lt;\\/button&gt;\\n   Learn More&lt;\\/a&gt;\\n   ```\\n\\n### Design System Integration\\n\\nThe branding module should be integrated into your design system as:\\n- **Color tokens** (exported to Figma, Storybook, CSS)\\n- **Typography tokens** (font families, sizes, weights, line heights)\\n- **Spacing tokens** (8px grid multiples)\\n- **Component library** (buttons, cards, badges using brand colors)\\n- **Icon library** (stroke-based, brand colors)\\n\\n## Contact &amp; Support\\n\\nFor questions about brand usage, logo files, color specifications, or application scenarios:\\n\\n- **Primary Contact:** Product Lead / Brand Owner\\n- **Design System:** Maintained in `/branding/` folder of NexusOS PSA repo\\n- **Updates:** Check BRAND_GUIDELINES.md for latest version date and changelog\\n\\n---\\n\\n## Quick Reference Checklist\\n\\nUse this checklist when creating brand-aligned materials:\\n\\n- [ ] Logo uses official files with correct colors and clear space\\n- [ ] Color palette follows 80/15/5 rule (80% Navy, 15% Cyan, 5% secondary)\\n- [ ] All text meets WCAG AA contrast minimum (4.5:1)\\n- [ ] Headlines use Fraunces Bold, body uses Inter Regular\\n- [ ] Spacing follows 8px grid system\\n- [ ] Copy is results-driven, not feature-focused\\n- [ ] No generic tech clich\u00e9s or buzzwords\\n- [ ] Imagery is professional, modern, human-centered\\n- [ ] Brand voice is confident, clear, and strategic\\n- [ ] All materials reference current brand version (1.3+)\",\"cmd\":\"# cmd\\n\\n# cmd Module Documentation\\n\\nThe `cmd` module contains the entry points for NexusOS services. It includes three main binaries: the PSA (Professional Services Automation) server, the auth-gate TCP proxy, and a one-off QuickBooks item synchronization tool.\\n\\n## Overview\\n\\n### cmd/psa/main.go \u2014 NexusOS PSA Server\\n\\nThe PSA server is the core application binary. It initializes the entire platform: database connections, JWT authentication, HTTP routing, background services, and integrations with external systems (QuickBooks, Metasploit, SIEM, etc.).\\n\\n**Key responsibilities:**\\n- Load configuration from environment variables\\n- Connect to PostgreSQL and run migrations\\n- Initialize JWT manager and session management\\n- Register HTTP routes for all modules (helpdesk, compliance, RMM, billing, etc.)\\n- Start background workers (SLA checker, QB sync, email polling, etc.)\\n- Enforce authentication and authorization\\n- Manage graceful shutdown\\n\\n**Deployment:**\\n```bash\\ngo build -o nexusos-psa ./cmd/psa/\\n./nexusos-psa -addr :3000\\n./nexusos-psa -migrate  # Run migrations and exit\\n```\\n\\n### cmd/auth-gate/main.go \u2014 TCP Proxy for RMM Relay\\n\\nA lightweight TCP proxy that sits in front of the NexusRMM relay (hbbs/hbbr). It validates session tokens from incoming agent connections before forwarding them to the relay backend.\\n\\n**Key responsibilities:**\\n- Listen for incoming TCP connections\\n- Extract and validate session tokens from the handshake frame\\n- Call the PSA API to validate tokens\\n- Forward authenticated connections to the relay backend\\n- Drop unauthenticated connections\\n\\n**Deployment:**\\n```bash\\nGOOS=linux GOARCH=amd64 go build -o auth-gate ./cmd/auth-gate/\\n./auth-gate -listen :21116 -relay 127.0.0.1:21117 -api https://nexusos.example.com\\n```\\n\\n### cmd/qb-push-items/main.go \u2014 QuickBooks Item Sync Tool\\n\\nA one-off utility that creates service items in QuickBooks Online and links them to local products. Safe to run multiple times\u2014it checks for existing items by name.\\n\\n**Key responsibilities:**\\n- Query QBO for income accounts\\n- Create service items that exist locally but not in QBO\\n- Update the local products table with QB item IDs\\n\\n---\\n\\n## PSA Server Architecture\\n\\n### Initialization Flow\\n\\n```\\nmain()\\n  \u251c\u2500 Parse flags (addr, migrate, version)\\n  \u251c\u2500 Load config from environment\\n  \u251c\u2500 Connect to PostgreSQL\\n  \u251c\u2500 Run migrations\\n  \u251c\u2500 Initialize JWT manager (with fallback for dev mode)\\n  \u251c\u2500 Initialize session manager\\n  \u251c\u2500 Initialize template renderer\\n  \u251c\u2500 Build HTTP router (mux)\\n  \u251c\u2500 Register all module routes\\n  \u251c\u2500 Start background workers\\n  \u251c\u2500 Apply middleware stack\\n  \u251c\u2500 Start HTTP server (:3000)\\n  \u251c\u2500 Start mTLS agent listener (:8443)\\n  \u2514\u2500 Wait for SIGINT/SIGTERM \u2192 graceful shutdown\\n```\\n\\n### HTTP Router Structure\\n\\nThe PSA server uses Go's `http.ServeMux` with Go 1.22+ path patterns. Routes are organized by module:\\n\\n**Public routes (no auth required):**\\n- `/health` \u2014 Health check\\n- `/login` \u2014 Login page\\n- `/invite/accept` \u2014 Invitation acceptance\\n- `/install/*` \u2014 Agent installer downloads\\n- `/api/rmm/enroll` \u2014 Agent enrollment\\n- `/api/devices/*` \u2014 Device API (HTTP-signature auth)\\n- `/api/ca/enroll` \u2014 Certificate enrollment\\n- `/portal/*` \u2014 Client portal (separate auth)\\n\\n**Authenticated routes (JWT required):**\\n- `/` \u2014 Dashboard\\n- `/api/helpdesk/*` \u2014 Ticketing system\\n- `/api/compliance/*` \u2014 Compliance frameworks\\n- `/api/rmm/*` \u2014 RMM agent management\\n- `/api/billing/*` \u2014 Quotes and invoices\\n- `/api/crm/*` \u2014 CRM and deals\\n- And 30+ other modules...\\n\\n### Authentication &amp; Authorization\\n\\n#### JWT Manager\\n\\nThe `auth.JWTManager` validates access tokens from the `Authorization: Bearer` header or `access_token` cookie. If JWT keys fail to load and `DEV_MODE` is not set, the server refuses to start (audit finding H4).\\n\\n```go\\n// In enforceAuth middleware:\\nif tokenStr != \\\"\\\" {\\n    if claims, err := jwtMgr.VerifyAccessToken(tokenStr); err == nil {\\n        ctx := auth.ContextWithClaims(r.Context(), claims)\\n        // ... inject user info and UI prefs into context\\n        next.ServeHTTP(w, r.WithContext(ctx))\\n        return\\n    }\\n}\\n```\\n\\n#### Dev Mode Fallback\\n\\nWhen `DEV_MODE=true` and no valid token is present, the server auto-authenticates as the first active user. This prevents lockout during development but is never used in production.\\n\\n```go\\nif devMode {\\n    // Query first active user and inject into context\\n    var devTenantID, devUserID, devEmail, devFullName, devRole string\\n    pool.QueryRow(r.Context(), `\\n        SELECT u.tenant_id::text, u.id::text, u.email, u.full_name, COALESCE(r.name, 'admin')\\n        FROM users u LEFT JOIN roles r ON r.id = u.role_id\\n        WHERE u.is_active = true ORDER BY u.created_at LIMIT 1\\n    `).Scan(&amp;devTenantID, &amp;devUserID, &amp;devEmail, &amp;devFullName, &amp;devRole)\\n    // ... create claims and inject\\n}\\n```\\n\\n#### Middleware Stack\\n\\nApplied in order (innermost to outermost):\\n1. **CSRF protection** \u2014 Validates CSRF tokens on state-changing requests\\n2. **Logging** \u2014 Logs all HTTP requests\\n3. **Recovery** \u2014 Catches panics and returns 500\\n4. **Auth enforcement** \u2014 Validates JWT or redirects to login\\n5. **UI preferences injection** \u2014 Loads per-user theme/nav settings\\n\\n### Key Adapters &amp; Bridges\\n\\nThe PSA server wires together 30+ internal modules. Several adapters bridge incompatible interfaces:\\n\\n#### sessionAdapter\\nBridges the `session_recordings` table to the `helpdesk.SessionProvider` interface. Allows the helpdesk module to query agent session history.\\n\\n```go\\ntype sessionAdapter struct {\\n    pool *pgxpool.Pool\\n}\\n\\nfunc (sa *sessionAdapter) GetSessionsForAgent(ctx context.Context, tenantID, agentID string) ([]helpdesk.SessionSummaryData, error) {\\n    // Query session_recordings table\\n}\\n```\\n\\n#### rmmCommandAdapter\\nInserts commands directly into the `agent_commands` table instead of calling a separate RMM server. Implements `helpdesk.RMMCommander`.\\n\\n```go\\nfunc (a *rmmCommandAdapter) SendCommand(ctx context.Context, agentID string, cmd helpdesk.RMMCommand) (string, error) {\\n    _, err := a.pool.Exec(ctx, `\\n        INSERT INTO agent_commands (tenant_id, rmm_agent_id, command_id, action, payload)\\n        VALUES ($1::uuid, $2, $3, $4, $5)\\n    `, tenantID, agentID, cmd.ID, cmd.Action, cmd.Payload)\\n    return cmd.ID, err\\n}\\n```\\n\\n#### orchNotifyAdapter\\nBridges `notify.Service` to the `helpdesk.OrchestratorNotifier` interface. Allows the workflow orchestrator to send notifications.\\n\\n#### emailSecAIAdapter\\nBridges `ai.Provider` to the `emailsec.AIEmailAnalyzer` interface. Allows email security to use Claude for threat analysis.\\n\\n---\\n\\n## Background Workers\\n\\nThe PSA server starts several long-running goroutines:\\n\\n### SLA Breach Checker\\nRuns every 1 minute, checking for tickets that have breached response or resolution SLAs.\\n\\n```go\\nslaEngine := helpdesk.NewSLAEngine(pool)\\ngo slaEngine.StartBreachChecker(slaCtx, 1*time.Minute)\\n```\\n\\n### Agent Session Cleanup\\nRuns periodically to clean up stale agent sessions and check for staleness.\\n\\n```go\\ngo agentAPI.StartSessionCleanup(agentCtx)\\ngo agentAPI.StartStalenessChecker(agentCtx)\\n```\\n\\n### Compliance Evidence Persistence\\nRuns every 30 seconds, reading compliance evidence from the database and persisting it to disk.\\n\\n```go\\ngo func() {\\n    ticker := time.NewTicker(30 * time.Second)\\n    for range ticker.C {\\n        // Persist device evidence for all companies\\n    }\\n}()\\n```\\n\\n### QuickBooks Sync\\nSyncs products, invoices, and customers from QB every 15 minutes.\\n\\n```go\\ngo qbService.StartScheduler(qbCtx, 15*time.Minute)\\n```\\n\\n### QB Recurring Billing\\nChecks for contracts to bill every 1 hour.\\n\\n```go\\ngo qbService.StartRecurringBillingScheduler(qbCtx, 1*time.Hour)\\n```\\n\\n### Distributor Ingest\\nNightly sync of distributor catalog from the MCP server (if `SIEM_ENCRYPTION_KEY` is set).\\n\\n```go\\ngo distScheduler.Start(qbCtx, 24*time.Hour)\\n```\\n\\n### NexusPulse Monthly Collection\\nCollects SMART numbers (revenue, margin, etc.) daily, auto-finalizes previous month on the 3rd.\\n\\n```go\\ngo func() {\\n    time.Sleep(15 * time.Second)\\n    pulseHandler.RunMonthlyCollection(ctx)\\n    ticker := time.NewTicker(24 * time.Hour)\\n    for range ticker.C {\\n        pulseHandler.RunMonthlyCollection(ctx)\\n    }\\n}()\\n```\\n\\n### SIEM Log Retention\\nHourly purge of expired events based on Nexie score tiers.\\n\\n```go\\ngo siemHandler.StartLogRetention(ctx)\\n```\\n\\n### Email Security Polling\\nPolls Microsoft Graph for email threats every 5 minutes.\\n\\n```go\\ngo emailPipeline.StartPolling(emailSecCtx, 5*time.Minute)\\n```\\n\\n### NCSR Stale Job Sweeper\\nMarks plan jobs stuck in 'running' state as interrupted, sweeps every 60 seconds with a 90-second heartbeat window.\\n\\n```go\\nncsrHandler.StartStaleJobSweeper(context.Background(), 60*time.Second, 90*time.Second)\\n```\\n\\n### EPA Nightly Baseline\\nRecalculates 30-day rolling focus-score and active-hours averages per employee, runs every 24 hours.\\n\\n```go\\ngo func() {\\n    ticker := time.NewTicker(24 * time.Hour)\\n    for range ticker.C {\\n        // Update baseline for all tenants\\n    }\\n}()\\n```\\n\\n---\\n\\n## Module Integration Points\\n\\n### Orchestrator Workflow Engine\\n\\nThe helpdesk module's `WorkflowEngine` is wired with cross-module callbacks that allow workflows to trigger actions in other modules:\\n\\n```go\\nwfEngine.SetModuleActions(helpdesk.ModuleActions{\\n    CreateAssessment: func(ctx context.Context, tenantID, companyID, dealID string) (string, error) {\\n        // Create assessment in assessment module\\n    },\\n    CreateContractFromQuote: func(ctx context.Context, tenantID, quoteID string) (string, error) {\\n        // Create contract in legal module\\n    },\\n    GenerateInvoiceFromQuote: func(ctx context.Context, tenantID, quoteID string) (string, error) {\\n        // Push invoice to QuickBooks\\n    },\\n    TriggerOnboarding: func(ctx context.Context, tenantID, contractID, companyID string) error {\\n        // Start onboarding pipeline\\n    },\\n    AdvanceDealStage: func(ctx context.Context, tenantID, dealID, stageName string) error {\\n        // Move deal to next stage in CRM\\n    },\\n    // ... more actions\\n})\\n```\\n\\n### CRM \u2192 Orchestrator Events\\n\\nWhen a deal stage changes in the CRM, it emits an event to the orchestrator:\\n\\n```go\\ncrmHandler.OnDealStageChanged = func(ctx context.Context, tenantID, dealID, companyID, stageID, stageName string, isWon, isLost bool, value float64) {\\n    wfEngine.EvaluateModuleEvent(ctx, tenantID, helpdesk.OrchestratorEvent{\\n        Category:   \\\"crm\\\",\\n        EventType:  \\\"deal_stage_changed\\\",\\n        EntityID:   dealID,\\n        EntityType: \\\"deal\\\",\\n        Fields: map[string]string{\\n            \\\"company_id\\\": companyID,\\n            \\\"stage_id\\\":   stageID,\\n            \\\"stage_name\\\": stageName,\\n            \\\"is_won\\\":     fmt.Sprintf(\\\"%v\\\", isWon),\\n            \\\"is_lost\\\":    fmt.Sprintf(\\\"%v\\\", isLost),\\n            \\\"deal_value\\\": fmt.Sprintf(\\\"%.2f\\\", value),\\n        },\\n    })\\n}\\n```\\n\\n### Bid Engine \u2192 Orchestrator Events\\n\\nSimilar pattern for bid creation and status changes.\\n\\n### Assessment \u2192 Orchestrator Events\\n\\nEmits events when assessments are completed or approved.\\n\\n### Survey \u2192 Orchestrator Events\\n\\nEmits events when surveys are completed, allowing workflows to auto-create bids.\\n\\n### Risk Acceptance Service\\n\\nA shared service across multiple compliance modules (NCSR, CMMC, Composer). Each module registers a `SourceResolver` so the Risk Register can deep-link back and flip status when clients sign/revoke.\\n\\n```go\\nrisk.Register(risk.ModuleNCSR, ncsr.NewRiskResolver(pool))\\nrisk.Register(risk.ModuleCMMC, cmmc.NewRiskResolver(pool))\\nrisk.Register(risk.ModuleComposer, composer.NewRiskResolver(pool))\\n```\\n\\n### Review Engine\\n\\nA generic host-driven, participant-attested review primitive. First domain is Compliance Review (NCSR queue). Each framework registers an `ActionPlanSeeder`:\\n\\n```go\\nreviewRegistry := review.NewRegistry()\\nreviewRegistry.Register(ncsr.NewSeeder(ncsrHandler))\\ncomposerHandler.SetReview(reviewHandler.Repo(), reviewRegistry)\\n```\\n\\nWhen a review session completes, decisions are fanned back to the seeder:\\n\\n```go\\nreviewHandler.RegisterCompletionHook(func(ctx context.Context, sessionID, frameworkSlug, companyID, contextID string) {\\n    seeder := reviewRegistry.Get(frameworkSlug)\\n    if seeder != nil {\\n        seeder.ApplyDecisions(ctx, sessionID)\\n        seeder.RecomputeScore(ctx, contextID)\\n    }\\n})\\n```\\n\\n### QuickBooks Integration\\n\\nQB is the single source of truth for financial data. The PSA reads products/invoices/customers from QB and pushes estimates and time-based invoices back.\\n\\n**Quote \u2192 Contract \u2192 QB Invoice pipeline:**\\n1. Quote is approved\\n2. Legal contract is auto-generated from the quote\\n3. Invoice is pushed to QuickBooks\\n4. Escalation and SLA policies are auto-applied to the client company\\n\\n```go\\nbillingHandler.OnQuoteApproved = func(ctx context.Context, tenantID, quoteID string) {\\n    contractID, _ := legalHandler.CreateContractFromQuote(ctx, tenantID, quoteID)\\n    qbService.Syncer.PushInvoiceFromQuote(ctx, tenantID, quoteID)\\n    // Auto-apply policies...\\n}\\n```\\n\\n**Timesheet \u2192 QB TimeActivity:**\\n```go\\ntimesheetHandler.SetQBPusher(quickbooks.NewTimesheetPusher(qbService))\\n```\\n\\n### Bid Spec \u2192 Drawing Takeoff\\n\\nWhen a bid spec drawing is uploaded, the Vision AI pipeline runs takeoff and persists results:\\n\\n```go\\nbidHandler.OnDrawingTakeoff = func(ctx context.Context, tenantID, drawingID, filePath string) {\\n    drawingHandler.RunTakeoffAndPersist(ctx, tenantID, drawingID, filePath)\\n}\\n```\\n\\n### Email Security \u2192 Helpdesk Workflow\\n\\nEmail threat analysis results can trigger helpdesk workflows:\\n\\n```go\\nemailPipeline.SetWorkflow(wfEngine)\\n```\\n\\n---\\n\\n## Configuration\\n\\nConfiguration is loaded from environment variables via `config.Load()`:\\n\\n| Variable | Default | Purpose |\\n|----------|---------|---------|\\n| `LISTEN_ADDR` | `:3000` | HTTP server listen address |\\n| `DATABASE_URL` | (required) | PostgreSQL connection string |\\n| `JWT_PRIVATE_KEY_PATH` | (required in prod) | Path to JWT signing key |\\n| `JWT_PUBLIC_KEY_PATH` | (required in prod) | Path to JWT verification key |\\n| `DEV_MODE` | `false` | Enable dev-only features (auto-auth, no JWT required) |\\n| `PUBLIC_URL` | (required) | Public-facing URL for links in emails, etc. |\\n| `RMM_LISTEN_ADDR` | `:8443` | mTLS agent listener address |\\n| `RMM_CERT_PATH` | (required for agents) | Server certificate for mTLS |\\n| `RMM_KEY_PATH` | (required for agents) | Server key for mTLS |\\n| `RMM_CA_CERT_PATH` | (required for agents) | CA certificate for mTLS |\\n| `SIEM_ENCRYPTION_KEY` | (optional) | AES-256-GCM key for SIEM/Sentinel (64 hex chars) |\\n| `SIEM_SYSLOG_PORT` | `5514` | Syslog listener port |\\n| `SIEM_TLS_CERT_PATH` | (optional) | TLS cert for syslog |\\n| `SIEM_TLS_KEY_PATH` | (optional) | TLS key for syslog |\\n| `SENTINEL_SOCKS_BIND_IP` | (optional) | IP to bind SOCKS5 hub to |\\n| `SENTINEL_SOCKS_PORT_MIN` | `40000` | Min port for SOCKS5 hub |\\n| `SENTINEL_SOCKS_PORT_MAX` | `40999` | Max port for SOCKS5 hub |\\n| `SENTINEL_SOCKS_ALLOWED_SOURCES` | (optional) | Comma-separated CIDR blocks allowed to connect to SOCKS hub |\\n| `QB_SANDBOX` | `true` | Use QB sandbox (dev) or production |\\n| `QB_REDIRECT_URI` | `http://localhost:3000/auth/quickbooks/callback` | OAuth redirect URI |\\n| `RECORDER_STORAGE_ROOT` | (required) | Path to store session recordings |\\n| `PORTAL_BASE_URL` | `http://localhost:3000` | Base URL for client portal links |\\n\\n---\\n\\n## Auth-Gate Proxy\\n\\n### How It Works\\n\\n1. **Listen** on the configured address (default `:21116`)\\n2. **Accept** incoming TCP connections from agents\\n3. **Read** the first frame (up to 4KB) \u2014 expect JSON handshake with token\\n4. **Extract** the session token from the frame (JSON `token` field or raw `nxs_` prefix)\\n5. **Validate** the token by calling the PSA API (`POST /api/remote/session/validate`)\\n6. **Forward** the handshake frame to the relay backend if valid\\n7. **Proxy** bidirectional traffic between client and relay\\n\\n### Token Extraction\\n\\nThe `extractToken()` function tries two approaches:\\n\\n1. **JSON parse:** Look for `{\\\"token\\\":\\\"nxs_...\\\", ...}`\\n2. **Fallback:** Scan for `nxs_` prefix in raw bytes, extract until quote/brace/comma/space\\n\\nTokens must be at least 68 characters (`nxs_` + 64 hex chars).\\n\\n### Token Validation\\n\\nCalls `POST {API_URL}/api/remote/session/validate` with:\\n```json\\n{\\n  \\\"token\\\": \\\"nxs_...\\\"\\n}\\n```\\n\\nOptional header: `X-Gate-Key: {API_KEY}` (if `NX_AUTH_GATE_KEY` env is set)\\n\\nResponse (on success):\\n```json\\n{\\n  \\\"device_id\\\": \\\"...\\\",\\n  \\\"tech_id\\\": \\\"...\\\",\\n  \\\"tenant_id\\\": \\\"...\\\",\\n  \\\"session_type\\\": \\\"...\\\"\\n}\\n```\\n\\n### Error Handling\\n\\n- **Handshake read timeout:** 10 seconds\\n- **Relay connect timeout:** 5 seconds\\n- **API request timeout:** 5 seconds\\n- **Invalid token:** Returns `{\\\"error\\\":\\\"invalid or expired session token\\\"}` and closes connection\\n- **Relay unavailable:** Returns `{\\\"error\\\":\\\"relay unavailable\\\"}` and closes connection\\n\\n---\\n\\n## Graceful Shutdown\\n\\nThe PSA server listens for `SIGINT` and `SIGTERM`:\\n\\n```go\\ndone := make(chan os.Signal, 1)\\nsignal.Notify(done, os.Interrupt, syscall.SIGTERM)\\n\\n&lt;-done\\nlog.Printf(\\\"server: shutting down...\\\")\\n\\nshutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\\ndefer cancel()\\n\\nslaCancel() // Stop SLA checker\\nif err := srv.Shutdown(shutdownCtx); err != nil {\\n    log.Printf(\\\"server: shutdown error: %v\\\", err)\\n}\\n```\\n\\nAll background workers are tied to cancellable contexts that are cancelled during shutdown.\\n\\n---\\n\\n## Mobile Browser Detection\\n\\nThe dashboard auto-redirects mobile browsers to the mobile PWA (`/m/`):\\n\\n```go\\nfunc isMobileBrowser(r *http.Request) bool {\\n    ua := strings.ToLower(r.Header.Get(\\\"User-Agent\\\"))\\n    mobileKeywords := []string{\\\"iphone\\\", \\\"ipad\\\", \\\"android\\\", \\\"mobile\\\", \\\"webos\\\", \\\"ipod\\\", \\\"blackberry\\\", \\\"windows phone\\\"}\\n    for _, kw := range mobileKeywords {\\n        if strings.Contains(ua, kw) {\\n            return true\\n        }\\n    }\\n    return false\\n}\\n```\\n\\nHTMX requests (`HX-Request: true`) bypass the redirect.\\n\\n---\\n\\n## QuickBooks Item Sync Tool\\n\\nThe `qb-push-items` tool is a one-off utility for linking local products to QB items:\\n\\n1. **Query QB** for existing items and extract the income account\\n2. **Load orphan products** (no `qb_item_id` in the DB)\\n3. **Create items in QB** for each orphan (if not already present by name)\\n4. **Update local products** with the QB item ID\\n\\nSafe to run multiple times\u2014it checks for existing items by name before creating.\\n\\n```bash\\ngo run ./cmd/qb-push-items/\\n```\\n\\n---\\n\\n## Key Design Patterns\\n\\n### Context Injection\\n\\nUser claims, tenant ID, and UI preferences are injected into the request context:\\n\\n```go\\nctx := auth.ContextWithClaims(r.Context(), claims)\\nctx = ui.ContextWithUserInfo(ctx, userInfo)\\nctx = loadUIPrefsIntoContext(ctx, pool, tenantID, userID)\\nr = r.WithContext(ctx)\\n```\\n\\nHandlers retrieve them with:\\n```go\\ntenantID := auth.TenantIDFromContext(r.Context())\\nuserID := auth.UserIDFromContext(r.Context())\\n```\\n\\n### Adapter Pattern\\n\\nIncompatible interfaces are bridged with adapter types:\\n- `sessionAdapter` \u2192 `helpdesk.SessionProvider`\\n- `rmmCommandAdapter` \u2192 `helpdesk.RMMCommander`\\n- `orchNotifyAdapter` \u2192 `helpdesk.OrchestratorNotifier`\\n- `emailSecAIAdapter` \u2192 `emailsec.AIEmailAnalyzer`\\n\\n### Callback Hooks\\n\\nModules emit events via callbacks wired in main:\\n- `crmHandler.OnDealStageChanged`\\n- `bidHandler.OnBidCreated`\\n- `assessmentHandler.OnAssessmentCompleted`\\n- `surveyHandler.OnSurveyCompleted`\\n\\nThese callbacks trigger orchestrator workflows and cross-module actions.\\n\\n### Background Worker Pattern\\n\\nLong-running tasks are started as goroutines with cancellable contexts:\\n\\n```go\\nctx, cancel := context.WithCancel(ctx)\\ndefer cancel()\\ngo worker.Start(ctx, interval)\\n```\\n\\nAll workers respect context cancellation for graceful shutdown.\",\"docs-architecture\":\"# docs \u2014 architecture\\n\\n# NexusOS Architecture Documentation\\n\\n## Overview\\n\\nNexusOS is a multi-tenant, modular PSA (Professional Services Automation) platform designed to provide enterprise-grade MSP tools at an affordable price point. The architecture is built around a **single binary serving all tenants**, with feature access controlled through a database-driven module licensing system.\\n\\nThe platform is organized into distinct functional domains (CRM, Helpdesk, Billing, RMM, Security, Compliance) that can be independently licensed and toggled per tenant. This document covers the architectural decisions, module structure, and key systems that enable this multi-tenant, modular approach.\\n\\n---\\n\\n## Core Architecture Principles\\n\\n### Single Binary, Multi-Tenant\\nNexusOS runs as one Go binary that serves all MSP tenants. Tenant isolation is enforced at the database layer through:\\n- Tenant ID in request context (extracted from JWT or session)\\n- Row-level filtering on all queries\\n- Module entitlements checked before route access\\n\\n### Module-First Design\\nEvery feature is a **module** with a unique key (e.g., `crm.clients`, `nexie_blue.remediation`). Modules can have dependencies on other modules, forming a directed acyclic graph. The licensing system controls which modules are available to each tenant.\\n\\n### Separation of Concerns\\n- **Core modules** (`core.auth`, `core.dashboard`, `core.settings`, `core.ai`) are always enabled\\n- **Feature modules** are grouped by domain (CRM, Helpdesk, Billing, etc.)\\n- **Nexie product line** modules (`nexie_siem.*`, `nexie_blue.*`, `nexie_red.*`) represent premium security offerings\\n- **Integration modules** handle third-party connections (QuickBooks, Teleport, etc.)\\n\\n---\\n\\n## Module Registry &amp; Licensing\\n\\n### Module Definitions\\n\\nThe platform defines 60+ modules across 11 categories. Each module has:\\n- **Key**: unique identifier (e.g., `helpdesk.tickets`)\\n- **Category**: logical grouping (e.g., \\\"Helpdesk\\\", \\\"Nexie Blue\\\")\\n- **Dependencies**: other modules that must be enabled first\\n- **Description**: human-readable purpose\\n\\nKey modules include:\\n\\n| Domain | Modules | Purpose |\\n|--------|---------|---------|\\n| **Core** | auth, dashboard, settings, ai | Platform foundation |\\n| **CRM** | clients, contacts, pipeline, campaigns, activities | Client relationship management |\\n| **Helpdesk** | tickets, kanban, time, itil, workflows, email, ai | Ticket management &amp; automation |\\n| **Billing** | quotes, invoices, contracts, quickbooks, nexie_quote_builder | Financial operations |\\n| **RMM** | agents, terminal, desktop, inventory, compliance | Remote monitoring &amp; management |\\n| **Security** | dashboard, alerts | Unified security posture |\\n| **Nexie SIEM** | collection, events, analysis, retention, sources | Threat detection &amp; log ingestion |\\n| **Nexie Blue** | remediation, approval, execution, posture, audit, reports, morning, bulletins | Automated defense &amp; response |\\n| **Nexie Red** | scanning, pentest, attack_surface, validation, simulation | Vulnerability testing |\\n\\n### Database Schema for Licensing\\n\\n```sql\\n-- Module definitions (platform-wide, immutable)\\nCREATE TABLE platform_modules (\\n    key         TEXT PRIMARY KEY,\\n    name        TEXT NOT NULL,\\n    category    TEXT NOT NULL,\\n    description TEXT,\\n    depends_on  TEXT[],\\n    sort_order  INT DEFAULT 0,\\n    is_core     BOOLEAN DEFAULT FALSE\\n);\\n\\n-- Licensing plans (bundles of modules)\\nCREATE TABLE platform_plans (\\n    id          UUID PRIMARY KEY,\\n    name        TEXT NOT NULL,           -- 'Founder', 'Starter', 'Professional', 'Enterprise'\\n    slug        TEXT UNIQUE NOT NULL,\\n    modules     TEXT[] NOT NULL,         -- wildcard patterns like 'crm.*', 'helpdesk.*'\\n    price_monthly DECIMAL(10,2),\\n    price_annual  DECIMAL(10,2),\\n    max_users   INT,\\n    max_agents  INT,\\n    is_active   BOOLEAN DEFAULT TRUE,\\n    created_at  TIMESTAMPTZ DEFAULT NOW()\\n);\\n\\n-- Tenant entitlements (what each tenant can access)\\nCREATE TABLE tenant_entitlements (\\n    id          UUID PRIMARY KEY,\\n    tenant_id   UUID NOT NULL REFERENCES tenants(id),\\n    plan_id     UUID REFERENCES platform_plans(id),\\n    module_overrides JSONB DEFAULT '{}',  -- per-module toggles\\n    is_founder  BOOLEAN DEFAULT FALSE,    -- first 10 get all modules\\n    trial_ends  TIMESTAMPTZ,\\n    status      TEXT DEFAULT 'active',\\n    created_at  TIMESTAMPTZ DEFAULT NOW(),\\n    updated_at  TIMESTAMPTZ DEFAULT NOW(),\\n    UNIQUE(tenant_id)\\n);\\n\\n-- Usage metering (for future usage-based billing)\\nCREATE TABLE tenant_usage (\\n    id          UUID PRIMARY KEY,\\n    tenant_id   UUID NOT NULL REFERENCES tenants(id),\\n    module_key  TEXT NOT NULL,\\n    metric      TEXT NOT NULL,           -- 'ai_tokens', 'agents', 'siem_events'\\n    value       BIGINT DEFAULT 0,\\n    period      DATE NOT NULL,\\n    UNIQUE(tenant_id, module_key, metric, period)\\n);\\n```\\n\\n### Module Gate Middleware\\n\\nRoutes are protected by a middleware that checks tenant entitlements before allowing access:\\n\\n```go\\nfunc (m *ModuleGate) Check(moduleKey string) func(http.Handler) http.Handler {\\n    return func(next http.Handler) http.Handler {\\n        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\\n            tenantID := auth.TenantIDFromContext(r.Context())\\n            if !m.HasAccess(tenantID, moduleKey) {\\n                http.Error(w, \\\"module not available on your plan\\\", http.StatusForbidden)\\n                return\\n            }\\n            next.ServeHTTP(w, r)\\n        })\\n    }\\n}\\n```\\n\\n### Dynamic UI Rendering\\n\\nThe sidebar and navigation are rendered conditionally based on tenant entitlements. Template data includes a map of enabled modules:\\n\\n```go\\ntype PageData struct {\\n    Modules map[string]bool  // {\\\"crm.clients\\\": true, \\\"nexie_red.scanning\\\": false}\\n}\\n```\\n\\n```html\\n{{if .Modules.nexie_blue_remediation}}\\nNexie Blue Team&lt;\\/a&gt;\\n{{end}}\\n```\\n\\n---\\n\\n## Master Console \u2014 Platform Administration\\n\\nThe Master Console is a separate administrative interface for the platform operator (you) to manage all tenants, plans, and billing from a single view. It is **not accessible to tenant users**.\\n\\n### Routes &amp; Features\\n\\n```\\n/master/                           Master Dashboard (all-tenant overview)\\n/master/tenants                    Tenant list &amp; management\\n/master/tenants/{id}               Tenant detail (config, users, usage)\\n/master/tenants/{id}/entitlements  Module licensing for this tenant\\n/master/tenants/{id}/billing       Tenant billing status &amp; invoices\\n/master/tenants/{id}/impersonate   Login as tenant admin (support access)\\n/master/plans                      Plan management (create/edit bundles)\\n/master/modules                    Module registry (enable/disable globally)\\n/master/billing                    Platform-wide billing overview\\n/master/billing/revenue            MRR/ARR dashboard across all tenants\\n/master/usage                      AI usage, agent counts, event volumes\\n/master/health                     System health (DB, services, queues)\\n/master/deployments                Binary version tracking per tenant\\n/master/security                   Platform-wide security posture\\n/master/bulletins                  Security bulletin management\\n/master/announcements              Push announcements to tenant dashboards\\n```\\n\\n### Master Authentication\\n\\nMaster Console access is restricted to platform admins with strict security controls:\\n\\n```sql\\nCREATE TABLE platform_admins (\\n    id          UUID PRIMARY KEY,\\n    email       TEXT UNIQUE NOT NULL,\\n    name        TEXT NOT NULL,\\n    password_hash TEXT NOT NULL,\\n    mfa_secret  TEXT,\\n    mfa_enabled BOOLEAN DEFAULT FALSE,\\n    is_active   BOOLEAN DEFAULT TRUE,\\n    last_login  TIMESTAMPTZ,\\n    created_at  TIMESTAMPTZ DEFAULT NOW()\\n);\\n\\nCREATE TABLE platform_audit_log (\\n    id          UUID PRIMARY KEY,\\n    admin_id    UUID REFERENCES platform_admins(id),\\n    action      TEXT NOT NULL,           -- 'tenant.create', 'entitlement.update'\\n    target_type TEXT,\\n    target_id   TEXT,\\n    details     JSONB,\\n    ip_address  INET,\\n    created_at  TIMESTAMPTZ DEFAULT NOW()\\n);\\n```\\n\\n**Security requirements:**\\n- MFA enforced (no exceptions)\\n- IP allowlist for console access\\n- Session timeout: 30 minutes maximum\\n- Audit trail on every action\\n- Impersonation requires explicit logging\\n\\n---\\n\\n## Nexie Product Line \u2014 Premium Security\\n\\nThe Nexie product line represents three complementary security offerings that work together in a continuous loop.\\n\\n### Nexie SIEM \u2014 \\\"Your Eyes\\\"\\n\\n**Purpose:** Centralized threat detection and log analysis.\\n\\n**Capabilities:**\\n- Syslog collection from any device (FortiGate, Windows, Linux, etc.)\\n- PCAP deep packet inspection\\n- AI-powered threat detection and analysis\\n- Real-time event feed with filtering and statistics\\n- Configurable log retention policies\\n- Multi-session concurrent capture\\n\\n**Module keys:** `nexie_siem.collection`, `nexie_siem.events`, `nexie_siem.analysis`, `nexie_siem.retention`, `nexie_siem.sources`\\n\\n**Key components:**\\n- `internal/siem/handler.go` \u2014 HTTP routes for capture, analysis, session management\\n- `internal/siem/syslog.go` \u2014 BSD syslog parser (RFC 3164), TLS auto-detection\\n- Port 6514 TLS multiplexer \u2014 peeks first byte to detect TLS ClientHello vs plain syslog\\n- Per-session event routing via `sourceMap` and `RegisterSession`/`UnregisterSession`\\n- UTF-8 sanitization prevents PostgreSQL encoding errors from binary data\\n\\n### Nexie Blue Team \u2014 \\\"Your Shield\\\"\\n\\n**Purpose:** Automated threat response and remediation.\\n\\n**Capabilities:**\\n- AI-generated remediation plans from SIEM findings\\n- One-click approval workflow for remediation actions\\n- Automated execution on FortiGate and other devices (block IPs, create policies, enable IPS)\\n- Post-remediation posture verification\\n- Full audit trail with evidence chain\\n- \\\"Morning with Nexie\\\" overnight findings dashboard\\n- Security bulletin curation (CISA, NIST NVD, US-CERT)\\n- Client-facing security reports (PDF/CSV)\\n\\n**Module keys:** `nexie_blue.remediation`, `nexie_blue.approval`, `nexie_blue.execution`, `nexie_blue.posture`, `nexie_blue.audit`, `nexie_blue.reports`, `nexie_blue.morning`, `nexie_blue.bulletins`\\n\\n**Execution actions (FortiGate):**\\n- `block_ip` \u2014 create address object + deny policy\\n- `create_deny_policy` \u2014 add policy to block traffic\\n- `enable_ips_signature` \u2014 enable IPS sensor for threat detection\\n\\n### Nexie Red Team \u2014 \\\"Your Adversary\\\"\\n\\n**Purpose:** Proactive vulnerability testing and validation.\\n\\n**Capabilities:**\\n- Vulnerability scanning (Metasploit integration)\\n- Automated penetration testing\\n- Attack surface discovery\\n- Post-remediation validation (\\\"did the fix actually work?\\\")\\n- Adversary simulation mapped to MITRE ATT&amp;CK\\n- Findings feed directly into Blue Team remediation queue\\n\\n**Module keys:** `nexie_red.scanning`, `nexie_red.pentest`, `nexie_red.attack_surface`, `nexie_red.validation`, `nexie_red.simulation`\\n\\n### The Nexie Loop\\n\\nThe three products work together in a continuous cycle:\\n\\n```\\nSIEM (Detect) \u2192 Blue Team (Defend) \u2192 Red Team (Validate) \u2192 SIEM (Detect)\\n     \u2191                                                          \u2502\\n     \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n```\\n\\nThis loop represents the core value proposition: continuous, automated security improvement.\\n\\n---\\n\\n## Security Bulletin System\\n\\n### Purpose\\n\\nCurate relevant security advisories from multiple sources and present them alongside overnight scan findings in the \\\"Morning with Nexie\\\" dashboard.\\n\\n### Data Sources\\n\\n- **CISA KEV** \u2014 Known Exploited Vulnerabilities (api.cisa.gov)\\n- **NIST NVD** \u2014 National Vulnerability Database (services.nvd.nist.gov)\\n- **US-CERT Alerts** \u2014 us-cert.cisa.gov RSS\\n- **The Hacker News** \u2014 Security news RSS\\n- **BleepingComputer** \u2014 Security news RSS\\n- **Krebs on Security** \u2014 Security news RSS\\n- **Fortinet PSIRT** \u2014 FortiGate-specific advisories (fortiguard.com)\\n\\n### Processing Pipeline\\n\\n1. **Fetch** \u2014 Background job pulls feeds every 4 hours\\n2. **Curate** \u2014 Nexie AI filters by relevance to tenant's device inventory (FortiGate, Windows endpoints, etc.)\\n3. **Summarize** \u2014 AI generates 2\u20133 sentence technical brief\\n4. **Classify** \u2014 Assign severity (Critical/High/Medium/Low) and affected product tags\\n5. **Display** \u2014 Show in \\\"Morning with Nexie\\\" dashboard alongside overnight findings\\n6. **Link** \u2014 Provide link to original source for full details\\n\\n### Database Schema\\n\\n```sql\\nCREATE TABLE security_bulletins (\\n    id          UUID PRIMARY KEY,\\n    source      TEXT NOT NULL,           -- 'cisa_kev', 'nist_nvd', 'hacker_news'\\n    external_id TEXT,                     -- CVE-2026-XXXX\\n    title       TEXT NOT NULL,\\n    summary     TEXT,                     -- AI-curated summary\\n    severity    TEXT,                     -- critical, high, medium, low\\n    affected_products TEXT[],             -- '{fortigate,windows_server,exchange}'\\n    url         TEXT,\\n    published_at TIMESTAMPTZ,\\n    fetched_at  TIMESTAMPTZ DEFAULT NOW(),\\n    UNIQUE(source, external_id)\\n);\\n\\nCREATE TABLE tenant_bulletin_relevance (\\n    id          UUID PRIMARY KEY,\\n    tenant_id   UUID REFERENCES tenants(id),\\n    bulletin_id UUID REFERENCES security_bulletins(id),\\n    relevance_score INT,                  -- 0-100 based on device inventory match\\n    is_read     BOOLEAN DEFAULT FALSE,\\n    is_actioned BOOLEAN DEFAULT FALSE,\\n    created_at  TIMESTAMPTZ DEFAULT NOW()\\n);\\n```\\n\\n---\\n\\n## Pricing Tiers\\n\\n### Founder Plan (First 10 MSPs)\\n- **All modules unlocked forever** (grandfather clause)\\n- Unlimited users, unlimited agents\\n- Locked-in discounted rate\\n- Early adoption partnership\\n\\n### Starter Plan\\n- Core + CRM + Helpdesk (basic) + Products\\n- Up to 5 users, 50 agents\\n- No AI features, no security modules\\n\\n### Professional Plan\\n- Everything in Starter + Billing + Contracts + KB + RMM + Projects\\n- Up to 15 users, 250 agents\\n- Basic AI (ticket suggestions, KB generation)\\n\\n### Enterprise Plan\\n- Everything in Professional + Compliance + Nexie SIEM + Nexie Blue + Nexie Red\\n- Unlimited users, unlimited agents\\n- Full AI features, security bulletins, audit compliance\\n\\n### Add-Ons\\n- Nexie SIEM \u2014 $X/month\\n- Nexie Blue Team \u2014 $X/month\\n- Nexie Red Team \u2014 $X/month\\n- QuickBooks Integration \u2014 $X/month\\n- Additional AI tokens \u2014 $X per 1M tokens\\n\\n---\\n\\n## TruMethods Revenue Framework\\n\\nNexusOS follows the **TruMethods framework** for MSP business operations and revenue modeling.\\n\\n### Revenue Classification\\n\\nRevenue is classified into three categories (independent of billing cadence):\\n\\n- **MRR** \u2014 Monthly Recurring Revenue: core managed services revenue that repeats monthly (foundation of MSP health)\\n- **ORR** \u2014 Other Recurring Revenue: recurring but non-core (resold SaaS/M365, hosted services, telecom, backup subscriptions)\\n- **NRR** \u2014 Non-Recurring Revenue: one-time charges (projects, break-fix labor, hardware sales, onboarding fees)\\n\\n### Billing Cadence\\n\\nHow often a charge is billed: **Monthly**, **Quarterly**, **Yearly**, or **One-Time**. This is independent of revenue classification.\\n\\nExample: An M365 license resale is **ORR** billed **Monthly**; a hardware sale is **NRR** billed **One-Time**.\\n\\n### Implementation in NexusOS\\n\\n- Products have a `revenue_type` field (MRR/ORR/NRR)\\n- Contract line items inherit revenue type from product (user-overridable)\\n- Dashboards and reports break down revenue by MRR vs ORR vs NRR\\n- MRR is the primary health metric tracked across the platform\\n\\n---\\n\\n## Current Implementation Status\\n\\n### Fully Built &amp; Deployed\\n- Authentication (JWT, Sessions, SSO stub)\\n- Dashboard &amp; Settings\\n- CRM (Clients, Contacts, Pipeline, Campaigns, Activities)\\n- Helpdesk (Tickets, Kanban, Time, ITIL, Workflows, Email, AI)\\n- Billing (Quotes, Invoices, Contracts, QuickBooks, Nexie Quote Builder)\\n- Legal/Contracts (E-Sign, ORDER/MSA auto-generation, Contract Summary)\\n- Products (Catalog, Assignments, Legal Doc Associations)\\n- Projects (Projects, Milestones, Tasks, Board)\\n- Knowledge Base (Articles, Search, AI Generation)\\n- RMM (Agents, Heartbeat, Commands, Terminal, Desktop, Inventory, mTLS)\\n- Compliance (14 frameworks, Gap Analysis, Device Evidence, Documents)\\n- Security Dashboard (Unified posture, alerts)\\n- Metasploit/Vulnerability Scanning\\n- SIEM (Syslog, PCAP, AI Analysis, Multi-session capture)\\n- Devices (Credential Vault, FortiGate API Client)\\n- Certificates (CA Generation, CSR Signing)\\n- Teleport (Session Recording, Sync)\\n- QuickBooks (OAuth, Bidirectional Sync)\\n- AI Provider (Claude API, Usage Tracking)\\n- Tenant/Settings (Users, Integrations, MCP, Email)\\n\\n### Proven on Live Hardware\\n- FortiGate 100E v7.2.13: block_ip, create_address_object, create_deny_policy, enable_ips_signature \u2014 all working\\n- Syslog ingestion from FortiGate over TLS on port 6514\\n- PCAP analysis with Nexie AI detecting real threats\\n- Real remediation executed: address objects and deny policies created on live firewall\\n\\n### Not Yet Built\\n- Master Console (full implementation)\\n- Module licensing/entitlements system (database schema exists, middleware not integrated)\\n- Nexie Blue Team (as separate module \u2014 code exists in SIEM, needs extraction)\\n- Nexie Red Team (Metasploit exists, needs branding + validation loop)\\n- Security bulletin curation (data sources defined, fetcher not implemented)\\n- \\\"Morning with Nexie\\\" dashboard\\n- Client-facing PDF/CSV security reports\\n- Hudu sync\\n- Full SSO (SAML 2.0 / OIDC)\\n- EVA.AI (bill collection agent)\\n- Voice-to-Quote (DeepGram STT)\\n\\n---\\n\\n## Key Design Decisions\\n\\n### Why Single Binary?\\n- Simpler deployment and updates\\n- Consistent behavior across all tenants\\n- Easier to debug and monitor\\n- Tenant isolation enforced at database layer, not process layer\\n\\n### Why Module-Based Licensing?\\n- Granular control over feature access per tenant\\n- Enables flexible pricing tiers\\n- Supports founder/early-adopter programs\\n- Allows gradual feature rollout and A/B testing\\n\\n### Why Separate Master Console?\\n- Clear separation between tenant and operator concerns\\n- Stricter security controls for administrative access\\n- Audit trail for all platform-level changes\\n- Prevents accidental tenant data exposure\\n\\n### Why Nexie Product Line?\\n- Differentiates premium security offerings\\n- Creates a continuous improvement loop (detect \u2192 defend \u2192 validate)\\n- Aligns with MSP security maturity journey\\n- Enables upsell path from basic to enterprise\\n\\n---\\n\\n## Integration Points\\n\\n### With RMM Module\\n- SIEM collects logs from RMM agents\\n- Blue Team executes remediation on RMM-managed devices\\n- Red Team validates fixes on RMM-managed endpoints\\n\\n### With CRM Module\\n- Client context in SIEM analyses\\n- Security findings linked to client companies\\n- Compliance evidence tied to client assets\\n\\n### With Billing Module\\n- AI token usage tracked for cost allocation\\n- Security services billed separately from core MSP services\\n- Compliance audit reports for client invoicing\\n\\n### With Helpdesk Module\\n- SIEM findings auto-create tickets\\n- Blue Team remediation plans linked to tickets\\n- KB articles generated from security findings\\n\\n---\\n\\n## Deployment &amp; Operations\\n\\n### Build &amp; Deploy\\n```bash\\ncd /Users/tempest/Documents/Claude.Code/psa\\nCGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o nexusos-psa ./cmd/psa/\\nscp nexusos-psa root@96.76.137.181:/tmp/nexusos-psa\\nssh root@96.76.137.181 'pct exec 715 -- systemctl stop nexusos-psa &amp;&amp; \\\\\\n  pct push 715 /tmp/nexusos-psa /opt/nexusos/psa &amp;&amp; \\\\\\n  pct exec 715 -- chmod +x /opt/nexusos/psa &amp;&amp; \\\\\\n  pct exec 715 -- systemctl start nexusos-psa'\\n```\\n\\n**Critical:** Binary must go to `/opt/nexusos/psa` \u2014 that's the path in the systemd service.\\n\\n### Server Details\\n- Proxmox host: `96.76.137.181`\\n- Container ID: `715`\\n- Container IP: `10.100.100.231`\\n- Service name: `nexusos-psa`\\n\\n---\\n\\n## Next Steps\\n\\n### High Priority\\n1. **Module licensing integration** \u2014 Wire ModuleGate middleware into all routes\\n2. **Master Console implementation** \u2014 Build tenant management UI\\n3. **Nexie Blue extraction** \u2014 Move remediation code from SIEM to separate module\\n4. **Security bulletin fetcher** \u2014 Implement feed polling and curation\\n\\n### Medium Priority\\n1. **\\\"Morning with Nexie\\\" dashboard** \u2014 Overnight findings + bulletin display\\n2. **Client-facing reports** \u2014 PDF/CSV generation for security findings\\n3. **Hudu sync** \u2014 Documentation integration\\n4. **Full SSO** \u2014 SAML 2.0 / OIDC support\\n\\n### Lower Priority\\n1. **EVA.AI** \u2014 Autonomous bill collection\\n2. **Voice-to-Quote** \u2014 DeepGram STT integration\\n3. **TruMethods revenue classification** \u2014 Full dashboard implementation\",\"docs-docs\":\"# docs \u2014 docs\\n\\n# Documentation Module\\n\\nThe **docs** module is a collection of design documents, guides, implementation plans, and reference materials that capture the architecture, procedures, and decision history of NexusOS PSA. It is not executable code but rather the **source of truth** for how the system is organized and how to work within it.\\n\\n## Purpose\\n\\nThe docs module serves three audiences:\\n\\n1. **New contributors** \u2014 understand the system shape, conventions, and where to find answers\\n2. **Operators** \u2014 deploy, troubleshoot, and maintain the running system\\n3. **Decision makers** \u2014 review architectural choices, compliance posture, and roadmap items\\n\\nEvery document in this module is referenced from code comments, Claude skills, and runbooks. When a document becomes stale, it is updated immediately \u2014 stale docs are worse than no docs.\\n\\n## Core Documents\\n\\n### Architecture &amp; System Design\\n\\n**`ARCHITECTURE.md`** \u2014 The entry point for understanding NexusOS PSA. Read this first.\\n\\n- One-paragraph summary of the entire system\\n- What's deployed where (VMs, processes, databases)\\n- Request flow through middleware and routing\\n- Multi-tenancy model (three-layer isolation)\\n- Module organization (six functional clusters)\\n- Data layer (pgx/v5, raw SQL, migrations)\\n- UI stack (Go templates, HTMX, Alpine, nexus.css)\\n- AI integration (Claude API wrapper)\\n- Cross-cutting concerns (CSRF, RBAC, audit logging, rate limiting)\\n- External integrations (RMM, SIEM, FortiGate, M365, QuickBooks, Metasploit, Hudu, Teleport)\\n- Background work (contract sync, SIEM listeners, RMM listener, email pipeline)\\n- Index of source-of-truth references for deeper dives\\n\\n**`INFRASTRUCTURE.md`** \u2014 Deployment topology, host inventory, SSH paths, and operational procedures. (Referenced but not included in this context.)\\n\\n### API &amp; Routes\\n\\n**`API_REFERENCE.md`** \u2014 Comprehensive catalog of all 487+ HTTP endpoints, organized by functional area.\\n\\n- Authentication (13 endpoints)\\n- System &amp; Settings (33 endpoints)\\n- Helpdesk (Tickets, ITIL, Contracts, Email, Workflows, SLA)\\n- CRM (28 endpoints)\\n- Legal &amp; Contracts (20 endpoints)\\n- Quotes (21 endpoints)\\n- Invoices (14 endpoints)\\n- Projects (83 endpoints)\\n- RMM (Dashboard, Extended, Agent API, Enrollment)\\n- SIEM (23 endpoints)\\n- Security Dashboard, Metasploit, Health, Compliance, Knowledge Base, VoIP, Certificates, Devices, Nexie, Hudu, Products, Teleport, Mobile, Procurement, Notifications\\n\\nEach endpoint entry includes HTTP method, path, and one-line description. Notes section clarifies HTMX behavior, tenant isolation, mTLS, token-based public routes, and WebSocket upgrades.\\n\\n### Module Inventory\\n\\n**`INVENTORY.md`** \u2014 Per-package description of every module under `internal/`. (Referenced but not included in this context.)\\n\\nFor each package:\\n- What it does\\n- Key types and functions\\n- Database tables it owns\\n- Routes it registers\\n- External integrations\\n- Known limitations or TODOs\\n\\n### Audit &amp; Compliance\\n\\n**`AUDIT_CRM_TENANT_SCOPE.md`** \u2014 Detailed audit of CRM handler tenant isolation.\\n\\n- Method: scanned 95 SQL callsites in `internal/crm/`\\n- Five findings (all fixed in PR #6e):\\n  - Campaign recipients load (fragile read, fixed by migration 236)\\n  - Recipient count enumeration (fixed by migration 236)\\n  - Recipient UPDATE cross-tenant write (fixed by RowsAffected check)\\n  - Company tag INSERT without tenant verification (fixed by pre-check helper)\\n  - Tag DELETE without tenant verification (fixed by pre-check helper)\\n- Architectural note on `ScopedDB` helper (deferred as future work)\\n- Two helper functions introduced: `requireTenantOwnsCompany`, `requireTenantOwnsTag`\\n\\n### Implementation Plans &amp; Test Guides\\n\\n**`BID_ENGINE_TEST_GUIDE.md`** \u2014 End-to-end test procedure for the Tier 1 + Pre-Submission Audit work in `feat/bid-engine-scope-accuracy`.\\n\\n- Prerequisites and setup\\n- Compilation and unit tests\\n- Migration application and verification\\n- OFCI extractor unit tests\\n- Smoke test on real bid (local)\\n- Manual scope-flag toggle and recalculation\\n- End-to-end regeneration of BID-21 (the headline test)\\n- Tenant-specific trade profile override\\n- Production smoke test (read-only)\\n- Deploy checklist\\n- Rollback procedure\\n- Key files touched and new endpoints\\n\\n**`CMMC_ONE_CLICK_COMPLIANCE_PLAN.md`** \u2014 Implementation plan for one-click CMMC Level 2 compliance activation.\\n\\n- Executive summary\\n- Architecture overview (three-layer system)\\n- Part 1: Infrastructure Verification Engine (`internal/infra/`)\\n  - Database schema (migrations 176+)\\n  - Go package structure\\n  - HTTP routes\\n  - Verification protocols (HTTPS, SSH, SNMP, LDAP, WinRM, HTTP, API)\\n  - Hudu integration\\n  - Runbook parsing\\n  - Audit trail\\n- Part 2: CMMC Compliance Module (`internal/cmmc/`)\\n- Part 3: Onboarding Integration\\n- Deployment checklist\\n- Rollback procedure\\n\\n**`BASE_HTML_MIGRATION_PLAN.md`** \u2014 Plan to remove Tailwind CSS dependency from `base.html` and consolidate all styling into `nexus.css`.\\n\\n- Why this file matters (3,517 lines, main-app shell)\\n- File map (regions and line ranges)\\n- Target nexus.css extensions (new sections for shell, sidebar, topbar, Nexie, glass variants)\\n- Risk register (six risks with mitigations)\\n- Execution sequence (six phases, ~3\u20134 hours)\\n- Success criteria\\n- Explicit non-goals\\n\\n### Meeting Notes &amp; Decisions\\n\\n**`2026-04-07-Meeting-Email-Security-Flow.md`** \u2014 Meeting notes from 2026-04-07 on email security architecture.\\n\\n- Current flow: Microsoft Exchange \u2192 Defender \u2192 Inky \u2192 Mailboxes\\n- Proposed change: split handling by risk tags (no caution vs. warning/critical)\\n- Targeted secondary analysis for warning/caution emails (header-based, not body)\\n- Client and vendor profiles in CRM for contextual decisions\\n- Whitelisting strategy and risks\\n- Local LLM for email triage (single-purpose, cost-controlled)\\n- Header/data validation vs. content-based heuristics\\n- Vendor onboarding: capture known-good email headers\\n- Cost model for Claude API usage\\n- Next arrangements (8 action items)\\n- AI suggestions (5 unresolved issues)\\n\\n## How Documents Are Organized\\n\\n```\\ndocs/\\n\u251c\u2500\u2500 ARCHITECTURE.md                          \u2190 Start here\\n\u251c\u2500\u2500 API_REFERENCE.md                         \u2190 Route catalog\\n\u251c\u2500\u2500 INVENTORY.md                             \u2190 Per-module details\\n\u251c\u2500\u2500 INFRASTRUCTURE.md                        \u2190 Deployment topology\\n\u251c\u2500\u2500 AUDIT_CRM_TENANT_SCOPE.md               \u2190 Tenant isolation audit\\n\u251c\u2500\u2500 BID_ENGINE_TEST_GUIDE.md                \u2190 Test procedure\\n\u251c\u2500\u2500 CMMC_ONE_CLICK_COMPLIANCE_PLAN.md       \u2190 Implementation plan\\n\u251c\u2500\u2500 BASE_HTML_MIGRATION_PLAN.md             \u2190 UI migration plan\\n\u251c\u2500\u2500 2026-04-07-Meeting-Email-Security-Flow.md \u2190 Decision record\\n\u251c\u2500\u2500 security/                                \u2190 Threat models, controls, evidence\\n\u251c\u2500\u2500 CODEBASE_OBSERVATIONS.md                \u2190 Known oddities, dead code\\n\u251c\u2500\u2500 DEPENDENCIES.md                         \u2190 Internal import graph\\n\u251c\u2500\u2500 DEPENDENCIES-EXTERNAL.md                \u2190 Third-party deps\\n\u251c\u2500\u2500 ONBOARDING.md                           \u2190 Setup walkthrough\\n\u2514\u2500\u2500 CHANGELOG.md                            \u2190 Release history\\n```\\n\\n## Key Conventions\\n\\n### Document Lifecycle\\n\\n1. **Created** \u2014 When a decision is made or a feature is planned, a document is written immediately (not retroactively).\\n2. **Maintained** \u2014 When code changes, the corresponding doc is updated in the same PR.\\n3. **Archived** \u2014 Obsolete docs are moved to a `docs/archive/` folder with a date stamp, never deleted.\\n4. **Linked** \u2014 Every doc is referenced from at least one other doc or from code comments.\\n\\n### Audience Signals\\n\\nDocuments use these markers to signal who should read them:\\n\\n- **\\\"Read this first\\\"** \u2014 Entry point for new contributors\\n- **\\\"Source of truth\\\"** \u2014 Canonical reference for a topic\\n- **\\\"Deferred\\\"** \u2014 Planned but not yet implemented\\n- **\\\"Deprecated\\\"** \u2014 Superseded by another document\\n\\n### Accuracy Standards\\n\\n- **Factual claims** are backed by code references or test results\\n- **Architectural diagrams** are kept small (5\u201310 nodes) and updated when the system changes\\n- **Procedures** include success criteria and rollback steps\\n- **Decisions** record the rationale, not just the outcome\\n\\n## Connection to Code\\n\\n### Claude Skills\\n\\nThe `.claude/skills/` directory contains reusable rule sets that reference docs:\\n\\n- **`psa-conventions`** \u2014 References `ARCHITECTURE.md` for module layout, `INVENTORY.md` for per-package details\\n- **`psa-code-review`** \u2014 References `AUDIT_CRM_TENANT_SCOPE.md` for tenant isolation patterns\\n- **`psa-deploy`** \u2014 References `INFRASTRUCTURE.md` for deployment procedures\\n- **`psa-dbadmin`** \u2014 References `internal/database/schema.sql` as authoritative schema, `docs/DEPENDENCIES.md` for migration ordering\\n\\n### Code Comments\\n\\nHandlers and critical functions include comments like:\\n\\n```go\\n// See docs/ARCHITECTURE.md \u00a7 Multi-tenancy model for the three-layer isolation pattern.\\ntenantID := auth.TenantIDFromContext(r.Context())\\n```\\n\\n### Runbooks\\n\\nOperational procedures (deploy, rollback, incident response) are documented in `docs/` and referenced from CI/CD pipelines and on-call playbooks.\\n\\n## What This Module Is NOT\\n\\n- **Not a tutorial** \u2014 See `ONBOARDING.md` for setup instructions\\n- **Not a complete API reference** \u2014 See `API_REFERENCE.md` for routes\\n- **Not a schema reference** \u2014 See `internal/database/schema.sql` for the authoritative schema\\n- **Not a rule book** \u2014 See `CLAUDE.md` and `.claude/skills/` for discipline and conventions\\n- **Not a changelog** \u2014 See `CHANGELOG.md` for release history\\n\\n## Maintenance\\n\\n### When to Update Docs\\n\\n- **After every PR that changes architecture** \u2014 Update `ARCHITECTURE.md` or the relevant module doc\\n- **After every new endpoint** \u2014 Add it to `API_REFERENCE.md`\\n- **After every migration** \u2014 Update `INVENTORY.md` if the module's tables changed\\n- **After every decision** \u2014 Create a decision record (like the 2026-04-07 meeting notes)\\n- **After every audit** \u2014 Document findings and fixes (like `AUDIT_CRM_TENANT_SCOPE.md`)\\n\\n### Who Maintains Docs\\n\\n- **Architects** \u2014 `ARCHITECTURE.md`, `INFRASTRUCTURE.md`, implementation plans\\n- **Module owners** \u2014 `INVENTORY.md` entries for their packages\\n- **Security team** \u2014 `docs/security/` threat models and controls\\n- **Operators** \u2014 `INFRASTRUCTURE.md`, runbooks, incident playbooks\\n- **Everyone** \u2014 Update docs in the same PR as code changes\\n\\n## Quick Reference\\n\\n| Need | Document |\\n|---|---|\\n| Understand the system shape | `ARCHITECTURE.md` |\\n| Find an endpoint | `API_REFERENCE.md` |\\n| Understand a module | `INVENTORY.md` |\\n| Deploy to production | `INFRASTRUCTURE.md` + `CLAUDE.md` |\\n| Verify tenant isolation | `AUDIT_CRM_TENANT_SCOPE.md` |\\n| Test a feature | `BID_ENGINE_TEST_GUIDE.md` or similar |\\n| Plan a feature | `CMMC_ONE_CLICK_COMPLIANCE_PLAN.md` or similar |\\n| Understand a decision | Meeting notes or decision records in `docs/` |\\n| Set up locally | `ONBOARDING.md` |\\n| See what changed | `CHANGELOG.md` |\",\"docs-guide\":\"# docs \u2014 guide\\n\\n# Docs \u2014 Guide Module\\n\\n## Overview\\n\\nThe **docs/guide** module is a collection of feature documentation, testing guides, and user manuals for the NexusOS PSA platform. It serves as the canonical reference for developers, QA engineers, and end users implementing or using specific features.\\n\\nEach guide is a standalone Markdown document focused on a single feature area or workflow. Guides are living documents \u2014 they evolve as features ship and change.\\n\\n---\\n\\n## Module Contents\\n\\n### User &amp; Feature Guides\\n\\n**[NCSR_USER_GUIDE.md](NCSR_USER_GUIDE.md)** \u2014 Nationwide Cybersecurity Review intake and action planning\\n- NCSR survey upload (`.xlsm` parser)\\n- Enhanced Recommendations PDF parsing\\n- Maturity dashboard and heatmap visualization\\n- Action plan generation and client-ready reporting\\n- Pricing and MSP productization\\n\\n**[RISK_ACCEPTANCE_USER_GUIDE.md](RISK_ACCEPTANCE_USER_GUIDE.md)** \u2014 Shared risk acceptance service\\n- Cross-module risk register (CMMC, NCSR, Composer, future Vuln Mgmt)\\n- Client portal signing workflow with signature capture\\n- Lifecycle states (pending \u2192 accepted \u2192 under_review \u2192 expired/revoked/resolved)\\n- Renewal and revocation workflows\\n- Audit trail and polymorphic source linking\\n\\n**[billing-contracts.md](billing-contracts.md)** \u2014 Contract lifecycle and mixed-cadence billing\\n- Contract status flow (draft \u2192 pending_signature \u2192 active \u2192 expired)\\n- Multi-document e-signature collection with celebration animation\\n- Mixed-cadence billing (monthly, quarterly, yearly, one-time in one contract)\\n- Per-line cadence override with color-coded indicators\\n- Sync sources for auto-updating quantities (RMM agents, M365 licenses, product assignments)\\n- Contract-to-invoice generation with cadence-aware line item scheduling\\n\\n**[dispatch.md](dispatch.md)** \u2014 Unified dispatch board for tickets and work orders\\n- Four views: Calendar (weekly grid), Board (kanban by tech), List (filterable), Map (geographic)\\n- MS365 calendar sync (auto-creates Outlook events on dispatch)\\n- AI Suggest engine (Claude-powered tech matching by skill, workload, location)\\n- Tech skill profiles and weekly availability templates\\n- Slot status lifecycle (Scheduled \u2192 Dispatched \u2192 En Route \u2192 On Site \u2192 Complete)\\n\\n**[employee-hr.md](employee-hr.md)** \u2014 Employee profiles, skills, rates, and availability\\n- Six-tab employee detail: Profile, Skills &amp; Certifications, Pay Rates, Role Rates, Availability, Time Off\\n- Skill types: Trade, IT Category, Certification with proficiency levels\\n- Pay rates (what you pay) vs. Role rates (what you bill/cost)\\n- Weekly availability templates with max slot limits\\n- Cost flow: role rates \u2192 time entries \u2192 ticket/WO costing \u2192 invoicing\\n\\n**[projects.md](projects.md)** \u2014 PMP-aligned project management with full job costing\\n- Five PMP process groups (Initiating, Planning, Executing, Monitoring &amp; Controlling, Closing)\\n- Work Breakdown Structure (WBS) with hierarchical task codes and dependencies\\n- Four dependency types (FS, FF, SS, SF) with lag/lead support\\n- Critical path computation\\n- Job costing: labor, materials, subcontractor, travel, overhead, other\\n- Earned Value Management (EVM) with 11 PMI-standard metrics and S-curve snapshots\\n- Change control, risk register, stakeholder management, lessons learned\\n\\n### Testing &amp; QA Guides\\n\\n**[MULTI_REVIEWER_TEST_GUIDE.md](MULTI_REVIEWER_TEST_GUIDE.md)** \u2014 End-to-end test plan for Live Review v2\\n- Multi-reviewer compliance review (split by NIST CSF domain)\\n- Setup screen with per-domain reviewer assignment\\n- Per-reviewer URL routing and item filtering\\n- Host monitoring matrix with reviewer status tracking\\n- Multi-signature binder generation with per-reviewer blocks\\n- Regression testing for v1 single-participant flow\\n- Schema cheat-sheet and diagnostic queries\\n\\n**[PHASE3_PROCUREMENT_TESTING_GUIDE.md](PHASE3_PROCUREMENT_TESTING_GUIDE.md)** \u2014 Comprehensive Phase 3 distributor procurement test plan\\n- Migration suite (212\u2013217): fresh DB, legacy data, orphan tenants, idempotency keys\\n- Auto-approval patterns: threshold-based and agreement-driven (SaaS)\\n- Dispatch + idempotency: caller-mint key format, transport retry, operator resend\\n- Remediation queue: auto-open on failure, triage, resolution paths (retry, abandon, manual external)\\n- Settings UI: auto-approve threshold, SaaS provisioning toggles, per-vendor auto-dispatch\\n- UI surfaces: Ticket Related tab (v2), Project Costs, Contract Line Items, `/procurement` list &amp; detail, Agreement SaaS panel\\n- Seven end-to-end scenarios: materials happy path, manual approval, SaaS renewal, gated new agreement, failure \u2192 remediation \u2192 success, failure \u2192 abandon, new-distributor onboarding\\n- Sign-off matrix mapping test areas to PR coverage\\n\\n### Index &amp; Navigation\\n\\n**[README.md](README.md)** \u2014 Module index and guide structure\\n- Lists all guides by category (Core, CRM, Helpdesk, Billing &amp; Finance, Security &amp; Compliance, RMM, Projects)\\n- Marks guides as *(planned)* or linked\\n- Explains the standard structure (Overview, Key Concepts, Workflows, Configuration, Technical Notes)\\n\\n---\\n\\n## How Guides Are Structured\\n\\nEach guide follows a consistent pattern (though not rigidly \u2014 structure adapts to the feature):\\n\\n1. **Overview / At a Glance** \u2014 What the feature does, who uses it, why it matters\\n2. **Key Concepts** \u2014 Terminology, data model, state machines, relationships\\n3. **Workflows** \u2014 Step-by-step user journeys with screenshots/mockups referenced\\n4. **Configuration** \u2014 Settings, toggles, customization points\\n5. **Technical Notes** \u2014 Database tables, API endpoints, integration details\\n6. **Troubleshooting** \u2014 Common issues and fixes\\n7. **Reference** \u2014 Links to source code, migrations, PRs, release notes\\n\\nTesting guides follow a different pattern:\\n- **Preconditions** \u2014 Database state, tenant settings, test data\\n- **Steps** \u2014 Numbered click-paths or API calls\\n- **Expected** \u2014 Assertions and success criteria\\n- **Edge Cases** \u2014 Variations and failure modes\\n- **Sign-off Matrix** \u2014 Coverage mapping for PR review\\n\\n---\\n\\n## Key Design Principles\\n\\n### Single Source of Truth\\nEach feature has **one canonical guide**. If a feature appears in multiple guides, the primary guide is linked from the others. This prevents documentation drift.\\n\\n### Living Documents\\nGuides are versioned alongside code. When a PR ships a feature, the guide is updated in the same PR. When a bug is fixed or behavior changes, the guide is updated immediately.\\n\\n### Developer-Focused\\nGuides assume the reader is a developer, QA engineer, or technical MSP user. They include:\\n- Actual function/table/endpoint names (not invented APIs)\\n- SQL queries for verification\\n- Database schema snippets\\n- Call graphs and execution flows where relevant\\n- PR references for context\\n\\n### Mermaid Diagrams Only When Helpful\\nDiagrams are included only when they clarify architecture or flow. Simple text tables and code blocks are preferred for most content.\\n\\n### Test-First Documentation\\nTesting guides are written **before** features ship. They serve as acceptance criteria and become the regression test suite.\\n\\n---\\n\\n## Relationship to Codebase\\n\\nGuides document features that exist in the codebase. The mapping is:\\n\\n| Guide | Primary Code Paths |\\n|-------|-------------------|\\n| NCSR_USER_GUIDE | `internal/composer/ncsr/` |\\n| RISK_ACCEPTANCE_USER_GUIDE | `internal/risk/` + resolvers in `internal/{cmmc,composer,ncsr}/` |\\n| billing-contracts | `internal/legal/` + `internal/ui/templates/legal/` |\\n| dispatch | `internal/dispatch/` + `internal/ui/templates/dispatch/` |\\n| employee-hr | `internal/hr/` + `internal/ui/templates/hr/` |\\n| projects | `internal/projects/` + `internal/ui/templates/projects/` |\\n| MULTI_REVIEWER_TEST_GUIDE | `internal/reviews/` (Live Review v2 session logic) |\\n| PHASE3_PROCUREMENT_TESTING_GUIDE | `internal/po/` + migrations 212\u2013217 |\\n\\nGuides do **not** document:\\n- Internal implementation details (use code comments for that)\\n- Deprecated features (archive them in `docs/archive/`)\\n- Planned features not yet shipped (use `docs/ROADMAP.md` instead)\\n\\n---\\n\\n## Maintenance\\n\\n### When to Update a Guide\\n\\n- **Feature ships** \u2014 Update the guide in the same PR\\n- **Bug fix changes behavior** \u2014 Update the guide immediately\\n- **User reports confusion** \u2014 Add a Troubleshooting section or clarify the workflow\\n- **New test case discovered** \u2014 Add it to the testing guide\\n- **API endpoint changes** \u2014 Update the Technical Notes section\\n\\n### When to Create a New Guide\\n\\n- A new feature area ships (e.g., new compliance framework, new module)\\n- A complex workflow needs dedicated documentation\\n- A testing guide is needed for QA sign-off\\n\\n### Archive Old Guides\\n\\nWhen a feature is deprecated or replaced:\\n1. Move the guide to `docs/archive/{feature_name}.md`\\n2. Add a deprecation notice at the top\\n3. Link to the replacement guide if applicable\\n\\n---\\n\\n## Example: Adding a New Feature Guide\\n\\nTo document a new feature:\\n\\n1. **Create the file** in `docs/guide/{feature_name}.md`\\n2. **Start with Overview** \u2014 2-3 sentences on what it does\\n3. **Add Key Concepts** \u2014 Define terminology and data model\\n4. **Write Workflows** \u2014 Step-by-step user journeys\\n5. **Include Configuration** \u2014 Settings and customization\\n6. **Add Technical Notes** \u2014 Tables, endpoints, migrations\\n7. **Update README.md** \u2014 Add the guide to the index\\n8. **Link from related guides** \u2014 Cross-reference if it touches other features\\n9. **Include in PR** \u2014 Ship the guide with the feature code\\n\\n---\\n\\n## Navigation &amp; Discovery\\n\\nGuides are discoverable via:\\n\\n1. **README.md** \u2014 The index. Start here to find a guide by feature area.\\n2. **Cross-links** \u2014 Guides link to related guides (e.g., contracts guide links to billing guide).\\n3. **Sidebar in NexusOS** \u2014 Help icon on each page links to the relevant guide (future: in-app help system).\\n4. **Search** \u2014 GitHub search for guide filenames and content.\\n\\n---\\n\\n## Testing Guides as Acceptance Criteria\\n\\nTesting guides serve dual purposes:\\n\\n1. **QA Checklist** \u2014 Step-by-step test plan for sign-off\\n2. **Acceptance Criteria** \u2014 Defines what \\\"done\\\" means for a feature\\n3. **Regression Suite** \u2014 Becomes the manual test suite for future releases\\n\\nWhen a testing guide is complete and all steps pass, the feature is ready to ship.\\n\\n---\\n\\n## Future Enhancements\\n\\n- **In-app help system** \u2014 Embed guide sections directly in the UI\\n- **Video walkthroughs** \u2014 Link to recorded demos for complex workflows\\n- **API documentation** \u2014 Auto-generate from OpenAPI spec and link from guides\\n- **Glossary** \u2014 Centralized terminology reference\\n- **Localization** \u2014 Translate guides to Spanish, French, etc.\",\"docs-integrations\":\"# docs \u2014 integrations\\n\\n# Integrations Module\\n\\n## Overview\\n\\nThe **integrations** module manages the backlog and implementation roadmap for third-party vendor integrations into NexusOS. It serves as a staging ground for planned integrations, capturing vendor API details, authentication requirements, and cross-module dependencies before implementation begins.\\n\\nThis module is **not a runtime component**\u2014it is a reference and planning tool. The actual integration implementations live in `internal//` (direct REST clients) or route through the MCP layer (`mcp_servers`), but this documentation captures the *why*, *what*, and *how* for each integration before code is written.\\n\\n## Purpose\\n\\nThe backlog solves a critical workflow problem: vendor integrations require API documentation, authentication schemes, and architectural decisions to be made *before* implementation. Rather than discovering these mid-build, the backlog captures the bare minimum upfront:\\n\\n- **API entry point** (REST, GraphQL, webhook, etc.)\\n- **Authentication method** and documentation link\\n- **Data to ingest** (what fields, what events)\\n- **Why we need it** (compliance, contract verification, KPI tracking, alerting)\\n- **Cross-module tie-ins** (which existing modules depend on this data)\\n- **Open questions** that must be resolved in the build session\\n\\nThis prevents false starts and ensures the implementation session can begin with all reference material already in hand.\\n\\n## Structure\\n\\n### BACKLOG.md\\n\\nThe backlog is a single Markdown file (`docs/integrations/BACKLOG.md`) organized as:\\n\\n1. **Header** \u2014 purpose statement\\n2. **Vendor entries** \u2014 one per planned integration, each containing:\\n   - **Category** \u2014 vendor type (e.g., \\\"Backup &amp; Disaster Recovery\\\")\\n   - **What we want to ingest** \u2014 specific data fields and events\\n   - **Why** \u2014 business justification (compliance, reporting, alerting)\\n   - **API entry point** \u2014 protocol and authentication docs link\\n   - **Tie-ins to existing modules** \u2014 which modules consume this data\\n   - **Open questions** \u2014 architectural decisions for the build session\\n3. **Process section** \u2014 workflow for picking up a backlog item\\n\\n### Per-Vendor Documentation\\n\\nOnce an integration is picked up, a new file is created at `docs/integrations/.md` containing:\\n\\n- **Build plan** \u2014 phased approach (ingest \u2192 cache \u2192 display \u2192 tie-in)\\n- **Implementation PR link** \u2014 tracks the actual code\\n- **API reference** \u2014 condensed auth and endpoint details\\n- **Data model** \u2014 schema for cached data\\n- **Module dependencies** \u2014 which modules consume the data and how\\n\\n## Key Concepts\\n\\n### Backlog Entry Anatomy\\n\\nEach vendor entry captures:\\n\\n| Field | Purpose | Example |\\n|-------|---------|---------|\\n| **Category** | Vendor classification | \\\"Backup &amp; Disaster Recovery\\\" |\\n| **What we want to ingest** | Specific data points | \\\"protected devices, backup job status, last-success timestamps\\\" |\\n| **Why** | Business driver | \\\"feed BCDR posture into NexusOS for client reporting\\\" |\\n| **API entry point** | Protocol and auth docs | \\\"REST, https://docs.slide.tech/api/#description/authentication\\\" |\\n| **Tie-ins** | Consuming modules | Compliance, Contract Management, Helpdesk, NexusPulse |\\n| **Open questions** | Decisions for build session | \\\"Polling vs webhook?\\\", \\\"Per-tenant API key model?\\\" |\\n\\n### Ambiguity Handling\\n\\nWhen a backlog entry contains conflicting or unclear information (e.g., the Atakama/Ataccama case), the entry flags the ambiguity and defers resolution to the build session. This prevents wasted effort on the wrong product.\\n\\n## Workflow: From Backlog to Implementation\\n\\n```\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502 1. Backlog Entry Created                                    \u2502\\n\u2502    (vendor name, API docs, tie-ins, open questions)         \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                     \u2502\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502 2. Pick Up &amp; Confirm Scope                                  \u2502\\n\u2502    (resolve ambiguities, confirm with user)                 \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                     \u2502\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502 3. Read Auth Docs End-to-End                                \u2502\\n\u2502    (understand API authentication before coding)            \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                     \u2502\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502 4. Create docs/integrations/.md                     \u2502\\n\u2502    (build plan, data model, tie-ins)                        \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                     \u2502\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502 5. UI Mockup (artifact approval required)                   \u2502\\n\u2502    (no code without design sign-off)                        \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                     \u2502\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502 6. Implement in Phases                                      \u2502\\n\u2502    a) Ingest (REST client or MCP route)                     \u2502\\n\u2502    b) Cache (new migration, never modify existing)          \u2502\\n\u2502    c) Display (UI components)                               \u2502\\n\u2502    d) Tie-in (compliance, contracts, helpdesk, KPIs)        \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                     \u2502\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502 7. Link Implementation PR to Backlog Entry                   \u2502\\n\u2502    (move entry to docs/integrations/.md)            \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n```\\n\\n## Current Backlog\\n\\n### Slide (BCDR)\\n\\n**Status:** Queued for build\\n\\n**Category:** Backup &amp; Disaster Recovery\\n\\n**Data to ingest:**\\n- Protected devices\\n- Backup job status\\n- Last-success timestamps\\n- Retention policies\\n- Restore points\\n- Alerts\\n\\n**Business drivers:**\\n- Feed BCDR posture into NexusOS for client reporting\\n- Contract verification (verify paid backups are actually running)\\n- Compliance evidence (CMMC, HIPAA backup controls)\\n- KPI tracking (backup success rate in NexusPulse)\\n\\n**API:** REST, authenticated via https://docs.slide.tech/api/#description/authentication\\n\\n**Module tie-ins:**\\n- **Compliance** \u2014 auto-evidence for backup-related controls\\n- **Contract Management** \u2014 verify each \\\"Backup\\\" line item maps to a live, healthy Slide-protected device\\n- **Helpdesk** \u2014 auto-ticket on failed backup or missed retention\\n- **NexusPulse** \u2014 backup-success-rate KPI\\n\\n**Open questions for build session:**\\n- Polling vs webhook for job status updates?\\n- Per-tenant API key model (one Slide tenant per MSP, multi-client)?\\n- Map Slide \\\"agents\\\" to existing `devices` table or maintain separate cache?\\n\\n### Atakama / Ataccama \u2014 Requires Clarification\\n\\n**Status:** Blocked pending product confirmation\\n\\n**Issue:** User input references **Atakama** (atakama.com \u2014 browser endpoint security) but the provided URL points to **Ataccama** (ataccama.com \u2014 enterprise data quality platform). These are different products with different APIs.\\n\\n**If Atakama (most likely):**\\n- **Category:** Endpoint Security\\n- **Data to ingest:** protected resource events, encrypted-file activity, policy violations\\n- **Module tie-ins:** Cybersec/SIEM, Compliance (encryption evidence), Helpdesk (policy violation alerts)\\n\\n**If Ataccama:**\\n- **Category:** Data Quality &amp; Governance\\n- **Assessment:** Likely not aligned with MSP-focused PSA needs; flag for review\\n\\n**Next step:** Confirm product with user before proceeding to build.\\n\\n## Implementation Patterns\\n\\n### Direct REST Client\\n\\nFor vendors with straightforward REST APIs, create a client in `internal//`:\\n\\n```\\ninternal/\\n  slide/\\n    client.go          # REST client, auth, polling logic\\n    models.go          # Slide API response types\\n    cache.go           # Cache layer (read/write to DB)\\n```\\n\\n### MCP-Routed Integration\\n\\nFor vendors that integrate through the Model Context Protocol layer, add a row to `mcp_servers` and implement the handler in the MCP service.\\n\\n### Cache Tables\\n\\nEvery integration requires a new migration for cached data. **Never modify existing migrations.** Create a new migration file:\\n\\n```\\nmigrations/\\n  _add__cache_table.sql\\n```\\n\\n### Phased Rollout\\n\\n1. **Ingest** \u2014 fetch data from vendor API\\n2. **Cache** \u2014 store in local DB\\n3. **Display** \u2014 UI components to show cached data\\n4. **Tie-in** \u2014 integrate with Compliance, Contract Management, Helpdesk, NexusPulse\\n\\n## Cross-Module Dependencies\\n\\nIntegrations feed data into:\\n\\n- **Compliance** \u2014 auto-evidence for control compliance (backups, encryption, policy enforcement)\\n- **Contract Management** \u2014 verify service line items are active and healthy\\n- **Helpdesk** \u2014 auto-ticket on failures, policy violations, missed SLAs\\n- **NexusPulse** \u2014 KPIs (backup success rate, encryption coverage, policy violation trends)\\n- **Cybersec / SIEM** \u2014 ingest security events alongside FortiGate, agent telemetry\\n\\n## Contributing\\n\\nTo pick up a backlog item:\\n\\n1. **Confirm scope** \u2014 resolve any ambiguities with the user (e.g., Atakama vs Ataccama)\\n2. **Read auth docs** \u2014 understand the vendor's authentication scheme end-to-end\\n3. **Create vendor doc** \u2014 move the entry to `docs/integrations/.md` with a build plan\\n4. **Design first** \u2014 UI mockup and artifact approval before any code\\n5. **Implement in phases** \u2014 ingest \u2192 cache \u2192 display \u2192 tie-in\\n6. **Link PR** \u2014 update the backlog entry with the implementation PR link\",\"docs-mockups\":\"# docs \u2014 mockups\\n\\n# Docs \u2014 Mockups Module\\n\\n## Overview\\n\\nThe **docs/mockups** module is a collection of static HTML prototypes and design artifacts that document proposed UI changes across NexusOS. These are not executable code\u2014they are visual specifications for developers and stakeholders to review before implementation.\\n\\nThe module serves three purposes:\\n\\n1. **Design review** \u2014 Stakeholders approve UI layouts, copy, and interaction patterns before engineering begins\\n2. **Implementation reference** \u2014 Developers use mockups as pixel-accurate specs during feature build\\n3. **Decision capture** \u2014 Mockups embed architectural choices (routing, data flow, state management) that inform backend schema and API design\\n\\n## Structure\\n\\n```\\ndocs/mockups/\\n\u251c\u2500\u2500 brief_mockup_v2.html          # Ticket detail modal (Brief) \u2014 full 3-column layout\\n\u251c\u2500\u2500 live_review_generalization.html # Cross-framework compliance review UI\\n\u251c\u2500\u2500 phase3/\\n\u2502   \u251c\u2500\u2500 _shared.css               # Shared layout + utility classes for Phase 3 mockups\\n\u2502   \u251c\u2500\u2500 index.html                # Phase 3 mockup index &amp; decision matrix\\n\u2502   \u251c\u2500\u2500 agreement-saas.html       # SaaS provisioning on Agreement Detail\\n\u2502   \u251c\u2500\u2500 contract-materials.html   # Hardware procurement column on Contract Line Items\\n\u2502   \u251c\u2500\u2500 procurement-list.html     # Top-level PO roll-up dashboard\\n\u2502   \u251c\u2500\u2500 procurement-detail.html   # Individual PO detail page\\n\u2502   \u251c\u2500\u2500 remediation-queue.html    # Operator queue for failed distributions\\n\u2502   \u251c\u2500\u2500 settings-distributors.html # Tenant SaaS policy toggles\\n\u2502   \u251c\u2500\u2500 ticket-po-integration.html # Linked POs in Ticket detail (two options)\\n\u2502   \u2514\u2500\u2500 project-po-integration.html # Procurement column on Project Costs tab\\n```\\n\\n## Key Artifacts\\n\\n### 1. Brief Mockup v2 (`brief_mockup_v2.html`)\\n\\n**Purpose:** Full-screen modal for ticket detail in the helpdesk.\\n\\n**Layout:** 3-column grid\\n- **Left rail** (260px) \u2014 Customer metadata, contract info, linked assets, related tickets\\n- **Center stage** \u2014 Message thread with reply box\\n- **Right co-pilot** (360px) \u2014 AI suggestions, KB matches, asset health\\n\\n**Key components:**\\n- **Vital strip** \u2014 Ticket ID, title, status, priority, SLA, sentiment\\n- **Message thread** \u2014 Customer, internal, and system messages with timestamps\\n- **Reply box** \u2014 Tabbed (Reply / Internal Note / Status Change) with attachment + KB snippet buttons\\n- **Nexie co-pilot** \u2014 Pattern-matched KB articles, suggested actions (keyboard shortcuts), related assets\\n\\n**Design tokens:** Uses `nexus.css` custom properties (colors, typography, spacing, shadows, animations).\\n\\n**Interaction patterns:**\\n- Click table row \u2192 open Brief modal\\n- ESC key \u2192 close modal\\n- Keyboard shortcuts for common actions (\u23181, \u23182, \u23183 for co-pilot actions)\\n\\n### 2. Live Review Generalization (`live_review_generalization.html`)\\n\\n**Purpose:** Proposal to make \\\"Start Live Review\\\" available across all compliance frameworks (NCSR, CMMC, HIPAA, PCI, SOC2, ISO27001), not just NCSR.\\n\\n**Three artifacts:**\\n\\n#### Artifact 1 \u2014 Generic Composer Dashboard with Live Review banner\\n- **Banner** appears between stats and domain cards when `seeder.HasActionablePlan() == true`\\n- **Copy** is framework-agnostic: \\\"Walk [customer] through every actionable item with live signatures\u2026\\\"\\n- **Empty state** shows disabled button when no actionable items exist\\n- **Reusable component** \u2014 same banner renders on every framework dashboard\\n\\n#### Artifact 2 \u2014 Generalized Host Page header\\n- **Breadcrumbs** bind to `framework_name` instead of hardcoded \\\"NCSR\\\"\\n- **Title** format: `{Framework} Live Review \u2014 {Customer} \u00b7 {Period}`\\n- **Body** (message queue, signature pad, audit feed) is byte-for-byte unchanged\\n- **Relabel only** \u2014 no new template, just data binding\\n\\n#### Artifact 3 \u2014 Reviews History (cross-framework)\\n- **Route** generalizes from `/composer/{cid}/nist_csf/ncsr/{aid}/reviews` to `/composer/{cid}/{slug}/reviews`\\n- **Framework column** added so users can scan all reviews across frameworks at a glance\\n- **Same template** for every framework\\n\\n**Phasing strategy:**\\n1. **Phase 1 (Plumbing)** \u2014 Define `ActionPlanSeeder` interface, refactor NCSR seeder, add generic API route\\n2. **Phase 2 (UI cutover)** \u2014 Move banner to generic dashboard, generalize Reviews History route\\n3. **Phase 3+ (Framework onboarding)** \u2014 Implement seeders for CMMC, HIPAA, PCI, etc. (one PR per framework)\\n\\n### 3. Phase 3 Procurement Automation Mockups\\n\\n**Purpose:** Eight surfaces for a new procurement automation system that bridges NexusOS contracts/agreements with QuickBooks Online and distributor APIs (PAX8, Sherweb).\\n\\n#### 3.1 Procurement List (`procurement-list.html`)\\n- **Hex tiles** \u2014 Roll-up metrics (Total POs, Pending, Billed, Failed, etc.)\\n- **Filterable table** \u2014 All POs joined to QBO, grouped by status\\n- **Status pills** \u2014 `requested`, `po_approved`, `po_sent`, `received`, `billed`, `failed`\\n\\n#### 3.2 Procurement Detail (`procurement-detail.html`)\\n- **QBO header** (read-only) \u2014 PO number, vendor, amount, date\\n- **Local metadata** \u2014 Idempotency key, attempt sequence, dispatch history\\n- **Audit trail** \u2014 Timestamp log of state transitions (PO sent \u2192 received \u2192 billed)\\n- **Retry controls** \u2014 Manual retry button for failed dispatches\\n\\n#### 3.3 Remediation Queue (`remediation-queue.html`)\\n- **Grouped by error_class** \u2014 `sku_invalid`, `vendor_unavailable`, `qty_exceeded`, etc.\\n- **Suggested fixes** \u2014 AI-generated remediation steps (e.g., \\\"Check SKU mapping in settings\\\")\\n- **Bulk retry** \u2014 Select multiple failed items and retry in batch\\n\\n#### 3.4 Agreement SaaS (`agreement-saas.html`)\\n- **SaaS lines table** \u2014 M365, Teams Phone, Proofpoint, etc.\\n- **Provisioning column** \u2014 Status per line (`provisioned`, `failed`, `awaiting human gate`)\\n- **Activation timeline** \u2014 Chronological log of distributor responses\\n- **Policy toggle** \u2014 Tenant-level setting: auto-provision renewals vs. human gate for first-time\\n\\n#### 3.5 Settings Distributors (`settings-distributors.html`)\\n- **SaaS policy toggles** \u2014 Auto-provision renewals, human gate for new agreements\\n- **Per-vendor toggles** \u2014 Auto-dispatch materials on QBO PO Approved (PAX8, Sherweb, etc.)\\n- **Connected distributors** \u2014 List of active vendor integrations\\n\\n#### 3.6 Contract Materials (`contract-materials.html`)\\n- **Extension** of existing Line Items tab (no new tab)\\n- **Procurement column** \u2014 Only renders for `service_type='hardware'`\\n- **Status pills** \u2014 `requested`, `po_approved`, `po_sent`, `received`, `billed`\\n- **Schema delta** \u2014 Adds `procurement_status` and `po_ref_id` to `legal_contract_line_items`\\n\\n#### 3.7 Ticket PO Integration (`ticket-po-integration.html`)\\n- **Two options** \u2014 Linked POs in Related tab (Option A) vs. new Procurement tab (Option B)\\n- **Shared Request-PO modal** \u2014 Create ad-hoc PO from ticket context\\n- **Decision pending** \u2014 Awaiting stakeholder input on tab placement\\n\\n#### 3.8 Project PO Integration (`project-po-integration.html`)\\n- **Extension** of existing Costs tab\\n- **Procurement column** \u2014 Shows PO status for each line item\\n- **Consolidation** \u2014 Merges `project_purchase_orders` \u2192 `purchase_orders` table\\n- **Where POs are used most** \u2014 Projects are the primary context for hardware procurement\\n\\n## Design System Integration\\n\\nAll mockups use **nexus.css**, a custom design system with:\\n\\n- **Color tokens** \u2014 `--nx-accent`, `--nx-danger`, `--nx-success`, `--nx-warning`, `--nx-info`, `--nx-violet`\\n- **Typography** \u2014 Inter (sans-serif) + JetBrains Mono (monospace)\\n- **Spacing scale** \u2014 4px base unit (6px, 9px, 12px, 16px, 20px, 24px, 28px, 32px)\\n- **Radius scale** \u2014 6px (sm), 9px (base), 12px (md), 16px (lg)\\n- **Shadows** \u2014 Layered depth (shadow, shadow-md, shadow-lg, shadow-xl)\\n- **Animations** \u2014 Easing curve `cubic-bezier(0.16, 1, 0.3, 1)` with durations 150ms (fast), 250ms (base), 400ms (slow)\\n\\n**Component patterns:**\\n- **Pills** \u2014 Inline badges with semantic colors (`.pill-success`, `.pill-warning`, `.pill-danger`)\\n- **Cards** \u2014 Frosted glass effect with border and subtle gradient\\n- **Tables** \u2014 Sticky headers, hover states, monospace for IDs/codes\\n- **Buttons** \u2014 Primary (gradient), secondary (glass), danger (red tint)\\n- **Modals** \u2014 Full-screen overlay with backdrop blur, centered frame with inset padding\\n\\n## How to Use These Mockups\\n\\n### For Designers &amp; Product Managers\\n1. Open mockup in browser (from disk or dev server)\\n2. Review layout, copy, interaction patterns\\n3. Approve or request changes via comments/PR\\n4. Mockup becomes the spec for engineering\\n\\n### For Developers\\n1. Use mockup as pixel-accurate reference during implementation\\n2. Match spacing, colors, typography to mockup exactly\\n3. Implement interactions (click handlers, keyboard shortcuts) as shown\\n4. Reuse component classes from `nexus.css` (don't reinvent)\\n\\n### For Stakeholders\\n1. Review decision matrix in Phase 3 index (`phase3/index.html`)\\n2. Approve or reject proposed routes, data flows, state transitions\\n3. Decisions are embedded in mockup structure (e.g., \\\"Q11 \u2014 Materials activate on QBO Bill posted\\\")\\n\\n## Key Decisions Captured in Mockups\\n\\n### Live Review Generalization\\n- **Decision:** Make \\\"Start Live Review\\\" the default for every compliance framework\\n- **Schema:** Already framework-agnostic (migration 186) \u2014 no DB changes needed\\n- **Risk:** Low \u2014 NCSR keeps working through Phase 1; UI cutover is Phase 2\\n- **Phasing:** 5 phases, each a small PR with no speculation chains\\n\\n### Procurement Automation\\n- **Q7:** No \\\"Edit PO\\\" controls in NexusOS \u2014 QBO header is read-only with link out\\n- **Q8:** Two triggers: materials on QBO PO Approved (per-vendor toggle), SaaS on agreement line active (per-tenant toggle)\\n- **Q9:** Idempotency key visible on detail page; attempt_seq + format documented inline\\n- **Q10:** Remediation queue grouped by error_class with suggested fixes\\n- **Q11:** Materials activate on QBO Bill posted; SaaS activates on distributor `provisioned` response\\n\\n## Rendering &amp; Testing\\n\\n### Static HTML\\n- Open any `.html` file directly in a browser (no server required)\\n- CSS is embedded or linked to `nexus.css` (relative path)\\n- No JavaScript dependencies beyond vanilla DOM manipulation\\n\\n### With Dev Server\\n```bash\\n# Serve from project root\\npython -m http.server 8000\\n# Open http://localhost:8000/docs/mockups/brief_mockup_v2.html\\n```\\n\\n### Responsive Design\\n- Mockups are desktop-first (1280px+ viewport)\\n- Brief modal uses fixed layout (not responsive)\\n- Phase 3 mockups use CSS Grid for flexible layouts\\n\\n## Maintenance &amp; Updates\\n\\nWhen updating a mockup:\\n\\n1. **Keep nexus.css in sync** \u2014 If design tokens change, update all mockups\\n2. **Document decisions** \u2014 Add comments in HTML or update decision matrix\\n3. **Link to related code** \u2014 Add `` comments\\n4. **Version control** \u2014 Commit mockups alongside feature PRs for traceability\\n\\n## Related Code Paths\\n\\n- **Brief modal** \u2192 `internal/ui/templates/helpdesk/ticket_detail.html` (v2 implementation)\\n- **Live Review** \u2192 `internal/ui/templates/composer/dashboard.html`, `composer/host.html`, `composer/reviews.html`\\n- **Procurement** \u2192 `internal/ui/templates/procurement/`, `legal/`, `distributor/`\\n- **Design system** \u2192 `internal/ui/static/css/nexus.css`\\n\\n---\\n\\n**Last updated:** Phase 3 mockups added; Live Review generalization approved for Phase 1 implementation.\",\"docs-modules\":\"# docs \u2014 modules\\n\\n# Docs \u2014 Modules\\n\\n## Overview\\n\\nThe **docs \u2014 modules** subsystem is a documentation framework for NexusOS. It provides a standardized structure for documenting each module in the platform, ensuring consistency, discoverability, and maintainability across the codebase.\\n\\nEach module in NexusOS follows a consistent documentation pattern with four core documents:\\n\\n- **API.md** \u2014 HTTP endpoints, request/response formats, authentication requirements\\n- **MANUAL.md** \u2014 User-facing guide for features and workflows\\n- **SOURCE_MAP.md** \u2014 Code structure, file organization, key types and functions\\n- **WHITEPAPER.md** \u2014 Technical architecture, design decisions, security model\\n\\nAdditionally, modules may include:\\n\\n- **TODO.md** \u2014 Tracked work items and technical debt\\n- **README.md** \u2014 Quick reference (optional)\\n\\n---\\n\\n## Document Types\\n\\n### API.md\\n\\nComprehensive reference for all HTTP endpoints exposed by the module.\\n\\n**Structure:**\\n- Endpoint tables with method, path, handler name, and description\\n- Request/response examples (JSON or form-encoded)\\n- Query parameters and filters\\n- Authentication requirements\\n- Type definitions (JSON schemas)\\n- Inter-module integration points\\n\\n**Audience:** Backend developers, API consumers, frontend developers\\n\\n**Example sections:**\\n- Page Routes (HTMX/HTML)\\n- API Routes (JSON)\\n- Public Routes (token-secured)\\n- Query Parameters\\n- Type Reference\\n\\n### MANUAL.md\\n\\nUser-facing documentation explaining how to use the module's features.\\n\\n**Structure:**\\n- Feature overview with step-by-step workflows\\n- UI navigation paths\\n- Field descriptions and validation rules\\n- Lifecycle diagrams (e.g., quote status transitions)\\n- Common tasks and troubleshooting\\n- Screenshots or UI references (implied)\\n\\n**Audience:** End users, support staff, product managers\\n\\n**Example sections:**\\n- Table of Contents\\n- Feature-by-feature breakdown\\n- Workflows with numbered steps\\n- Status/state definitions\\n- Tips and best practices\\n\\n### SOURCE_MAP.md\\n\\nDeveloper-focused guide to the module's codebase.\\n\\n**Structure:**\\n- File index with line counts and purposes\\n- Struct map (types and their SQL tables)\\n- Key functions and methods\\n- Database tables referenced\\n- External dependencies\\n- Callback hooks and integration points\\n\\n**Audience:** Backend developers, code reviewers, maintainers\\n\\n**Example sections:**\\n- File Index\\n- Struct Map\\n- SQL Tables Referenced\\n- Dependencies\\n- Callback Hooks\\n\\n### WHITEPAPER.md\\n\\nTechnical deep-dive into architecture, design, and security.\\n\\n**Structure:**\\n- Executive summary\\n- Architecture overview (technology stack, patterns)\\n- Subsystem design (major features broken down)\\n- Security model (threat mitigation)\\n- Data flow diagrams\\n- Database schema with column descriptions\\n- Algorithm explanations\\n- Performance considerations\\n\\n**Audience:** Architects, senior developers, security reviewers\\n\\n**Example sections:**\\n- Executive Summary\\n- Architecture Overview\\n- Subsystem Design\\n- Security Model\\n- Data Flow Diagram\\n- Database Schema\\n- Algorithm Explanations\\n\\n### TODO.md\\n\\nTracked work items, technical debt, and enhancement ideas.\\n\\n**Structure:**\\n- Organized by priority (High, Medium, Low)\\n- Grouped by category (features, bugs, refactoring)\\n- Checkbox format for tracking\\n- Brief description of each item\\n- Context or rationale where helpful\\n\\n**Audience:** Product managers, developers, tech leads\\n\\n**Example sections:**\\n- High Priority\\n- Medium Priority\\n- Low Priority / Enhancements\\n- Technical Debt\\n\\n---\\n\\n## Module Structure\\n\\nEach module directory follows this layout:\\n\\n```\\ndocs/modules/{module_name}/\\n\u251c\u2500\u2500 API.md           # HTTP endpoints and types\\n\u251c\u2500\u2500 MANUAL.md        # User guide\\n\u251c\u2500\u2500 SOURCE_MAP.md    # Code structure\\n\u251c\u2500\u2500 WHITEPAPER.md    # Technical architecture\\n\u251c\u2500\u2500 TODO.md          # Work items\\n\u2514\u2500\u2500 README.md        # (optional) Quick reference\\n```\\n\\nExample modules documented:\\n- `auth` \u2014 Authentication, MFA, SSO, session management\\n- `billing` \u2014 Quotes, invoices, payments, AR aging\\n- `certificates` \u2014 Internal CA, certificate signing, device assignment\\n- `compliance` \u2014 Framework tracking, gap analysis, device compliance\\n- `crm` \u2014 Client management, contacts, pipeline, activities, campaigns\\n\\n---\\n\\n## Key Patterns\\n\\n### Multi-Tenant Isolation\\n\\nAll modules enforce tenant isolation via `auth.TenantIDFromContext()`. Every SQL query includes a `WHERE tenant_id = $1` filter. This is documented in SOURCE_MAP.md under \\\"External Dependencies\\\" and in WHITEPAPER.md under \\\"Security Model.\\\"\\n\\n### Authentication &amp; Authorization\\n\\nModules document authentication requirements in API.md:\\n- Which endpoints require JWT authentication\\n- Which endpoints are public (e.g., token-secured approval portals)\\n- Permission checks via `auth.RequirePermission()`\\n\\n### HTMX + Server-Rendered HTML\\n\\nModules use server-rendered HTML templates with HTMX for dynamic updates. API.md distinguishes between:\\n- **Page Routes** \u2014 Return full HTML or HTMX fragments\\n- **API Routes** \u2014 Return JSON\\n\\n### Error Handling\\n\\nModules document error responses in API.md (HTTP status codes, error messages). WHITEPAPER.md may discuss error recovery strategies.\\n\\n### Database Patterns\\n\\nSOURCE_MAP.md lists all tables referenced by the module. WHITEPAPER.md provides full schema with column descriptions, constraints, and relationships.\\n\\n---\\n\\n## Cross-Module Integration\\n\\nModules document their dependencies and integration points:\\n\\n**In SOURCE_MAP.md:**\\n- External dependencies (imports, packages)\\n- Callback hooks (function pointers set by main.go)\\n- Tables owned by other modules that this module reads\\n\\n**In API.md:**\\n- Inter-module calls (e.g., CRM reads from Ticketing, Billing, RMM)\\n\\n**In WHITEPAPER.md:**\\n- Data flow diagrams showing how modules interact\\n- Async workflows (e.g., OnQuoteApproved callback)\\n\\n### Example: Billing Module Integration\\n\\nThe Billing module reads from:\\n- `auth` \u2014 User context, tenant isolation\\n- `ui` \u2014 Template rendering\\n- `ai` \u2014 Claude API for PLAUD quote generation\\n- `companies` \u2014 Client company data\\n- `products` \u2014 Product catalog\\n- `users` \u2014 User names for audit logs\\n- `tickets` \u2014 Billable time entries and charges\\n\\nThis is documented in:\\n- **API.md** \u2014 Endpoints that accept company_id, product_id, ticket_id\\n- **SOURCE_MAP.md** \u2014 External dependencies section\\n- **WHITEPAPER.md** \u2014 Data flow diagram, database schema with FK relationships\\n\\n---\\n\\n## Documentation Maintenance\\n\\n### When to Update\\n\\n- **API.md** \u2014 When adding/removing endpoints or changing request/response formats\\n- **MANUAL.md** \u2014 When UI changes or new workflows are added\\n- **SOURCE_MAP.md** \u2014 When files are added/removed or major refactoring occurs\\n- **WHITEPAPER.md** \u2014 When architecture changes or new subsystems are added\\n- **TODO.md** \u2014 Continuously as work progresses\\n\\n### Consistency Checks\\n\\n- API.md endpoint paths must match actual route registrations in code\\n- SOURCE_MAP.md file names and line counts should be approximate (updated periodically)\\n- WHITEPAPER.md database schema should match actual SQL migrations\\n- MANUAL.md UI paths should match actual route handlers\\n\\n### Cross-References\\n\\nDocuments reference each other:\\n- API.md references WHITEPAPER.md for security model details\\n- MANUAL.md references API.md for technical details\\n- SOURCE_MAP.md references WHITEPAPER.md for architecture context\\n- TODO.md references API.md/SOURCE_MAP.md for context on work items\\n\\n---\\n\\n## Example: Auth Module Documentation\\n\\nThe Auth module demonstrates the full documentation pattern:\\n\\n**API.md** defines:\\n- Login, MFA, token refresh, logout endpoints\\n- JWT claims structure (tid, uid, email, name, role, perms)\\n- Session management endpoints\\n- SSO endpoints with PKCE\\n\\n**MANUAL.md** explains:\\n- How to log in (password or SSO)\\n- How to enroll in MFA\\n- How to use backup codes\\n- How to manage sessions\\n- Token refresh behavior\\n\\n**SOURCE_MAP.md** maps:\\n- `handler.go` \u2014 HTTP handlers for all auth endpoints\\n- `jwt.go` \u2014 RS256 token signing/verification\\n- `mfa.go` \u2014 TOTP and backup code logic\\n- `middleware.go` \u2014 Authentication and permission checks\\n- `password.go` \u2014 bcrypt hashing\\n- `session.go` \u2014 Refresh token management\\n- `sso.go` \u2014 OAuth2/OIDC integration\\n\\n**WHITEPAPER.md** covers:\\n- JWT token system (RS256, 8-hour TTL)\\n- MFA architecture (TOTP + backup codes)\\n- Password security (bcrypt cost 12, min 8 chars)\\n- Refresh token sessions (7-day TTL, rotate-on-use)\\n- SSO with PKCE\\n- Authorization middleware (permission wildcards)\\n- Context helpers for extracting claims\\n\\n---\\n\\n## Benefits\\n\\n### For Developers\\n\\n- **Onboarding** \u2014 New developers can understand a module's purpose and structure in 30 minutes\\n- **Code Review** \u2014 Reviewers can verify changes against documented API contracts\\n- **Refactoring** \u2014 Clear understanding of dependencies and integration points\\n- **Debugging** \u2014 SOURCE_MAP.md and WHITEPAPER.md provide context for troubleshooting\\n\\n### For Product &amp; Support\\n\\n- **MANUAL.md** provides step-by-step user guides\\n- **API.md** clarifies what's possible (feature scope)\\n- **TODO.md** shows planned work and known limitations\\n\\n### For Architecture &amp; Security\\n\\n- **WHITEPAPER.md** documents design decisions and threat models\\n- **API.md** clarifies authentication and authorization\\n- **SOURCE_MAP.md** shows external dependencies and integration risks\\n\\n---\\n\\n## Governance\\n\\n### Who Writes Documentation\\n\\n- **API.md** \u2014 Backend developer who implements the endpoints\\n- **MANUAL.md** \u2014 Product manager or UX writer, reviewed by support\\n- **SOURCE_MAP.md** \u2014 Backend developer (can be auto-generated from code analysis)\\n- **WHITEPAPER.md** \u2014 Architect or senior developer\\n- **TODO.md** \u2014 Product manager or tech lead\\n\\n### Review Process\\n\\n1. Code changes are submitted with documentation updates\\n2. Reviewer checks that API.md matches actual endpoints\\n3. Reviewer checks that SOURCE_MAP.md reflects code structure\\n4. For major changes, WHITEPAPER.md is reviewed by architect\\n\\n### Versioning\\n\\nDocumentation is versioned alongside code. Breaking API changes require:\\n- API.md update with version note\\n- MANUAL.md update with migration guide\\n- WHITEPAPER.md update if architecture changes\\n\\n---\\n\\n## Tools &amp; Automation\\n\\n### Code Analysis\\n\\nSOURCE_MAP.md can be partially auto-generated from:\\n- Go AST parsing to extract struct definitions\\n- Function signatures and comments\\n- Import statements for dependencies\\n\\n### Validation\\n\\nCI/CD can validate:\\n- API.md endpoint paths match route registrations\\n- Database table names in WHITEPAPER.md match migrations\\n- External dependencies in SOURCE_MAP.md match actual imports\\n\\n### Search &amp; Discovery\\n\\nDocumentation is indexed for:\\n- Full-text search across all modules\\n- API endpoint discovery (method + path)\\n- Type/struct lookup\\n- Cross-module dependency graph\\n\\n---\\n\\n## Summary\\n\\nThe **docs \u2014 modules** framework ensures that every module in NexusOS is thoroughly documented with consistent structure and quality. By separating concerns (API, user guide, code structure, architecture), documentation remains maintainable and useful for different audiences.\\n\\nEach module's four core documents form a complete reference:\\n- **API.md** \u2014 What can you do?\\n- **MANUAL.md** \u2014 How do you do it?\\n- **SOURCE_MAP.md** \u2014 Where is the code?\\n- **WHITEPAPER.md** \u2014 Why is it designed this way?\",\"docs-security\":\"# docs \u2014 security\\n\\n# Security Evidence Module\\n\\nThe `docs/security/` module is a **source-of-truth corpus** for security claims in NexusOS PSA. It bridges code, tests, and compliance frameworks by maintaining a set of markdown documents that map every security control to its implementation and proof.\\n\\n## Purpose\\n\\nThis module serves three audiences:\\n\\n1. **Prospects and auditors** \u2014 open the rendered dashboard (`index.html`) to see which compliance frameworks are covered and drill down to code references and test evidence.\\n2. **Engineers** \u2014 when you modify security-critical code (auth, crypto, network listeners, audit logging), the corresponding skill (`psa-compliance` or `psa-netsec`) tells you which artifact in this corpus must update alongside your change.\\n3. **Compliance teams** \u2014 the corpus is the single source of truth for what's implemented, what's tested, and what's still a gap.\\n\\nThe module is **not** a policy document or a security handbook. It is a **traceability matrix** \u2014 every claim has a code reference and a test reference.\\n\\n## Structure\\n\\n### Core documents\\n\\n**`EVIDENCE.md`** (this file)\\n- Entry point and navigation guide\\n- Lists all sections and their purpose\\n- Tracks stale verification stamps (controls not re-tested in 90+ days)\\n\\n**`controls/`** directory\\n- One markdown file per (framework, control) pair\\n- Frontmatter: `framework`, `control_id`, `title`, `last_verified`, `status`\\n- Three sections: **Control text** (from the framework), **Implementation** (code references), **Proof** (test references)\\n- Status values: `implemented`, `partial`, `gap`\\n- Example: `controls/soc2-cc6.1-logical-access.md`\\n\\n**`network-surface.md`**\\n- Inventory of every listener (inbound) and external API client (outbound)\\n- Columns: module, listener/target, port, protocol, TLS version, auth method, rate limiting, audit log, code reference, test reference\\n- Enforced by `psa-netsec` skill: no merge without a row for new listeners\\n\\n**`crypto-inventory.md`**\\n- Every use of TLS, mTLS, JWT, AES, or other cryptographic primitive\\n- Sections: TLS/mTLS, symmetric encryption, token signing, hashing\\n- Columns: module, use, algorithm/version, key source, rotation policy, code reference\\n- Enforced by `psa-netsec` skill: no merge without a row for new crypto\\n\\n**`audit-catalog.md`**\\n- Every sensitive action (delete, cross-tenant access, RBAC change, credential export, contract execution, billing adjustment) mapped to its audit log destination\\n- Columns: action, module, trigger code, audit table, tenant-scoped flag\\n- Enforced by `psa-compliance` skill: no merge without a row for new privileged actions\\n\\n**`threats/`** directory\\n- STRIDE-lite threat model per network-facing module\\n- Example: `threats/rmm.md` covers the RMM agent listener\\n- Sections: Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege\\n- Each section describes the threat, mitigations, and known gaps\\n- Gaps are tracked in a table with severity and notes\\n\\n### Rendered dashboard\\n\\n**`index.html`**\\n- Generated from the markdown corpus (run `make security-html` to regenerate)\\n- Shows framework coverage (% of controls implemented per framework)\\n- Lists all controls with status badges\\n- Embeds the network surface, crypto inventory, threat models, and audit catalog in tabular form\\n- Provides a single-page view for demos and audits\\n\\n## How to use\\n\\n### For a prospect or auditor\\n\\n1. Open `index.html` in a browser.\\n2. Scan the \\\"Framework coverage\\\" section to see which compliance frameworks are tracked and how complete each is.\\n3. Click on a framework card or navigate to the \\\"Controls\\\" section to see individual controls.\\n4. For each control, click the code reference (e.g., `cmd/psa/main.go:1553`) to jump to the implementation.\\n5. Click the test reference (e.g., `internal/crm/tenant_isolation_test.go:282`) to see the proof.\\n\\n### For an engineer modifying security code\\n\\n1. Identify which security artifact you're changing (e.g., auth middleware, TLS config, audit logging).\\n2. The corresponding skill (`psa-compliance` or `psa-netsec`) will flag which corpus file must update:\\n   - Changing `enforceAuth` \u2192 update `controls/soc2-cc6.1-logical-access.md` (implementation and proof sections)\\n   - Adding a new listener \u2192 add a row to `network-surface.md`\\n   - Adding a new crypto primitive \u2192 add a row to `crypto-inventory.md`\\n   - Adding a new privileged action \u2192 add a row to `audit-catalog.md`\\n3. Update the `last_verified` timestamp in the control file to today's date.\\n4. If your change affects test coverage, update the proof section.\\n\\n### For a compliance team\\n\\n1. Open `EVIDENCE.md` to see the \\\"Stale stamps\\\" section \u2014 any control with `last_verified` older than 90 days needs re-testing.\\n2. For each stale control, re-run the test referenced in the Proof section and update `last_verified`.\\n3. If a control's status is `partial` or `gap`, review the Notes section to understand what's missing and file a task.\\n\\n## Key concepts\\n\\n### Status values\\n\\n- **`implemented`** \u2014 the control is fully implemented and tested. All code paths are covered by tests.\\n- **`partial`** \u2014 the control is implemented but test coverage is incomplete. The Notes section explains what's missing.\\n- **`gap`** \u2014 the control is not yet implemented. This is a placeholder for future work.\\n\\n### Verification stamps\\n\\nEach control has a `last_verified` date in YYYY-MM-DD format. This is the date the control's tests were last run. If a control is older than 90 days, the skill surfaces it as stale and the compliance team re-runs the test to confirm it still passes.\\n\\n### Code and test references\\n\\nEvery claim in the Implementation and Proof sections includes a code reference in the format `path/to/file.go:line_number`. These are **exact line numbers** \u2014 they should point to the specific line of code that implements or proves the claim. When code changes, these references must be updated.\\n\\n### Tenant scoping\\n\\nThe `audit-catalog.md` includes a \\\"Tenant-scoped?\\\" column. All audit rows for multi-tenant systems must carry a `tenant_id` column to ensure one tenant cannot read another's audit logs.\\n\\n## Connection to the codebase\\n\\n### Skills\\n\\nTwo skills maintain this corpus:\\n\\n- **`psa-compliance`** \u2014 watches for new privileged actions (delete, RBAC change, credential export, etc.) and requires a row in `audit-catalog.md` before merge. Also flags stale control verifications.\\n- **`psa-netsec`** \u2014 watches for new network listeners and crypto primitives and requires rows in `network-surface.md` and `crypto-inventory.md` before merge.\\n\\n### Audit logging\\n\\nAll audit writes in the codebase go through helpers in `internal/audit/` (when present). The `audit-catalog.md` maps each privileged action to its audit table and code location. If you add a new privileged action, you must:\\n\\n1. Call the appropriate audit helper in `internal/audit/`\\n2. Add a row to `audit-catalog.md` with the action, module, trigger code, audit table, and tenant-scoped flag\\n\\n### Auth and network security\\n\\nThe `controls/soc2-cc6.1-logical-access.md` control references:\\n- `cmd/psa/main.go:1553` \u2014 the `enforceAuth` middleware that gates `/api/*` paths\\n- `internal/auth/jwt.go:291` \u2014 the `VerifyAccessToken` function that validates JWTs\\n\\nThe `network-surface.md` references:\\n- `cmd/psa/main.go:1437` \u2014 the RMM mTLS listener setup\\n- `internal/rmm/crypto/tls.go:40` \u2014 the TLS config construction\\n\\nIf you modify either of these, update the corresponding control and network-surface rows.\\n\\n## Known gaps and future work\\n\\nThe corpus is **freshly bootstrapped** and covers only one control (SOC2 CC6.1). The following are tracked for follow-up:\\n\\n- **Tenant scoping (CC6.6)** \u2014 RBAC and row-level isolation controls\\n- **Network encryption (CC6.6)** \u2014 TLS in transit for all listeners\\n- **Monitoring and alerting (CC7.2)** \u2014 audit log ingestion and alerting\\n- **Threat models** \u2014 STRIDE-lite for all network-facing modules (RMM is the first)\\n- **Crypto inventory** \u2014 populate all TLS, JWT, and AES entries as code ships\\n- **Audit catalog** \u2014 add rows as privileged actions are implemented\\n\\nThe RMM threat model (`threats/rmm.md`) documents three known gaps:\\n\\n1. **No `user_id` in `agent_commands`** \u2014 operator repudiation is incomplete\\n2. **No application-level rate limit on `:8443`** \u2014 DoS mitigation relies on OS TCP limits\\n3. **Intra-tenant agent impersonation not bound to cert subject** \u2014 a compromised agent can impersonate other agents in the same tenant\\n\\n## Regenerating the dashboard\\n\\nAfter any change to the markdown corpus, regenerate the HTML dashboard:\\n\\n```bash\\nmake security-html\\n```\\n\\nThis reads all markdown files in `docs/security/` and outputs `index.html` with the latest framework coverage, control status, and embedded tables.\",\"docs-ui-samples\":\"# docs \u2014 ui-samples\\n\\n# UI Samples Module\\n\\n## Overview\\n\\nThe **ui-samples** module is a collection of HTML mockups and design system demonstrations for NexusOS and Orchestrator Studio. These are **static reference documents** intended for designers, developers, and stakeholders to preview UI patterns, material finishes, button styles, and component configurations before implementation.\\n\\nThe module contains **four independent sample files**, each showcasing a different aspect of the visual design system:\\n\\n1. **Action Config Panels** \u2014 Orchestrator workflow action configuration UI\\n2. **Frosted Black Matte** \u2014 Dark mode material comparison (glass vs. matte)\\n3. **Frosted White Ceramic** \u2014 Light mode material system\\n4. **Glass Action Buttons** \u2014 Button styles and interaction patterns\\n\\n---\\n\\n## File Structure\\n\\n```\\ndocs/ui-samples/\\n\u251c\u2500\u2500 action_config_panels_mockup.html\\n\u251c\u2500\u2500 hex-frosted-matte-sample.html\\n\u251c\u2500\u2500 hex-frosted-white-ceramic-sample.html\\n\u2514\u2500\u2500 hex-glass-buttons-sample.html\\n```\\n\\nEach file is **self-contained** with embedded CSS and no external dependencies (except Tailwind CDN in the material samples).\\n\\n---\\n\\n## Key Components\\n\\n### 1. Action Config Panels (`action_config_panels_mockup.html`)\\n\\n**Purpose:** Demonstrates the right-column configuration panels that appear in Orchestrator Studio when a user selects an action type in the node editor.\\n\\n**Panels Included:**\\n\\n| Action | Purpose | Key Fields |\\n|--------|---------|-----------|\\n| `send_notification` | Notify users of events | Recipients, Severity, Message, Delivery channels |\\n| `set_status` | Change ticket/deal status | New Status (button group) |\\n| `set_priority` | Escalate or deprioritize | New Priority (color-coded buttons) |\\n| `assign_to` | Assign to user or round-robin | User search, Round Robin toggle |\\n| `advance_deal_stage` | Move deal through pipeline | Target Stage (color-coded list) |\\n| `add_note` | Auto-add internal/public notes | Note text, Visibility toggle |\\n| `send_webhook` | Trigger external API | URL, Method, Headers, Body (JSON), Auth |\\n| `create_assessment` | Auto-create client assessment | Title prefix, auto-linked to company |\\n| `trigger_onboarding` | Start 8-phase onboarding | Assigned tech, Target completion days |\\n| `block_ip` | Security action | IP source, Duration, Target device, Approval |\\n| `create_contract_from_quote` | Generate MSA + contract docs | Auto-send for e-signature toggle |\\n| `stop_workflow` | Halt remaining actions | (No config \u2014 terminal action) |\\n\\n**Design Notes:**\\n- All panels are **310px wide** to match existing Orchestrator Studio layout\\n- Use existing `.orch-search-inp`, `.orch-dp-section-lbl` CSS patterns\\n- Color-coded action badges (`THEN`, `STOP`) indicate action type\\n- Info boxes explain auto-config actions (e.g., `create_assessment` auto-detects industry)\\n- Variable hints show available template variables (e.g., `{{company_name}}`, `{{deal_value}}`)\\n\\n---\\n\\n### 2. Frosted Black Matte (`hex-frosted-matte-sample.html`)\\n\\n**Purpose:** Demonstrates the **dark mode material system** with three texture intensity options.\\n\\n**Material Variants:**\\n\\n| Variant | Background | Noise Opacity | Use Case |\\n|---------|------------|---------------|----------|\\n| **Subtle Grain** (Option A) | `#0f1218` opaque | 3.5% | Default cards, stat displays |\\n| **Heavy Grain** (Option B) | `#0d1017` opaque | 5.5% | Tactical/security contexts |\\n| **Matte+Glass Hybrid** (Option C) | `rgba(12,15,22,0.92)` + `blur(8px)` | 4% | Best of both worlds |\\n\\n**Key Technique:**\\n```css\\n/* Noise grain = the matte texture */\\nbackground-image: url(\\\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256'...%3E\\\");\\nbackground-size: 180px 180px;\\nopacity: 0.035;\\nmix-blend-mode: overlay;\\n```\\n\\nThe SVG uses `` to generate fractal noise, creating a ceramic/brushed-metal feel without glossy reflections.\\n\\n**Components Shown:**\\n- Stat cards (4-column strip)\\n- Hex-cut cards with colored left borders\\n- Module cards with progress bars\\n- Matte buttons (no glass blur, grain texture instead)\\n- Tables with matte header styling\\n- Detail cards with sidebar\\n\\n---\\n\\n### 3. Frosted White Ceramic (`hex-frosted-white-ceramic-sample.html`)\\n\\n**Purpose:** Demonstrates the **light mode material system** \u2014 porcelain-like white surface with micro-grain texture.\\n\\n**Material Variants:**\\n\\n| Variant | Background | Grain Opacity | Vibe |\\n|---------|------------|---------------|------|\\n| **Cool White** | `#f8f9fc` | 2.5% | Clean, tech-forward |\\n| **Warm Ceramic** | `#faf9f7` | 2% | Approachable, premium |\\n\\n**Key Differences from Dark Mode:**\\n- **Opaque white base** (no translucency)\\n- **Soft diffused shadows** instead of hard edges\\n- **Kiln-glaze top edge** \u2014 subtle 1px inset highlight at top\\n- **Multiply blend mode** for grain (darker on light background)\\n- **Hex pattern very faint** (0.02 opacity vs. 0.4 in dark mode)\\n\\n**Components Shown:**\\n- Stat cards (cool vs. warm comparison)\\n- Hex-cut cards with colored left borders\\n- Module cards (Helpdesk, Security, Orchestrator)\\n- Ceramic buttons with action-colored borders\\n- Hex badges (status indicators)\\n- Tables with light-mode header styling\\n- Form inputs with hex-cut clip paths\\n- Detail card with ticket conversation thread\\n\\n---\\n\\n### 4. Glass Action Buttons (`hex-glass-buttons-sample.html`)\\n\\n**Purpose:** Comprehensive button style guide for both dark and light modes.\\n\\n**Button Anatomy:**\\n```\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502  [Icon] Label           \u2502  \u2190 Frosted glass body\\n\u2502                         \u2502     (backdrop-filter: blur)\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n  \u2191 Border color = action type\\n```\\n\\n**Action Color Mapping:**\\n\\n| Border Color | Semantic | Use |\\n|--------------|----------|-----|\\n| Blue (`#3b82f6`) | Primary | Create, add, confirm |\\n| Green (`#22c55e`) | Success | Approve, save, complete |\\n| Red (`#ef4444`) | Danger | Delete, remove, cancel |\\n| Amber (`#f59e0b`) | Warning | Escalate, alert, caution |\\n| Purple (`#a855f7`) | Automation | Workflow, orchestrate |\\n| Cyan (`#06b4d4`) | Info | Refresh, sync, details |\\n| Gray (`#94a3b8`) | Secondary | Cancel, dismiss, neutral |\\n\\n**Button Shapes:**\\n- **Hex-cut rect** \u2014 `polygon(8px 0, calc(100% - 8px) 0, ...)`\\n- **Hex diamond** \u2014 `polygon(10px 0, calc(100% - 10px) 0, 100% 50%, ...)`\\n- **Icon buttons** \u2014 38\u00d738px with hex-cut clip path\\n- **Button groups** \u2014 Connected buttons with shared edges\\n\\n**Dark Mode Styling:**\\n```css\\n.glass-btn {\\n  background: rgba(12, 16, 28, 0.6);\\n  backdrop-filter: blur(16px);\\n  box-shadow: inset 0 1px 0 rgba(255,255,255,0.04),\\n              0 2px 8px rgba(0,0,0,0.3);\\n}\\n.glass-btn:hover {\\n  background: rgba(18, 22, 38, 0.75);\\n  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06),\\n              0 4px 16px rgba(0,0,0,0.4);\\n  transform: translateY(-1px);\\n}\\n```\\n\\n**Light Mode Styling:**\\n```css\\n.glass-btn {\\n  background: rgba(255, 255, 255, 0.65);\\n  backdrop-filter: blur(16px);\\n  box-shadow: inset 0 1px 0 rgba(255,255,255,0.9),\\n              0 2px 8px rgba(0,0,0,0.06);\\n}\\n```\\n\\n---\\n\\n## Design System Patterns\\n\\n### Hex Clipping\\n\\nAll components use **hexagonal clip paths** for a cohesive, geometric aesthetic:\\n\\n```css\\n/* Hex-cut rectangle (8px corners) */\\nclip-path: polygon(8px 0, calc(100% - 8px) 0, 100% 8px, \\n                   100% calc(100% - 8px), calc(100% - 8px) 100%, \\n                   8px 100%, 0 calc(100% - 8px), 0 8px);\\n\\n/* Hex diamond (50% width) */\\nclip-path: polygon(10px 0, calc(100% - 10px) 0, 100% 50%, \\n                   calc(100% - 10px) 100%, 10px 100%, 0 50%);\\n```\\n\\n### Material Finishes\\n\\n| Material | Dark Mode | Light Mode | Texture | Blur |\\n|----------|-----------|-----------|---------|------|\\n| **Glass** | `rgba(8,12,20,0.7)` | `rgba(255,255,255,0.65)` | Hex pattern | `blur(16px)` |\\n| **Matte** | `#0f1218` opaque | `#f8f9fc` opaque | Noise grain | None |\\n| **Hybrid** | `rgba(12,15,22,0.92)` | `rgba(255,255,255,0.92)` | Noise + hex | `blur(8px)` |\\n\\n### Color Palette\\n\\n**Semantic Colors:**\\n- Primary: `#3b82f6` (blue)\\n- Success: `#22c55e` (green)\\n- Danger: `#ef4444` (red)\\n- Warning: `#f59e0b` (amber)\\n- Purple: `#a855f7`\\n- Cyan: `#06b4d4`\\n\\n**Neutral Grays (Dark Mode):**\\n- Text: `#e1e4eb`\\n- Secondary: `#94a3b8`\\n- Muted: `#64748b`\\n- Background: `#0a0e17`\\n\\n**Neutral Grays (Light Mode):**\\n- Text: `#1a1d2e`\\n- Secondary: `#64748b`\\n- Muted: `#94a3b8`\\n- Background: `#eef1f6`\\n\\n---\\n\\n## Usage &amp; Integration\\n\\n### For Designers\\n1. Open any `.html` file in a browser to preview components\\n2. Use as reference for Figma/design tool specifications\\n3. Test material finishes and button interactions in real browser\\n4. Compare dark vs. light mode side-by-side\\n\\n### For Developers\\n1. **Copy CSS patterns** from these samples into production stylesheets\\n2. **Reference variable names** (e.g., `glass-btn`, `ceramic-card`, `hex-cut-sm`)\\n3. **Adapt clip paths** for custom shapes\\n4. **Use SVG noise data URIs** for texture generation (no image files needed)\\n\\n### For Product/Stakeholders\\n1. Preview Orchestrator action panels before implementation\\n2. Evaluate material finishes (glass vs. matte) for brand fit\\n3. Review button styles and interaction feedback\\n4. Assess light/dark mode contrast and readability\\n\\n---\\n\\n## Technical Notes\\n\\n### SVG Noise Generation\\n\\nAll texture is generated via inline SVG with ``:\\n\\n```html\\n\\n  \\n    \\n  &lt;\\/filter&gt;\\n  \\n&lt;\\/svg&gt;\\n```\\n\\n**Parameters:**\\n- `baseFrequency`: Controls grain size (0.75\u20130.9 range)\\n- `numOctaves`: Adds detail layers (4\u20135 typical)\\n- `stitchTiles='stitch'`: Seamless tiling\\n\\nEncoded as **data URI** to avoid external image files.\\n\\n### Backdrop Filter Support\\n\\nGlass effects use `backdrop-filter: blur()` with `-webkit-` prefix for Safari:\\n\\n```css\\nbackdrop-filter: blur(16px);\\n-webkit-backdrop-filter: blur(16px);\\n```\\n\\n**Fallback:** Browsers without support show solid background color (graceful degradation).\\n\\n### Z-Index Layering\\n\\nTexture layers use `position: absolute` with `::before` and `::after` pseudo-elements:\\n\\n```css\\n.ceramic-card::before {\\n  /* Noise grain layer */\\n  opacity: 0.025;\\n  mix-blend-mode: multiply;\\n  z-index: 0;\\n}\\n.ceramic-card::after {\\n  /* Hex pattern layer */\\n  opacity: 0.02;\\n  z-index: 1;\\n}\\n/* Content sits at z-index: 10 */\\n```\\n\\nContent must have `position: relative; z-index: 10` to appear above textures.\\n\\n---\\n\\n## Browser Compatibility\\n\\n| Feature | Chrome | Firefox | Safari | Edge |\\n|---------|--------|---------|--------|------|\\n| `backdrop-filter` | \u2713 | \u2713 (88+) | \u2713 (9+) | \u2713 |\\n| `clip-path` (polygon) | \u2713 | \u2713 | \u2713 | \u2713 |\\n| SVG data URI | \u2713 | \u2713 | \u2713 | \u2713 |\\n| `mix-blend-mode` | \u2713 | \u2713 | \u2713 | \u2713 |\\n| CSS Grid | \u2713 | \u2713 | \u2713 | \u2713 |\\n\\n**Minimum versions:** Chrome 76+, Firefox 88+, Safari 9+, Edge 79+\\n\\n---\\n\\n## Future Enhancements\\n\\n- [ ] Interactive component library (Storybook integration)\\n- [ ] Dark/light mode toggle in samples\\n- [ ] Accessibility audit (WCAG 2.1 AA)\\n- [ ] Animation/transition specifications\\n- [ ] Responsive breakpoint examples\\n- [ ] Copy-to-clipboard CSS snippets\\n- [ ] Figma design tokens export\",\"docs\":\"# docs\\n\\n# docs Module\\n\\n## Purpose\\n\\nThe **docs** module is the authoritative reference layer for NexusOS PSA. It captures architecture, compliance evidence, feature specifications, integration roadmaps, and design artifacts\u2014everything needed to understand, build, operate, and audit the system without reading source code.\\n\\nThis is not executable code. Every document here is a **living artifact** that evolves alongside the codebase and is actively referenced by developers, operators, and compliance teams.\\n\\n## Core Subsystems\\n\\n### [Architecture](architecture.md)\\nDefines the multi-tenant, modular platform design. Explains how NexusOS runs as a single binary serving all tenants, with feature access controlled through database-driven licensing. Essential reading for understanding module boundaries and tenant isolation.\\n\\n### [Guide](guide.md)\\nFeature documentation and user manuals for end users and implementers. Covers workflows like NCSR intake, action planning, and other feature-specific procedures. Living documentation that evolves as features ship.\\n\\n### [Modules](modules.md)\\nA **documentation framework** that standardizes how each module in NexusOS is documented. Defines the four-document pattern (API.md, MANUAL.md, SOURCE_MAP.md, WHITEPAPER.md) that ensures consistency and discoverability across the codebase.\\n\\n### [Security](security.md)\\nSource-of-truth corpus mapping every security control to its implementation and test evidence. Serves prospects, auditors, engineers, and compliance teams. Rendered as an interactive dashboard with drill-down to code references.\\n\\n### [Integrations](integrations.md)\\nBacklog and planning reference for third-party vendor integrations. Captures API details, authentication requirements, and cross-module dependencies *before* implementation begins. Not a runtime component\u2014a staging ground for planned work.\\n\\n### [Mockups](mockups.md)\\nStatic HTML prototypes documenting proposed UI changes. Serves as design review artifact, implementation reference, and decision capture before engineering begins.\\n\\n### [UI Samples](ui-samples.md)\\nDesign system demonstrations (button styles, material finishes, component patterns) for NexusOS and Orchestrator Studio. Reference documents for designers and developers previewing visual patterns.\\n\\n## How They Work Together\\n\\n**Architecture** \u2192 **Modules** \u2192 **Guide/Security/Integrations**\\n\\nThe architecture document establishes the platform shape. The modules framework ensures every functional area (CRM, Helpdesk, Billing, RMM, Security, Compliance) documents itself consistently. Within each module, the guide provides user-facing workflows, security captures control evidence, and integrations tracks vendor work.\\n\\n**Mockups** and **UI Samples** feed into guide documentation\u2014visual specs become implementation references and eventually user manuals.\\n\\n## Key Workflows\\n\\n- **Onboarding a new contributor**: Start with [Architecture](architecture.md), then [Modules](modules.md) to understand the documentation pattern, then navigate to the specific module's guide.\\n- **Implementing a security control**: [Security](security.md) maps the control to code and tests; update the artifact when you modify security-critical code.\\n- **Planning a vendor integration**: [Integrations](integrations.md) captures the API spec and dependencies; use it to scope work before writing code.\\n- **Designing a feature**: [Mockups](mockups.md) and [UI Samples](ui-samples.md) establish visual specs; [Guide](guide.md) documents the resulting workflow.\",\"extensions\":\"# extensions\\n\\n# NexusOS Capture Extension\\n\\n## Overview\\n\\nNexusOS Capture is a Chrome extension that enables one-click product data extraction from any webpage. It scrapes product information (name, price, SKU, vendor, etc.), optionally enriches it with AI classification via Nexie, and pushes the result directly into NexusOS for inventory and QuickBooks integration.\\n\\nThe extension is built with vanilla JavaScript and zero dependencies, making it lightweight and CSP-compliant.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    A[\\\"Content Script(content.js)\\\"] --&gt;|scrape message| B[\\\"Background Worker(background.js)\\\"]\\n    B --&gt;|relay| C[\\\"Popup UI(popup.js)\\\"]\\n    C --&gt;|POST /products| D[\\\"NexusOS API\\\"]\\n    C --&gt;|POST /api/products/analyze| D\\n    D --&gt;|Nexie AI| E[\\\"Classification&amp; Enrichment\\\"]\\n    A --&gt;|DOM scraping| F[\\\"Current Page\\\"]\\n```\\n\\n## Components\\n\\n### 1. Content Script (`content.js`)\\n\\nRuns on every webpage and performs the core data extraction.\\n\\n**Key Function: `scrapePageData()`**\\n\\nExtracts product metadata from the DOM using a multi-layered selector strategy:\\n\\n- **Title**: Tries semantic HTML (`h1[itemprop=\\\"name\\\"]`), common product selectors (`.product-title`, `.pdp-title`), then falls back to `document.title`\\n- **Description**: Searches for `[itemprop=\\\"description\\\"]`, meta tags, and common description containers\\n- **Price**: Regex-based extraction from price elements, normalizes currency symbols\\n- **SKU/MPN**: Cleans up \\\"SKU: ABC123\\\" format, handles both generic and manufacturer-specific identifiers\\n- **Vendor (Brand)**: Multi-stage detection:\\n  1. Distributor-specific selectors (CDW, Ingram, etc.) if on a known distributor domain\\n  2. Standard brand selectors (`[itemprop=\\\"brand\\\"]`, `.brand-name`)\\n  3. Known brand matching against product title\\n  4. Domain-to-vendor mapping (e.g., `microsoft.com` \u2192 \\\"Microsoft\\\")\\n  5. Fallback: clean domain name\\n- **Supplier**: Maps domain to distributor name (CDW, Amazon Business, Newegg, etc.)\\n- **Image**: Prioritizes high-res sources, validates against placeholder patterns\\n- **Structured Data**: Parses JSON-LD `` blocks for rich product data\\n\\n**Site-Specific Overrides**\\n\\nAmazon and Newegg have custom extraction logic:\\n\\n- **Amazon**: Extracts ASIN from URL or detail table as SKU, handles Amazon's complex price selectors, extracts model number from detail bullets\\n- **Newegg**: Targets `.price-current` for accurate pricing\\n\\n**Message Handler**\\n\\n```javascript\\nchrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {\\n  if (request.action === 'scrape') {\\n    var data = scrapePageData();\\n    sendResponse(data);\\n  }\\n  return true; // Keep channel open for async response\\n});\\n```\\n\\n### 2. Background Service Worker (`background.js`)\\n\\nMinimal service worker that logs extension installation. Acts as a relay point for future background tasks (currently unused in active flow).\\n\\n### 3. Popup UI (`popup.html` + `popup.js`)\\n\\nThe user-facing interface displayed when the extension icon is clicked.\\n\\n**UI States**\\n\\n- **Loading**: \\\"Scanning page...\\\" while scraping\\n- **Empty**: \\\"No product data found\\\" if scrape returns no title\\n- **Product**: Full form with scraped data and editable fields\\n\\n**Key Functions**\\n\\n#### `scrapeCurrentPage()`\\nSends `{ action: 'scrape' }` message to content script, receives product data, populates form, and triggers Nexie analysis if configured.\\n\\n#### `populateForm(data)`\\nMaps scraped data to form fields:\\n- Display preview card (image, name, vendor, price)\\n- Populate editable inputs (name, vendor, supplier, SKU, MPN, price, description)\\n- Show category selector\\n\\n#### `analyzeWithNexie(name)`\\nPOST to `/api/products/analyze` with product name. Receives:\\n- AI-generated description\\n- Category classification\\n- Revenue type (MRR, ORR, NRR, product_sales)\\n- Billing type (monthly, annual, perpetual)\\n- Average cost estimate\\n- AI processing cost\\n\\nUpdates form fields and displays classification pills.\\n\\n#### `addToNexusOS()`\\nConstructs product object and POST to `/products` endpoint:\\n\\n```javascript\\n{\\n  name, vendor, category, price_monthly, price_yearly,\\n  unit_cost, billing_type, revenue_type, description,\\n  sku, manufacturer_part_number, default_supplier,\\n  vendor_url, image_url, is_active, ...\\n}\\n```\\n\\nHandles success/error states with visual feedback.\\n\\n#### Settings Management\\n- `loadSettings()`: Retrieves NexusOS URL and auth token from `chrome.storage.local`\\n- `saveSettings()`: Persists settings and validates connectivity\\n- `toggleSettings()`: Shows/hides settings panel\\n\\n**UI Styling**\\n\\nDark theme (Tailwind-inspired) with:\\n- Rose accent color (#f43f5e) for primary actions\\n- Classification pills (green for MRR, blue for ORR, amber for NRR)\\n- Responsive 380px popup width\\n\\n### 4. Manifest (`manifest.json`)\\n\\n**Permissions**\\n- `activeTab`: Access current tab for scraping\\n- `storage`: Persist NexusOS credentials\\n- `scripting`: Inject content script\\n\\n**Host Permissions**\\n- `https://*.horizonmanaged.com/*`: NexusOS API\\n- `http://localhost:3000/*`: Local development\\n\\n**Content Script Injection**\\n- Matches `` (runs on every page)\\n- Runs at `document_idle` (after page fully loads)\\n\\n## Data Flow\\n\\n```\\nUser clicks extension icon\\n    \u2193\\npopup.js calls scrapeCurrentPage()\\n    \u2193\\nchrome.tabs.sendMessage() \u2192 content.js\\n    \u2193\\nscrapePageData() extracts DOM data\\n    \u2193\\nsendResponse(data) \u2192 popup.js\\n    \u2193\\npopulateForm() displays preview &amp; editable fields\\n    \u2193\\n[Optional] analyzeWithNexie() enriches with AI\\n    \u2193\\nUser clicks \\\"Add to NexusOS\\\"\\n    \u2193\\nPOST /products with full product object\\n    \u2193\\nNexusOS creates product, syncs to QuickBooks\\n```\\n\\n## Scraping Strategy\\n\\nThe scraper uses a **fallback cascade** for each field:\\n\\n1. **Semantic HTML** (`itemprop` attributes) \u2014 most reliable\\n2. **Data attributes** (`data-testid`, `data-brand`) \u2014 framework-specific\\n3. **Common class patterns** (`.product-title`, `.price`, `.brand-name`)\\n4. **Structured data** (JSON-LD) \u2014 rich metadata\\n5. **Site-specific overrides** (Amazon ASIN, Newegg selectors)\\n6. **Heuristics** (brand matching from title, domain mapping)\\n7. **Fallback** (page title, domain name)\\n\\nThis approach handles:\\n- Vendor sites (Microsoft, Fortinet, Dell)\\n- Distributors (CDW, Ingram, Amazon Business)\\n- Marketplaces (Newegg, B&amp;H Photo)\\n- Generic e-commerce sites\\n\\n## Configuration\\n\\nSettings are stored in `chrome.storage.local`:\\n\\n```javascript\\n{\\n  nexusUrl: \\\"https://nexus.horizonmanaged.com\\\",\\n  nexusToken: \\\"eyJhbGc...\\\" // JWT from NexusOS\\n}\\n```\\n\\nThe extension displays connection status in the header:\\n- **Connected** (green) if both URL and token are set\\n- **Not configured** (red) if missing\\n\\n## Error Handling\\n\\n- **Scrape fails**: Shows empty state, suggests visiting a vendor product page\\n- **Settings missing**: Shows error when attempting to add product\\n- **API errors**: Displays HTTP status and error message\\n- **Image load fails**: Falls back to placeholder icon\\n- **JSON-LD parse errors**: Silently skipped, continues with DOM scraping\\n\\n## Security\\n\\n- **CSP-compliant**: No inline scripts, all event listeners in `popup.js`\\n- **Token storage**: Stored in `chrome.storage.local` (encrypted by browser)\\n- **HTTPS enforcement**: NexusOS API requires HTTPS (except localhost)\\n- **No external dependencies**: Reduces attack surface\\n\\n## Extension Lifecycle\\n\\n1. **Installation**: `chrome.runtime.onInstalled` logs to console\\n2. **Tab activation**: Content script auto-injects on all pages\\n3. **Icon click**: Popup opens, triggers `scrapeCurrentPage()`\\n4. **User action**: Form submission POSTs to NexusOS API\\n5. **Settings**: Persisted across browser sessions\\n\\n## Testing Checklist\\n\\n- [ ] Scrape product from vendor site (Microsoft, Fortinet)\\n- [ ] Scrape product from distributor (CDW, Amazon Business)\\n- [ ] Verify Amazon ASIN extraction\\n- [ ] Verify Newegg price extraction\\n- [ ] Test Nexie AI enrichment\\n- [ ] Test product creation in NexusOS\\n- [ ] Verify settings persistence\\n- [ ] Test error states (missing config, API failure)\\n- [ ] Verify image loading and fallback\\n\\n## Future Enhancements\\n\\n- Batch product import from CSV\\n- Competitor price tracking\\n- Automatic QuickBooks account mapping\\n- Browser history scanning for bulk capture\\n- Webhook notifications on product creation\",\"internal-ai\":\"# internal \u2014 ai\\n\\n# internal/ai Module\\n\\nThe `internal/ai` module provides AI-powered capabilities for NexusOS PSA using Anthropic's Claude API. It implements a multi-tenant provider pattern where each tenant configures their own Anthropic API key in Settings &gt; Integrations, enabling features across helpdesk automation, knowledge base generation, security analysis, and compliance workflows.\\n\\n## Overview\\n\\nThe module is split into two files:\\n\\n- **claude.go** \u2014 Core Claude API integration, tenant-aware API key management, and all AI feature implementations\\n- **redact.go** \u2014 Audit-mandated prompt sanitization (audit finding C9, 2026-04-29) that strips sensitive identifiers before they leave the platform\\n\\nEvery public API call flows through the redaction layer unless explicitly opted out with a `// redact:keep` comment at the call site.\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    A[\\\"Public API Methods(GenerateArticle, SuggestTicketResponse, etc.)\\\"]\\n    B[\\\"getAPIKey(tenant settings lookup)\\\"]\\n    C[\\\"callClaudeTracked(with usage logging)\\\"]\\n    D[\\\"callClaudeInternal(core API call)\\\"]\\n    E[\\\"Redact(C9 sanitization)\\\"]\\n    F[\\\"logUsage(async to ai_usage_log)\\\"]\\n    G[\\\"Claude APIv1/messages\\\"]\\n    \\n    A --&gt; B\\n    A --&gt; C\\n    C --&gt; D\\n    C --&gt; F\\n    D --&gt; E\\n    D --&gt; G\\n    \\n    style E fill:#f9f,stroke:#333\\n    style F fill:#bbf,stroke:#333\\n```\\n\\n## Core Components\\n\\n### Provider\\n\\nThe `Provider` struct is the main entry point. It holds a database connection pool and HTTP client:\\n\\n```go\\ntype Provider struct {\\n    pool       *pgxpool.Pool\\n    httpClient *http.Client\\n}\\n```\\n\\nCreate one with `NewProvider(pool)`. The provider is instantiated once at startup and reused across all requests.\\n\\n### API Key Management\\n\\nEach tenant stores their Anthropic API key in the `tenants` table's `settings` JSONB column under the key `ai_api_key`. The `getAPIKey(ctx, tenantID)` method retrieves and validates it:\\n\\n- Returns a descriptive error if the key is missing or empty\\n- Errors guide users to Settings &gt; Integrations to configure the key\\n- Called by every public method before making API requests\\n\\n### Claude API Integration\\n\\nThe module uses Claude Sonnet 4 (`claude-sonnet-4-20250514`) with a 300-second timeout. Three internal methods handle API communication:\\n\\n**`callClaudeInternal(ctx, apiKey, system, userMessage, maxTokens)`**\\n- Core method that makes the HTTP request to `https://api.anthropic.com/v1/messages`\\n- Applies redaction to both system and user messages\\n- Implements exponential backoff retry logic for 429 (rate limit) and 529 (overloaded) responses\\n- Returns text content, input tokens, and output tokens\\n- Parses JSON responses and extracts the first text content block\\n\\n**`callClaude(ctx, apiKey, system, userMessage, maxTokens)`**\\n- Wrapper around `callClaudeInternal` that returns only the text (discards token counts)\\n- Used when usage logging is not needed\\n\\n**`callClaudeTracked(ctx, apiKey, system, userMessage, maxTokens, meta)`**\\n- Wrapper that calls `callClaudeInternal` and asynchronously logs usage via `logUsage()`\\n- Used by all public methods that have tenant context\\n\\n### Usage Logging\\n\\nThe `logUsage(meta, model, inputTokens, outputTokens)` method runs asynchronously in a goroutine:\\n\\n- Inserts a row into `ai_usage_log` with tenant ID, company ID (optional), action type, token counts, and estimated cost\\n- Calculates cost using Sonnet pricing: $3 per million input tokens, $15 per million output tokens\\n- Logs errors but doesn't block the caller\\n- Called by `callClaudeTracked()` and `CallClaudeRawWithCost()`\\n\\nThe `UsageMeta` struct carries context for logging:\\n\\n```go\\ntype UsageMeta struct {\\n    TenantID  string // required\\n    CompanyID string // optional, may be empty\\n    Action    string // e.g., \\\"kb_generation\\\", \\\"ticket_suggestion\\\"\\n}\\n```\\n\\n### Redaction Layer (Audit C9)\\n\\nEvery prompt sent to Claude is sanitized by `Redact(s)` unless the call site explicitly opts out. The redactor strips:\\n\\n- **Email addresses** \u2192 `[REDACTED_EMAIL]`\\n- **Public IPv4 addresses** \u2192 `[REDACTED_IP]` (RFC1918, loopback, link-local preserved as MSP-internal context)\\n- **Public-looking hostnames** \u2192 `[REDACTED_HOST]` (*.local, *.lan, *.internal, *.corp preserved)\\n\\nThe redaction is intentionally conservative \u2014 false-redacts are acceptable (the prompt still reads), false-keeps are not. Employee name redaction requires tenant-scoped name lists and is applied by `RedactWithNames()` at call sites with tenant context.\\n\\nRedaction happens in `callClaudeInternal()` before the request leaves the platform \u2014 this is the single chokepoint for all public API calls.\\n\\n## Feature Categories\\n\\n### Knowledge Base Generation\\n\\n**`GenerateArticleForTenant(ctx, tenantID, prompt)`**\\n\\nGenerates a complete KB article from a natural language prompt. Returns title, markdown content, and comma-separated tags. The system prompt instructs Claude to act as a technical writer for MSPs and respond with structured JSON.\\n\\nThe method parses Claude's JSON response (handling markdown code block wrapping) and falls back to using the raw response as content if JSON parsing fails.\\n\\n### Ticket Assistance\\n\\n**`SuggestTicketResponse(ctx, tenantID, title, description, notes)`**\\n\\nDrafts a professional helpdesk response based on ticket context and conversation history. Useful for technicians to reply faster.\\n\\n**`ClassifyTicket(ctx, tenantID, title, description)`**\\n\\nAnalyzes a ticket and suggests priority (low/medium/high/critical), type (incident/service_request/problem/change), and team assignment. Returns a map of classification suggestions.\\n\\n**`SummarizeTicket(ctx, tenantID, title, description, notes)`**\\n\\nGenerates a concise summary suitable for handoffs and escalations. Includes what the issue is, what's been tried, and next steps.\\n\\n**`SummarizePeek(ctx, tenantID, system, userMessage, maxTokens)`**\\n\\nRuns a single Claude call with caller-supplied prompts and returns the summary text plus estimated cost in dollars. Backs the Peek's lazy Nexie summary card. Logs usage as `peek_summary`.\\n\\n### Ticket Solution &amp; NexusBOT\\n\\n**`GenerateSolution(ctx, tenantID, title, description, agentOS, notes)`**\\n\\nAnalyzes a ticket and produces a preliminary AI solution. If the problem can be resolved via CLI commands, it also generates an executable script suitable for NexusBOT remote execution on the target OS (bash for Linux/macOS, PowerShell for Windows).\\n\\nReturns a `SolutionResult` with:\\n- `Analysis` \u2014 brief root cause assessment\\n- `Solution` \u2014 step-by-step markdown resolution\\n- `Scriptable` \u2014 whether the fix can be automated\\n- `Script` \u2014 executable bash or PowerShell code (if scriptable)\\n- `KBTitle` and `KBTags` \u2014 suggested KB article metadata\\n\\n**`ValidateScript(ctx, tenantID, script, scriptType, context)`**\\n\\nReviews an AI-generated script for safety before execution. Evaluates for destructive side effects, idempotency, scope, and error handling. Returns a boolean safety verdict and assessment text.\\n\\n### Compliance &amp; Gap Analysis\\n\\n**`AnalyzeComplianceGaps(ctx, tenantID, frameworkName, documentTitle, documentText, controls)`**\\n\\nCompares uploaded document text against framework controls (e.g., HIPAA, SOC 2, NIST) and returns a gap analysis with:\\n- Overall compliance score (0-100)\\n- List of gaps with severity, description, and remediation steps\\n- List of controls that are adequately covered with evidence\\n- Top-level recommendations\\n\\nTruncates input at ~30K characters to stay within token limits.\\n\\n### Ticket Resolution &amp; Auto-Resolve\\n\\n**`GenerateResolution(ctx, tenantID, input)`**\\n\\nAccepts a `ResolutionInput` containing ticket metadata, conversation notes, remote session data, and call transcripts. Generates a `ResolutionResult` with:\\n- `Summary` \u2014 2-3 sentence resolution summary\\n- `RootCause` \u2014 what caused the issue\\n- `StepsTaken` \u2014 numbered markdown list of actions performed\\n- `Recommendations` \u2014 follow-up suggestions to prevent recurrence\\n\\nThe method only references actions evidenced in the data \u2014 it never fabricates steps. Session metadata is used to infer what was likely done based on ticket context.\\n\\n### Nexie Security Wizard\\n\\n**`GenerateScanProfile(ctx, tenantID, input)`**\\n\\nAnalyzes a client's environment (industry, compliance requirements, RMM inventory, scan history) and builds a custom vulnerability scan profile. Returns a `NexieWizardResult` with:\\n- Profile name and description\\n- Risk summary and recommended scan frequency\\n- List of Metasploit scanner modules to enable (with priority and rationale)\\n- Warnings for security gaps\\n\\nThe method validates that all recommended modules exist in the available list before returning.\\n\\n### Nexie Traffic Analysis\\n\\n**`AnalyzeTraffic(ctx, tenantID, trafficJSON)`**\\n\\nSends parsed network traffic data to Claude for anomaly detection and threat analysis. Returns a `NexieTrafficAnalysis` with:\\n- Risk score (0-100)\\n- Summary of findings\\n- List of findings with severity, title, description, evidence, and MITRE ATT&amp;CK IDs\\n- Traffic profile classification (normal/suspicious/malicious) with notable hosts and domains\\n\\n**`GenerateRemediation(ctx, tenantID, finding, analysisContext)`**\\n\\nCreates a structured remediation plan for a specific security finding. Returns a `NexieRemediationPlan` with:\\n- Summary of the plan\\n- List of executable actions (block_ip, create_deny_policy, create_address_object, enable_ips_signature)\\n- Pre-remediation posture assessment (current risk, exposed assets, attack surface, expected impact)\\n\\nAll object names are prefixed with `nexie-` for audit traceability.\\n\\n**`GeneratePostureReport(ctx, tenantID, preSnapshot, postContext)`**\\n\\nGenerates a security posture comparison report after remediation. Returns a JSON string with posture change assessment, risk delta, summary, remediations effective, remaining risks, and metrics.\\n\\n### Email Security Triage\\n\\n**`AnalyzeEmailThreat(ctx, tenantID, companyID, emailJSON)`**\\n\\nAnalyzes emails flagged as potentially suspicious by automated systems. Only called for caution-band emails (score 21-50) to minimize API costs. Returns an `EmailSecurityVerdict` with:\\n- `Verdict` \u2014 safe, suspicious, or malicious\\n- `Confidence` \u2014 0.0 to 1.0\\n- `Reasoning` \u2014 2-3 sentence explanation\\n- `Indicators` \u2014 list of specific flags found\\n\\nThe system prompt emphasizes that AI-generated phishing is common, perfect grammar doesn't mean legitimate, and urgency + action required + unusual sender = likely phishing.\\n\\n### Multimodal Support\\n\\n**`CallClaudeMultimodal(ctx, apiKey, system, contentBlocks, maxTokens, meta)`**\\n\\nSends multimodal requests (images, audio) to Claude. Accepts a list of `ContentBlock` structs with type (text/image), text content, or base64-encoded media data. Returns text response and estimated cost.\\n\\nRedacts text portions of multimodal payloads but passes image blocks through untouched (vision content is not text-scannable).\\n\\n### Usage Statistics\\n\\n**`GetUsageStats(ctx, tenantID)`**\\n\\nRetrieves aggregated AI usage statistics for a tenant:\\n- All-time totals (calls, input/output tokens, estimated cost)\\n- This month's usage\\n- Breakdown by action type\\n- Daily usage for the last 30 days (for charting)\\n\\nReturns a `UsageStats` struct with all aggregated data.\\n\\n### Connection Testing\\n\\n**`TestConnection(ctx, tenantID)`**\\n\\nVerifies the API key is valid by sending a minimal request to Claude. Returns the model name on success or an error if the key is invalid or missing.\\n\\n## Public API Methods\\n\\nThe module exports several methods for use by other packages:\\n\\n- `GetAPIKey(ctx, tenantID)` \u2014 retrieve the tenant's API key\\n- `CallClaudeRaw(ctx, apiKey, system, userMessage, maxTokens)` \u2014 call Claude without logging\\n- `CallClaudeRawTracked(ctx, apiKey, system, userMessage, maxTokens, meta)` \u2014 call Claude with logging\\n- `CallClaudeRawWithCost(ctx, apiKey, system, userMessage, maxTokens, meta)` \u2014 call Claude, log, and return estimated cost\\n\\nThese are used by call sites that have their own tenant context and want to make raw Claude calls without going through the feature-specific methods.\\n\\n## Integration Points\\n\\nThe module is used across the codebase for:\\n\\n- **Helpdesk** \u2014 ticket suggestions, classifications, summaries, resolutions\\n- **Knowledge Base** \u2014 article generation\\n- **Security** \u2014 Nexie traffic analysis, remediation planning, email triage\\n- **Compliance** \u2014 gap analysis against frameworks\\n- **Bidding Engine** \u2014 scope summaries, spec extraction, coaching generation\\n- **Assessment** \u2014 document analysis, transcription, image analysis\\n- **Dispatch** \u2014 AI-powered suggestions\\n\\nAll callers must provide a `tenantID` so the provider can look up the tenant's API key. Most callers also provide a `UsageMeta` struct to tag usage for cost attribution and analytics.\\n\\n## Error Handling\\n\\n- Missing or empty API key: returns a descriptive error directing users to Settings &gt; Integrations\\n- API errors: returns the HTTP status code and error message from Claude\\n- Rate limiting (429, 529): retries with exponential backoff (10s, 20s, 30s)\\n- JSON parsing failures: falls back to using raw response text as content\\n- Database errors: returns wrapped errors with context\\n\\n## Cost Tracking\\n\\nThe module tracks estimated costs using Sonnet pricing:\\n- Input: $3 per million tokens\\n- Output: $15 per million tokens\\n\\nCosts are calculated and logged to `ai_usage_log` for every tracked call. The `GetUsageStats()` method aggregates costs by tenant, action, and time period for billing and analytics.\\n\\n## Security Considerations\\n\\n1. **Redaction (C9)** \u2014 All prompts are sanitized before leaving the platform unless explicitly opted out\\n2. **API Key Storage** \u2014 Keys are stored in tenant settings JSONB; access is tenant-scoped\\n3. **Script Validation** \u2014 Generated scripts are reviewed for safety before execution\\n4. **Audit Trail** \u2014 All usage is logged with tenant, action, and cost for compliance\\n5. **Timeout** \u2014 HTTP client has a 300-second timeout to prevent hanging requests\",\"internal-assessment\":\"# internal \u2014 assessment\\n\\n# Assessment Module\\n\\nThe assessment module implements the discovery and analysis phase of the NexusOS sales lifecycle. It bridges CRM deals and billing quotes by ingesting discovery materials (documents, audio, images), running AI-driven analysis via Claude, and producing a **Client Operations Blueprint** \u2014 a comprehensive operational plan covering hardware standards, software stack, security posture, compliance requirements, and scheduled operations.\\n\\n## Overview\\n\\nAn assessment captures a prospective client's current state through uploaded discovery materials and structured intake data (industry, employee count, pain points, budget). The module then:\\n\\n1. **Extracts text** from diverse document formats (PDF, DOCX, audio, images)\\n2. **Runs AI analysis** using Claude with industry intelligence and product catalog context\\n3. **Produces structured output** (risk assessment, recommended stack, compliance requirements, management plan)\\n4. **Creates a blueprint** \u2014 a persistent operational reference for the client\\n5. **Converts to quotes** \u2014 transforms the recommended stack into draft sales quotes\\n6. **Manages approval gates** \u2014 enforces workflow transitions with optional approvals\\n7. **Schedules operations** \u2014 creates recurring security and maintenance tasks from the blueprint\\n\\n### Status Lifecycle\\n\\n```\\ndraft \u2192 analyzing \u2192 reviewed \u2192 approved \u2192 converted\\n                \u2193\\n              (failure)\\n```\\n\\n- **draft**: Initial creation, documents being uploaded\\n- **analyzing**: AI analysis in progress\\n- **reviewed**: Analysis complete, awaiting human review and edits\\n- **approved**: Approved by authorized user, blueprint locked, scheduled operations created\\n- **converted**: Converted to a quote, assessment archived\\n\\n## Core Components\\n\\n### Handler (`handler.go`)\\n\\nThe HTTP request handler for all assessment endpoints. Manages CRUD operations, document uploads, AI analysis triggers, and status transitions.\\n\\n**Key methods:**\\n\\n- `handleList()` \u2014 Lists assessments grouped by status (kanban view)\\n- `handleDetail()` \u2014 Loads full assessment with documents and audit log\\n- `handleCreate()` / `handleUpdate()` \u2014 CRUD for assessment metadata\\n- `handleRunAnalysis()` \u2014 Triggers AI analysis pipeline\\n- `handleApprove()` / `handleRevokeApproval()` \u2014 Status transitions with approval gates\\n- `handleConvertToQuote()` \u2014 Creates draft quote from approved assessment\\n- `handleUploadDocument()` / `handleDeleteDocument()` \u2014 Document lifecycle\\n\\n**Callbacks:**\\n\\n```go\\nOnAssessmentCompleted func(ctx context.Context, tenantID, assessmentID, companyID, dealID string)\\nOnAssessmentApproved func(ctx context.Context, tenantID, assessmentID, companyID, dealID string)\\n```\\n\\nThese are called by external modules (e.g., deals, onboarding) to react to assessment state changes.\\n\\n### AI Analysis Pipeline (`analyzer.go`)\\n\\nOrchestrates the full analysis flow: loads assessment context, extracts document text, fetches industry intelligence and product catalog, builds prompts, calls Claude, and parses structured JSON response.\\n\\n**Entry point:**\\n\\n```go\\nfunc (h *Handler) runAnalysis(ctx context.Context, tenantID, assessmentID string) (*AIAnalysisResult, float64, error)\\n```\\n\\n**Flow:**\\n\\n1. Load assessment metadata (industry, employee count, pain points, budget, notes)\\n2. Load all processed documents' extracted text\\n3. Load industry intelligence (compliance requirements, technology standards, risk factors)\\n4. Load product catalog (available products with pricing)\\n5. Build system prompt (Nexie persona, industry context, product catalog)\\n6. Build user prompt (client context + discovery documents)\\n7. Call Claude with 8192 token budget\\n8. Parse JSON response into `AIAnalysisResult`\\n9. Store results in database\\n10. Create/update client blueprint\\n\\n**Prompt construction:**\\n\\n- `buildAssessmentSystemPrompt()` \u2014 Defines Nexie's role, output format, and critical rules\\n- `buildAssessmentUserPrompt()` \u2014 Contextualizes the client and discovery materials\\n\\nThe system prompt enforces:\\n- Authoritative use of industry intelligence as ground truth\\n- Product matching by ID from catalog\\n- Risk scores (0 = perfect, 100 = critical)\\n- Specific, document-backed findings\\n- Comprehensive security and operational coverage\\n\\n### Document Processing (`document_processor.go`)\\n\\nExtracts text from uploaded files asynchronously. Supports multiple formats with format-specific extraction logic.\\n\\n**Supported formats:**\\n\\n| Format | Handler | Notes |\\n|--------|---------|-------|\\n| `.txt`, `.md`, `.csv` | `extractTextFile()` | Direct file read |\\n| `.docx` | `extractDOCX()` \u2192 `extractTextFromWordXML()` | ZIP archive, XML parsing |\\n| `.pdf` | `extractPDFBasic()` | Naive text extraction from PDF operators; scanned PDFs need OCR (Phase 6) |\\n| Audio (`.mp3`, `.wav`, `.m4a`, `.ogg`) | `transcribeAudio()` | Base64-encoded, sent to Claude with media type |\\n| Images (`.png`, `.jpg`, `.jpeg`) | `analyzeImage()` | Base64-encoded, Claude vision analysis |\\n| Video (`.mp4`, `.webm`) | Placeholder | Requires ffmpeg for audio extraction (Phase 6) |\\n\\n**Processing flow:**\\n\\n```\\nhandleUploadDocument()\\n  \u2193\\nSave file to disk (collision-safe filename)\\n  \u2193\\nInsert record with status='pending'\\n  \u2193\\nprocessDocument() [async goroutine]\\n  \u251c\u2500 Detect content type\\n  \u251c\u2500 Extract text (format-specific)\\n  \u251c\u2500 Update assessment_documents.extracted_text\\n  \u2514\u2500 Set status='complete' or 'failed'\\n```\\n\\n**Multimodal support:**\\n\\nAudio and image processing use Claude's multimodal API:\\n\\n```go\\nblocks := []ai.ContentBlock{\\n    {Type: \\\"text\\\", Text: \\\"...instructions...\\\"},\\n    {Type: \\\"image\\\", Source: &amp;ai.ContentBlockSource{\\n        Type: \\\"base64\\\",\\n        MediaType: \\\"audio/mpeg\\\",\\n        Data: base64EncodedData,\\n    }},\\n}\\nh.ai.CallClaudeMultimodal(ctx, apiKey, systemPrompt, blocks, ...)\\n```\\n\\n### Blueprint Builder (`blueprint_builder.go`)\\n\\nCreates or updates a persistent `client_blueprints` record from AI analysis results. The blueprint is the living operational reference for a client \u2014 it evolves as the assessment is refined.\\n\\n**Key method:**\\n\\n```go\\nfunc (h *Handler) createOrUpdateBlueprint(ctx context.Context, tenantID, assessmentID, companyID string, result *AIAnalysisResult) (string, error)\\n```\\n\\n**Upsert logic:**\\n\\n- Inserts new blueprint if none exists for the company\\n- Updates existing blueprint if one exists (increments version)\\n- Stores all AI outputs as JSONB columns\\n- Links blueprint back to assessment\\n\\n**Blueprint contents:**\\n\\n- Hardware standards (per role: executive, standard user, server, network)\\n- Software stack (per-user or per-device licensing)\\n- Security stack (EDR, email security, DNS filtering, SIEM, MFA, training)\\n- Compliance frameworks (HIPAA, PCI-DSS, CMMC, NIST CSF, SOC2, etc.)\\n- Management tier (fully_managed, co_managed, break_fix)\\n- Scheduled operations (vulnerability scans, patch cycles, backup verification, etc.)\\n- Contract design (term, SLAs, billing, escalation tiers)\\n\\n### Quote Conversion (`quote_converter.go`)\\n\\nTransforms an approved assessment's recommended stack into a draft quote with line items.\\n\\n**Entry point:**\\n\\n```go\\nfunc (h *Handler) handleConvertToQuote(w http.ResponseWriter, r *http.Request)\\n```\\n\\n**Process:**\\n\\n1. Load assessment (must be approved or reviewed)\\n2. Parse recommended stack JSON\\n3. Calculate subtotal from unit prices \u00d7 quantities\\n4. Create quote record with metadata (company, contact, payment terms)\\n5. Insert line items grouped by category (Managed Services, Security, Backup, etc.)\\n6. Link quote back to assessment and deal\\n7. Redirect to quote editor for review\\n\\n**Stack editing:**\\n\\n- `handleUpdateStack()` \u2014 Bulk update recommended stack\\n- `handleRemoveStackItem()` \u2014 Remove single item by index\\n\\nBoth update the assessment and linked blueprint, with audit logging.\\n\\n### Scheduled Operations (`scheduled_ops.go`)\\n\\nCreates recurring security and maintenance tasks from a blueprint's scheduled operations.\\n\\n**Entry point:**\\n\\n```go\\nfunc (h *Handler) createScheduledOps(ctx context.Context, tenantID, companyID, assessmentID string) error\\n```\\n\\n**Called when:**\\n- Assessment is approved\\n- Onboarding starts for a client\\n\\n**Operation types:**\\n\\n| Type | Auto-Approve | Purpose |\\n|------|--------------|---------|\\n| `vuln_scan` | \u2713 | Vulnerability scanning |\\n| `pen_test` | \u2717 | Penetration testing (requires approval) |\\n| `patch_cycle` | \u2713 | Patch management |\\n| `pcap_collection` | \u2713 | Network packet capture |\\n| `hardware_recon` | \u2713 | Hardware inventory |\\n| `backup_verification` | \u2713 | Backup integrity checks |\\n\\n**Cadence calculation:**\\n\\n`calculateNextRun()` computes the next execution time based on:\\n- Cadence (daily, weekly, monthly, quarterly, annually)\\n- Day of week (for weekly)\\n- Time of day (HH:MM, defaults to 2:00 AM)\\n\\nExample: Weekly vulnerability scan on Monday at 3:00 AM calculates the next Monday \u2265 now at 03:00 UTC.\\n\\n### Approval Gates (`approval_gate.go`)\\n\\nEnforces workflow transitions with optional approval requirements.\\n\\n**Entry point:**\\n\\n```go\\nfunc (h *Handler) checkApprovalGate(ctx context.Context, tenantID, entityType, stageFrom, stageTo, entityID, entityTitle string) bool\\n```\\n\\n**Returns:**\\n- `true` \u2014 Gate exists, transition blocked, notification sent\\n- `false` \u2014 No gate, proceed\\n\\n**Notification routing:**\\n\\n- If `approver_user` is set: notify specific user\\n- If `approver_role` is set: notify all admins\\n\\nUsed by other modules (deals, quotes) to enforce approval workflows.\\n\\n## Data Model\\n\\n### Assessment\\n\\n```go\\ntype Assessment struct {\\n    ID              string     // UUID\\n    TenantID        string     // Multi-tenancy\\n    DealID          string     // Link to CRM deal\\n    CompanyID       string     // Link to company\\n    ContactID       string     // Link to contact\\n    Title           string\\n    Status          string     // draft, analyzing, reviewed, approved, converted\\n    \\n    // Client context\\n    Industry        string\\n    VerticalMarket  string\\n    EmployeeCount   int\\n    LocationCount   int\\n    CurrentMSP      string\\n    PainPoints      string\\n    BudgetRange     string\\n    Timeline        string\\n    Notes           string\\n    \\n    // AI outputs (stored as JSONB)\\n    AISummary           string\\n    AIRiskScore         int\\n    AIRiskAssessment    string  // AIRiskAssessment JSON\\n    AIRecommendedStack  string  // []AIStackItem JSON\\n    AIComplianceReqs    string  // []AIComplianceReq JSON\\n    AIManagementPlan    string  // AIManagementPlan JSON\\n    AIContractDesign    string  // AIContractDesign JSON\\n    AIHardwareStandards string  // []AIHardwareStd JSON\\n    AISoftwareStack     string  // []AISoftwareItem JSON\\n    AISecurityStack     string  // []AISecurityItem JSON\\n    AIScheduledOps      string  // []AIScheduledOp JSON\\n    AICost              float64 // Claude API cost\\n    \\n    // Linkages\\n    QuoteID     string\\n    BlueprintID string\\n    ApprovedBy  string\\n    ApprovedAt  *time.Time\\n    ConvertedAt *time.Time\\n    CreatedBy   string\\n    CreatedAt   time.Time\\n    UpdatedAt   time.Time\\n}\\n```\\n\\n### Document\\n\\n```go\\ntype Document struct {\\n    ID               string    // UUID\\n    AssessmentID     string\\n    TenantID         string\\n    Filename         string    // Collision-safe (hex prefix)\\n    OriginalName     string    // User-provided name\\n    ContentType      string    // MIME type\\n    FileSize         int64\\n    FilePath         string    // Disk path\\n    ExtractedText    string    // Full extracted text\\n    ProcessingStatus string    // pending, processing, complete, failed\\n    ProcessingError  string\\n    UploadedBy       string    // User UUID\\n    CreatedAt        time.Time\\n}\\n```\\n\\n### AI Analysis Result\\n\\n```go\\ntype AIAnalysisResult struct {\\n    Summary           string\\n    RiskAssessment    AIRiskAssessment\\n    RecommendedStack  []AIStackItem\\n    HardwareStandards []AIHardwareStd\\n    SoftwareStack     []AISoftwareItem\\n    SecurityStack     []AISecurityItem\\n    ComplianceReqs    []AIComplianceReq\\n    ManagementPlan    AIManagementPlan\\n    ScheduledOps      []AIScheduledOp\\n    ContractDesign    AIContractDesign\\n}\\n```\\n\\nSee `types.go` for full struct definitions.\\n\\n## Integration Points\\n\\n### Incoming Dependencies\\n\\n- **`internal/auth`** \u2014 Tenant and user context extraction\\n- **`internal/ai`** \u2014 Claude API calls (text, multimodal, cost tracking)\\n- **`internal/notify`** \u2014 Notifications for approvals, analysis completion\\n- **`internal/ui`** \u2014 HTML template rendering\\n\\n### Outgoing Dependencies\\n\\n- **`internal/deals`** \u2014 Assessment linked to deal; callbacks on completion/approval\\n- **`internal/quotes`** \u2014 Assessment converted to quote\\n- **`internal/onboarding`** \u2014 Blueprint triggers onboarding workflow\\n- **`internal/scheduling`** \u2014 Scheduled operations created from blueprint\\n\\n### Callbacks\\n\\nExternal modules register callbacks to react to assessment state changes:\\n\\n```go\\nhandler.OnAssessmentCompleted = func(ctx context.Context, tenantID, assessmentID, companyID, dealID string) {\\n    // e.g., deals module updates deal status\\n}\\n\\nhandler.OnAssessmentApproved = func(ctx context.Context, tenantID, assessmentID, companyID, dealID string) {\\n    // e.g., onboarding module starts client setup\\n}\\n```\\n\\n## Workflow Example\\n\\n```\\n1. Sales rep creates assessment in CRM deal\\n   \u2192 Assessment created in draft status\\n\\n2. Rep uploads discovery documents (PDF, DOCX, audio, images)\\n   \u2192 Documents processed asynchronously, text extracted\\n\\n3. Rep clicks \\\"Run Analysis\\\"\\n   \u2192 Assessment status \u2192 analyzing\\n   \u2192 AI analysis pipeline runs:\\n      - Loads industry intelligence (compliance, tech standards, risks)\\n      - Loads product catalog\\n      - Builds prompts with client context + documents\\n      - Calls Claude (8192 tokens)\\n      - Parses JSON response\\n   \u2192 Results stored in assessment\\n   \u2192 Client blueprint created/updated\\n   \u2192 Status \u2192 reviewed\\n   \u2192 Admins notified\\n\\n4. Admin reviews analysis, edits recommended stack if needed\\n   \u2192 Can remove items, adjust quantities/prices\\n\\n5. Admin clicks \\\"Approve\\\"\\n   \u2192 Approval gate checked (may require additional approval)\\n   \u2192 Status \u2192 approved\\n   \u2192 Scheduled operations created from blueprint\\n   \u2192 OnAssessmentApproved callback fired\\n\\n6. Sales rep clicks \\\"Convert to Quote\\\"\\n   \u2192 Draft quote created with line items from stack\\n   \u2192 Quote linked to assessment and deal\\n   \u2192 Status \u2192 converted\\n   \u2192 Rep redirected to quote editor for final review\\n\\n7. Quote sent to customer, deal progresses\\n```\\n\\n## Key Design Decisions\\n\\n### Async Document Processing\\n\\nDocument text extraction runs in background goroutines to avoid blocking HTTP responses. Status is tracked in `assessment_documents.processing_status`.\\n\\n### Industry Intelligence as Ground Truth\\n\\nThe system prompt explicitly treats industry compliance requirements as authoritative. If an industry mandates HIPAA, the AI includes it regardless of discovery materials.\\n\\n### Product Catalog Matching\\n\\nThe AI is instructed to match products by ID from the tenant's product catalog. This ensures recommended stack items are real, priced products.\\n\\n### Blueprint Versioning\\n\\nBlueprints are versioned and updated whenever an assessment is re-analyzed. This creates an audit trail of operational plan evolution.\\n\\n### Approval Gates\\n\\nApproval gates are configurable per tenant and transition type. They can require approval from a specific user or role before status changes.\\n\\n### Scheduled Operations Auto-Approval\\n\\nMost operations (scans, patches, backups) auto-approve. Invasive operations (pen tests) require manual approval.\\n\\n## Error Handling\\n\\n- **Missing AI provider** \u2014 Returns error, assessment reverts to draft\\n- **No processed documents** \u2014 Analysis fails with clear message\\n- **JSON parse errors** \u2014 Logs raw response (first 200 chars) for debugging\\n- **Document extraction failures** \u2014 Marked as failed, error message stored\\n- **Approval gate failures** \u2014 Blocks transition, logs gate name\\n\\n## Performance Considerations\\n\\n- **Document processing** \u2014 Async, non-blocking\\n- **AI analysis** \u2014 Single Claude call per assessment (8192 tokens)\\n- **Database queries** \u2014 Indexed on tenant_id, assessment_id, company_id\\n- **File storage** \u2014 Collision-safe filenames, organized by tenant/assessment\\n- **Audit logging** \u2014 Lightweight JSON details, not full diffs\\n\\n## Testing Hooks\\n\\n- `OnAssessmentCompleted` and `OnAssessmentApproved` callbacks allow test modules to verify state transitions\\n- Document processing can be tested with mock files in various formats\\n- AI analysis can be tested with mock Claude responses (via `internal/ai` test doubles)\",\"internal-auth\":\"# internal \u2014 auth\\n\\n# internal/auth Module\\n\\nThe `internal/auth` module implements authentication and authorization for NexusOS PSA. It handles local password login, JWT token management, multi-factor authentication (TOTP), OAuth2/OIDC single sign-on, and session management.\\n\\n## Overview\\n\\nAuthentication in NexusOS follows a multi-stage flow:\\n\\n1. **Credential validation** \u2014 email + password checked against bcrypt hash\\n2. **MFA challenge** (if enabled) \u2014 user provides TOTP code or backup code\\n3. **Token issuance** \u2014 short-lived JWT access token + long-lived refresh token cookie\\n4. **Token refresh** \u2014 when access token expires, refresh token exchanges for a new pair\\n5. **Request authentication** \u2014 middleware validates JWT and injects claims into context\\n\\nThe module is tenant-aware: all user lookups and token operations are scoped to the tenant, preventing cross-tenant access.\\n\\n## Core Components\\n\\n### Handler (`handler.go`)\\n\\nThe `Handler` struct coordinates HTTP endpoints for login, logout, token refresh, MFA verification, and session management. It depends on:\\n\\n- `*pgxpool.Pool` \u2014 database access\\n- `*JWTManager` \u2014 token signing/verification\\n- `*SessionManager` \u2014 refresh token storage\\n- `*MFAManager` \u2014 TOTP and backup code handling\\n\\n**Key endpoints:**\\n\\n| Endpoint | Method | Purpose |\\n|----------|--------|---------|\\n| `/auth/login` | POST | Authenticate with email + password |\\n| `/auth/mfa/verify` | POST | Complete login with TOTP code |\\n| `/auth/refresh` | POST | Exchange refresh token for new access token |\\n| `/auth/logout` | POST | Revoke refresh token and clear cookies |\\n| `/auth/me` | GET | Return authenticated user's profile |\\n| `/api/auth/sessions` | GET | List active sessions for current user |\\n| `/api/auth/sessions/{id}` | DELETE | Revoke a specific session |\\n| `/api/auth/login-history` | GET | Audit trail of login events |\\n| `/api/auth/mfa/enroll` | POST | Start MFA enrollment (requires auth) |\\n| `/api/auth/mfa/confirm` | POST | Activate MFA with TOTP code |\\n| `/api/auth/mfa/disable` | POST | Disabled \u2014 admins only |\\n\\n**Login flow:**\\n\\n```\\nhandleLogin\\n  \u251c\u2500 Parse email + password (JSON or form)\\n  \u251c\u2500 Query user by email (tenant-scoped)\\n  \u251c\u2500 Validate password with bcrypt\\n  \u251c\u2500 Check if account is active\\n  \u251c\u2500 If MFA enabled: issue MFA challenge token, redirect to /login?mfa=true\\n  \u251c\u2500 If tenant requires MFA but user hasn't set it up: issue tokens + redirect to /settings/mfa-setup\\n  \u2514\u2500 Otherwise: issue tokens + redirect to dashboard\\n```\\n\\n**Token issuance:**\\n\\nThe `issueTokens` method:\\n1. Loads user permissions from the database\\n2. Signs an access token (8-hour TTL, embedded claims)\\n3. Creates a refresh token (7-day TTL, stored as SHA-256 hash)\\n4. Sets both as HTTP-only cookies\\n5. Updates `last_login_at` timestamp\\n6. Redirects to dashboard (or returns JSON for API clients)\\n\\nAccess tokens are embedded in the JWT claims so permission checks don't require database queries. Refresh tokens are server-side, enabling revocation and session listing.\\n\\n### JWT Manager (`jwt.go`)\\n\\nThe `JWTManager` uses RS256 (RSA + SHA-256) for token signing. The private key is loaded from disk at startup and validated for secure file permissions (mode 0o600).\\n\\n**Token types:**\\n\\n- **Access tokens** \u2014 8-hour TTL, carry user identity + permissions, used for API requests\\n- **MFA challenge tokens** \u2014 5-minute TTL, prove password was verified but MFA is pending\\n- **Portal tokens** \u2014 4-hour TTL, for client-facing portal (different issuer: `\\\"nexusos-portal\\\"`)\\n\\n**Claims structure:**\\n\\n```go\\ntype Claims struct {\\n    TenantID    string   // Tenant scope\\n    UserID      string   // User UUID\\n    Email       string   // Login email\\n    FullName    string   // Display name\\n    Role        string   // Role name (for quick RBAC)\\n    Permissions []string // Embedded permission list\\n    UITheme     string   // \\\"dark\\\" or \\\"light\\\"\\n}\\n```\\n\\nThe `Issuer` field distinguishes token types:\\n- `\\\"nexusos-psa\\\"` \u2014 MSP user access tokens\\n- `\\\"nexusos-portal\\\"` \u2014 client portal tokens\\n\\nThis prevents a stolen portal token from being replayed against MSP routes.\\n\\n### Session Manager (`session.go`)\\n\\nThe `SessionManager` handles refresh token lifecycle:\\n\\n- **CreateRefreshToken** \u2014 generates 32-byte random token, stores SHA-256 hash in database\\n- **ValidateRefreshToken** \u2014 checks hash against database, deletes token on use (rotation)\\n- **RevokeToken** \u2014 deletes a specific token (logout)\\n- **RevokeUserSessions** \u2014 deletes all tokens for a user (sign out everywhere)\\n- **CleanExpired** \u2014 removes expired tokens (called hourly)\\n\\nRefresh tokens are never stored plaintext \u2014 only the hash. The plaintext is sent to the client as an HTTP-only cookie.\\n\\n### MFA Manager (`mfa.go`)\\n\\nThe `MFAManager` implements TOTP-based multi-factor authentication (RFC 6238):\\n\\n- **BeginEnrollment** \u2014 generates TOTP secret + 10 backup codes, stores secret in database\\n- **ConfirmEnrollment** \u2014 validates first TOTP code, activates MFA\\n- **VerifyTOTP** \u2014 checks TOTP code during login\\n- **VerifyBackupCode** \u2014 checks backup code (single-use), removes it from database\\n- **DisableMFA** \u2014 clears MFA from user account (admin-only)\\n\\nBackup codes are bcrypt-hashed before storage. Each code can only be used once.\\n\\n### Middleware (`middleware.go`)\\n\\nThe `RequireAuth` middleware validates JWT access tokens from the `Authorization` header (or `access_token` cookie for HTMX requests). On success, it stores claims in the request context.\\n\\n**Context helpers:**\\n\\n- `ClaimsFromContext(ctx)` \u2014 retrieve authenticated user's claims\\n- `TenantIDFromContext(ctx)` \u2014 extract tenant ID\\n- `UserIDFromContext(ctx)` \u2014 extract user ID\\n- `HasPermission(ctx, required)` \u2014 check if user has permission (supports wildcards)\\n- `InjectUserData(ctx, data)` \u2014 populate template data with user info\\n\\n**Permission matching:**\\n\\n```go\\nHasPermission(ctx, \\\"tickets:read\\\")\\n// Matches if user has:\\n// - \\\"*\\\" (admin)\\n// - \\\"tickets:read\\\" (exact)\\n// - \\\"tickets:*\\\" (resource wildcard)\\n```\\n\\n### Password Management (`password.go`)\\n\\nThe `ValidatePassword` function enforces a unified password policy across the application:\\n\\n- Minimum 12 characters\\n- At least one uppercase letter\\n- At least one lowercase letter\\n- At least one digit\\n- At least one symbol\\n\\nPasswords are hashed with bcrypt (cost 12, ~250ms per hash). All password creation/change operations must call `ValidatePassword` before `HashPassword` to ensure consistency.\\n\\n### SSO (`sso.go`)\\n\\nThe `SSOManager` implements OAuth2/OIDC single sign-on for Microsoft 365, Google Workspace, and generic OIDC providers.\\n\\n**SSO flow:**\\n\\n```\\nUser clicks \\\"Sign in with [Provider]\\\"\\n  \u2193\\nhandleAuthorize\\n  \u251c\u2500 Generate PKCE code verifier + challenge\\n  \u251c\u2500 Generate state with CSRF nonce\\n  \u251c\u2500 Store verifier + nonce in HTTP-only cookie\\n  \u2514\u2500 Redirect to provider's authorize endpoint\\n  \u2193\\nProvider authenticates user, redirects to /auth/sso/callback\\n  \u2193\\nhandleCallback\\n  \u251c\u2500 Verify state parameter (CSRF check)\\n  \u251c\u2500 Exchange auth code for tokens (with PKCE verifier)\\n  \u251c\u2500 Extract user info from userinfo endpoint\\n  \u251c\u2500 Check allowed email domains\\n  \u251c\u2500 Look up existing SSO link\\n  \u251c\u2500 If no link and auto_provision enabled: create user + link\\n  \u2514\u2500 Issue JWT tokens + redirect to dashboard\\n```\\n\\n**Security:**\\n\\n- PKCE (S256) prevents authorization code interception\\n- State parameter includes CSRF nonce\\n- Allowed domains restrict which email addresses can log in\\n- Auto-provisioning creates users with a default role (configurable per provider)\\n\\n## Tenant Isolation\\n\\nAll authentication operations are tenant-scoped:\\n\\n- User lookups filter by `tenant_id`\\n- Token claims include `TenantID`\\n- Refresh tokens are scoped to (user, tenant)\\n- MFA operations query by (user, tenant)\\n- SSO links are scoped to (tenant, provider, external_id)\\n\\nThis prevents a user from one tenant accessing another tenant's data, even with a valid token.\\n\\n## Audit Logging\\n\\nThe `logLoginAudit` method records all authentication events to the `login_audit` table:\\n\\n- `login_success` \u2014 successful password authentication\\n- `login_failed` \u2014 invalid password, disabled account, SSO-only, etc.\\n- `mfa_challenge` \u2014 MFA code requested\\n- `mfa_success` \u2014 MFA code verified\\n- `mfa_failed` \u2014 invalid MFA code\\n- `token_refresh` \u2014 refresh token used\\n- `logout` \u2014 user logged out\\n\\nEach audit entry includes:\\n- User ID + email\\n- Client IP (respecting X-Forwarded-For header)\\n- User-Agent\\n- Failure reason (if applicable)\\n- Timestamp\\n\\n## Integration Points\\n\\n### Incoming Dependencies\\n\\n- **cmd/psa/main.go** \u2014 initializes `Handler`, `JWTManager`, `SessionManager`, registers routes\\n- **internal/portal** \u2014 uses `SignPortalToken` and `VerifyPortalToken` for client portal\\n- **internal/rmm** \u2014 uses `ClaimsFromContext` to identify authenticated agents\\n- **internal/helpdesk, internal/crm, internal/billing** \u2014 use `ClaimsFromContext` for tenant scoping\\n\\n### Outgoing Dependencies\\n\\n- **pgxpool.Pool** \u2014 all database operations\\n- **golang.org/x/crypto/bcrypt** \u2014 password hashing\\n- **github.com/golang-jwt/jwt/v5** \u2014 JWT signing/verification\\n- **github.com/pquerna/otp** \u2014 TOTP generation and validation\\n- **golang.org/x/oauth2** \u2014 OAuth2 token exchange\\n\\n## Security Considerations\\n\\n### Audit Findings\\n\\nSeveral security improvements are documented in the code:\\n\\n- **L1** \u2014 JWT private key file must have mode 0o600 (no group/world bits)\\n- **C8** \u2014 Access token is HttpOnly to prevent XSS theft (still works with WebSocket upgrades)\\n- **M2** \u2014 Refresh token validation checks user still belongs to the same tenant\\n- **M3** \u2014 Password policy is centralized in `ValidatePassword` to prevent drift\\n\\n### Token Handling\\n\\n- Access tokens are short-lived (8 hours) to limit exposure window\\n- Refresh tokens are long-lived (7 days) but server-side, enabling revocation\\n- Tokens are rotated on refresh \u2014 old refresh tokens are deleted\\n- MFA challenge tokens are very short-lived (5 minutes)\\n- Portal tokens use a different issuer to prevent cross-audience replay\\n\\n### Cookie Security\\n\\n- All auth cookies are `HttpOnly` (JavaScript cannot read them)\\n- All auth cookies are `Secure` (HTTPS only)\\n- Refresh token cookie is `SameSite=Lax` (allows OAuth redirects)\\n- Access token cookie is `SameSite=Lax` (allows HTMX requests)\\n- MFA challenge cookie is `SameSite=Strict` (no cross-site requests)\\n\\n## Usage Examples\\n\\n### Protecting a Route\\n\\n```go\\n// In main.go, wrap handler with auth middleware\\nmux.Handle(\\\"GET /api/tickets\\\", \\n    auth.RequireAuth(jwtMgr)(\\n        http.HandlerFunc(handleListTickets)))\\n```\\n\\n### Accessing User Info in a Handler\\n\\n```go\\nfunc handleListTickets(w http.ResponseWriter, r *http.Request) {\\n    claims := auth.ClaimsFromContext(r.Context())\\n    if claims == nil {\\n        http.Error(w, \\\"unauthorized\\\", http.StatusUnauthorized)\\n        return\\n    }\\n    \\n    // Scope query to user's tenant\\n    rows, _ := pool.Query(r.Context(),\\n        \\\"SELECT * FROM tickets WHERE tenant_id = $1\\\",\\n        claims.TenantID)\\n}\\n```\\n\\n### Checking Permissions\\n\\n```go\\nfunc handleDeleteTicket(w http.ResponseWriter, r *http.Request) {\\n    if !auth.HasPermission(r.Context(), \\\"tickets:delete\\\") {\\n        http.Error(w, \\\"forbidden\\\", http.StatusForbidden)\\n        return\\n    }\\n    // ... delete ticket\\n}\\n```\\n\\n### Creating a User with Password\\n\\n```go\\npassword := \\\"MyP@ssw0rd123\\\"\\nif err := auth.ValidatePassword(password); err != nil {\\n    http.Error(w, err.Error(), http.StatusBadRequest)\\n    return\\n}\\n\\nhash, _ := auth.HashPassword(password)\\npool.Exec(ctx, \\\"INSERT INTO users (email, password_hash) VALUES ($1, $2)\\\",\\n    email, hash)\\n```\\n\\n### Enabling MFA for a User\\n\\n```go\\n// Start enrollment\\nresult, _ := mfaMgr.BeginEnrollment(ctx, tenantID, userID, email)\\n// Return QR code URL + backup codes to client\\n\\n// After user scans QR code and provides TOTP code\\nerr := mfaMgr.ConfirmEnrollment(ctx, tenantID, userID, totpCode)\\n// MFA is now active\\n```\",\"internal-bidengine\":\"# internal \u2014 bidengine\\n\\n# Bid Engine Module\\n\\nThe **bidengine** module is the core calculation and estimation engine for NexusOS. It transforms construction drawings, specifications, and addendums into detailed line-item bids with automatic cost rollup, AI-driven takeoff, and pre-submission audit checks.\\n\\n## Overview\\n\\nThe module handles the complete bid lifecycle:\\n\\n1. **Document ingestion** \u2014 upload specs, drawings, and addendums\\n2. **AI-powered takeoff** \u2014 extract line items from drawings and specs\\n3. **Addendum reconciliation** \u2014 apply scope changes from addendums\\n4. **Cost calculation** \u2014 compute material, labor, burden, margins, and grand total\\n5. **Pre-submission audit** \u2014 flag OFCI items, trade violations, missing items, and duplicate labor\\n6. **Revision tracking** \u2014 maintain bid history and outcomes\\n\\nThe calculation engine (`Calculate`) is a pure function that matches the Excel-based DCRUA Controls Upgrade BID worksheet exactly, making it fully testable and deterministic.\\n\\n## Core Concepts\\n\\n### Bid Sheet\\n\\nA `BidSheet` is the top-level container for a single estimate. It holds:\\n- **Line items** \u2014 individual material, labor, equipment, or subcontractor rows\\n- **Settings** \u2014 labor rate, burden %, overhead %, profit %, contingency %, commission %, sales tax %, material tax %\\n- **Metadata** \u2014 trade, site location, customer, status (draft/submitted/won/lost)\\n- **Addendum state** \u2014 pending changes from uploaded addendums, applied changes history\\n- **Audit state** \u2014 findings and acknowledgments\\n\\n### Line Items\\n\\nEach line item represents a discrete cost component:\\n\\n```go\\ntype BidLineItem struct {\\n    ID                 string  // UUID\\n    Description        string  // \\\"Cat6 cable, 1000 ft\\\" or \\\"Camera installation labor\\\"\\n    Qty                float64 // quantity (feet, units, hours, etc.)\\n    MaterialEach       float64 // cost per unit\\n    ManHoursEach       float64 // labor hours per unit\\n    Trade              string  // \\\"cabling\\\", \\\"electrical\\\", \\\"security\\\", etc.\\n    SectionType        string  // \\\"materials\\\", \\\"labor\\\", \\\"equipment_rental\\\", \\\"subcontractor\\\"\\n    PartNumber         string  // optional vendor SKU\\n    ScopeIn            bool    // included in bid totals?\\n    OFCIFlag           bool    // owner-furnished, contractor-installed?\\n    AddendumSourceDocID string // which addendum modified this line?\\n    Confidence         float64 // AI confidence (0\u20131) for generated lines\\n}\\n```\\n\\nLine items are routed by `SectionType` during calculation:\\n- **materials** \u2192 material total + tax\\n- **labor** \u2192 labor total + burden\\n- **equipment_rental** \u2192 equipment total\\n- **subcontractor** \u2192 subcontractor total\\n\\n### Calculation Chain\\n\\nThe `Calculate` function implements the cost model:\\n\\n```\\nMaterialTotal + TaxOnMaterials + Subs + Equipment + Labor + Burden = HardCost\\nHardCost \u00d7 (Overhead% + Profit% + Contingency%) = Margins\\n(HardCost + Margins) \u00d7 Commission% + SalesTax% + Permits + Bond = GrandTotal\\n```\\n\\nEach step is deterministic and fully reversible. The engine supports per-item labor rates (for section-specific rates) and falls back to the bid-level default.\\n\\n## Key Components\\n\\n### 1. Addendum Processing (`addendum_apply.go`, `addendum_split.go`)\\n\\n**Purpose:** Reconcile scope changes from pre-bid addendums into the current line items.\\n\\n**Flow:**\\n\\n1. **AI diff generation** (`handleApplyAddendums` \u2192 `runAddendumApply`)\\n   - Gather all unprocessed addendums + current line items + current Scope of Work\\n   - Send to Claude with a specialized system prompt that recognizes patterns:\\n     - \\\"Hardware furnished by others\\\" \u2192 zero-out material\\n     - \\\"Terminations field-side only\\\" \u2192 reduce labor by ~25\u201340%\\n     - \\\"Cable only, no install\\\" \u2192 keep cable, remove labor\\n     - Quantity changes, part substitutions, section deletions\\n   - AI returns a JSON array of proposed changes (delete/modify/add)\\n\\n2. **Change storage** (`bid_addendum_changes` table)\\n   - Each proposed change is stored as pending with:\\n     - Action (delete/modify/add)\\n     - Line item ID (if modifying/deleting)\\n     - Payload (new values for modify/add)\\n     - Scope exclusion (contract-grade bullet for the SoW)\\n     - Rationale (why the change was proposed)\\n\\n3. **Estimator review** (UI)\\n   - Estimator sees pending changes in the bid detail page\\n   - Can Apply (execute the change) or Dismiss (ignore it)\\n   - Resolve-All applies all pending changes at once\\n\\n4. **Application** (`applyOneAddendumChange`)\\n   - Delete: remove the line item\\n   - Modify: overlay payload fields, recalculate totals\\n   - Add: insert new line item with calculated totals\\n   - Append scope exclusion to `ai_scope_summary` under \\\"Addendum-driven Exclusions\\\"\\n\\n**Drawing supersession** (`addendum_split.go`)\\n- When an addendum PDF is uploaded, every page is mirrored as a drawing candidate\\n- If a page's sheet number matches an existing base drawing, the old drawing is marked superseded\\n- Superseded drawings are auto-excluded from takeoff so the addendum sheet takes over\\n\\n### 2. Exclusion Extraction (`exclusion_extractor.go`)\\n\\n**Purpose:** Pre-scan specs and addendums for textual evidence that items are owner-furnished, by-others, or out of scope.\\n\\n**Patterns matched:**\\n- Legend definitions: \\\"OFCI = OWNER FURNISHED, CONTRACTOR INSTALLED\\\"\\n- Direct exclusions: \\\"provided by owner\\\", \\\"by others\\\", \\\"by electrical contractor\\\"\\n- N.I.C. (Not In Contract)\\n\\n**Output:** `ExclusionReport` with:\\n- `Hits` \u2014 individual evidence snippets with context\\n- `LegendDefinitions` \u2014 standardized definitions found\\n- `SuggestedExcludeItems` \u2014 deduped list of nouns (cameras, IDF racks, etc.)\\n\\n**Integration:** The report is rendered as a prompt block injected into the AI takeoff prompt so the AI knows \\\"cameras are OFCI \u2014 don't bill the device.\\\"\\n\\n### 3. Pre-Submission Audit (`audit.go`, `audit_handler.go`, `audit_resolve.go`, `audit_view.go`)\\n\\n**Purpose:** Catch common bid errors before submission (phantom OFCI hardware, double-counted labor, missing items).\\n\\n**Checks:**\\n\\n| Check | Severity | Action |\\n|-------|----------|--------|\\n| **OFCI Candidates** | Red | Flags items matching trade profile OFCI defaults but not marked as OFCI |\\n| **Trade Profile Violations** | Red | Flags items in the trade's EXCLUDE list (likely another contractor's scope) |\\n| **Duplicate Labor** | Yellow | Flags pairs of labor lines sharing activity keywords (e.g., \\\"camera install\\\" + \\\"camera mount\\\") |\\n| **Low Confidence** | Yellow | Flags AI-generated lines with confidence &lt; 0.7 |\\n| **Required Items** | Yellow/Info | Flags missing permits, as-builts, testing, mobilization, fiber backbone, bond |\\n| **Quantity Sanity** | Info | Warns on unusual quantities (&gt;500 drops, &gt;60 cameras) |\\n\\n**Acknowledgment system** (`bid_audit_acks` table):\\n- Whole-finding ignore: suppress a check entirely for this bid\\n- Per-line ignore: suppress a check for specific line items\\n- Stale acks: automatically invalidated when an addendum modifies the line (snapshot of `addendum_source_doc_id` at ignore time)\\n\\n**Resolution:**\\n- Auto-resolvable findings (OFCI, trade violations) can be bulk-resolved with one click\\n- Resolve-All applies all auto-resolvable findings of a given severity\\n- Suggested items (permits, fiber, bond) can be added with one click\\n\\n**Blocking:** Red findings block status change to submitted; yellow/info are warnings.\\n\\n### 4. Drawing Discipline Detection (`drawing_discipline.go`)\\n\\n**Purpose:** Auto-include only drawing sheets relevant to the bid's trade scope.\\n\\n**Mechanism:**\\n- Extract sheet number from each PDF page (e.g., \\\"E-201\\\" \u2192 discipline \\\"E\\\")\\n- Map disciplines to trades:\\n  - E, EE, ED, T, TC, LV, V \u2192 cabling/security/AV\\n  - FA \u2192 fire alarm\\n  - A, AD, AS \u2192 always included (architectural context)\\n- Auto-include pages matching the bid's trades; others default to not-included (estimator opts in)\\n\\n**Text extraction:** `ExtractPageText` runs pdftotext on each page and stores the result in `drawings.extracted_text` so title-block notes, legends, and schedules reach the AI prompt (Vision sees devices but not text).\\n\\n### 5. Calculation Engine (`engine.go`)\\n\\n**Pure function:** `Calculate(BidInput) \u2192 BidResult`\\n\\n**Inputs:**\\n- Line items (qty, material_each, man_hours_each, labor_rate per item)\\n- Subcontractors and equipment (legacy support)\\n- Percentages (burden, overhead, profit, contingency, commission, sales tax, material tax)\\n- Permits and bond (fixed amounts)\\n- Optional manual bid price override\\n\\n**Outputs:**\\n- All intermediate totals (material, labor, burden, hard cost, margins, subtotal, commission, sales tax, grand total)\\n- Effective bid price (manual override or calculated)\\n- Profit per man-day and profit margin %\\n\\n**Rounding:** All money fields rounded to 2 decimal places.\\n\\n**Test coverage:** Three real bids from the Excel workbook (Controls Per Site, Frontend Server, SLA Contract) verify exact match.\\n\\n## HTTP Handlers\\n\\n### Bid Management\\n- `GET /bids` \u2014 list all bids\\n- `GET /bids/{id}` \u2014 detail view with stepper, line items, addendum changes, audit findings\\n- `POST /bids` \u2014 create new bid\\n- `PATCH /bids/{id}` \u2014 update bid metadata (trade, site, customer, settings)\\n- `POST /bids/{id}/revisions` \u2014 create a revision (snapshot for history)\\n\\n### Line Items\\n- `POST /bids/{id}/line-items` \u2014 add line item\\n- `PATCH /bids/{id}/line-items/{itemId}` \u2014 update line item\\n- `DELETE /bids/{id}/line-items/{itemId}` \u2014 delete line item\\n- `PATCH /bids/{id}/line-items/{itemId}/scope` \u2014 toggle scope_in / ofci_flag / exclusion_source\\n\\n### Addendum Processing\\n- `POST /bids/{id}/apply-addendums` \u2014 run AI diff on all unprocessed addendums\\n- `GET /bids/{id}/addendum-diff-status` \u2014 poll status (elapsed time, pending count, error)\\n- `POST /bids/{id}/addendum-changes/{cid}/apply` \u2014 apply single change\\n- `POST /bids/{id}/addendum-changes/{cid}/dismiss` \u2014 dismiss single change\\n- `POST /bids/{id}/addendum-changes/apply-all` \u2014 apply all pending changes\\n- `POST /bids/{id}/addendum-changes/dismiss-all` \u2014 dismiss all pending changes\\n\\n### Audit\\n- `GET /api/bids/{id}/audit` \u2014 run audit, return JSON report\\n- `GET /bids/{id}/audit` \u2014 render audit page (HTML)\\n- `POST /api/bids/{id}/audit/resolve` \u2014 apply canonical fix to a finding\\n- `POST /api/bids/{id}/audit/resolve-all` \u2014 apply all auto-resolvable findings of a severity\\n- `POST /api/bids/{id}/audit/ignore` \u2014 acknowledge a finding (whole or per-line)\\n- `POST /api/bids/{id}/audit/add-line-item` \u2014 add suggested line item + ack the finding\\n\\n### Drawings &amp; Specs\\n- `GET /bids/{id}/drawings` \u2014 drawing selection page (Vision progress, discipline legend, opt-in checkboxes)\\n- `POST /bids/{id}/drawings/remirror` \u2014 re-extract pages from a spec PDF as drawing candidates\\n- `PATCH /bids/{id}/drawings/{drawingId}/include` \u2014 toggle included_in_takeoff\\n- `POST /bids/{id}/run-vision` \u2014 trigger Vision on selected drawings\\n- `GET /bids/{id}/vision-progress` \u2014 poll Vision status\\n\\n### Processing\\n- `POST /bids/{id}/process` \u2014 dispatch to AI takeoff (drawings-primary or generate-from-spec)\\n- `GET /bids/{id}/processing-status` \u2014 poll takeoff progress\\n\\n### Outcomes\\n- `POST /bids/{id}/capture-outcome` \u2014 record bid result (won/lost/withdrawn) + actual cost\\n\\n## Data Model\\n\\n### Core Tables\\n\\n**bid_sheets**\\n- id, tenant_id, trade, site_state, customer, status (draft/submitted/won/lost)\\n- grand_total, effective_bid_price, manual_bid_price\\n- labor_rate, burden_pct, overhead_pct, profit_pct, contingency_pct, commission_pct, sales_tax_pct, material_tax_pct\\n- permits, bond\\n- ai_scope_summary (generated Scope of Work narrative)\\n- addendum_diff_status, addendum_diff_started_at, addendum_diff_finished_at, addendum_diff_error\\n\\n**bid_line_items**\\n- id, bid_sheet_id, tenant_id\\n- description, qty, material_each, material_total, man_hours_each, labor_total, line_total\\n- trade, section_type, part_number, notes\\n- scope_in, ofci_flag, exclusion_source\\n- confidence (AI confidence for generated lines)\\n- addendum_source_doc_id (which addendum modified this line?)\\n- created_at, updated_at\\n\\n**bid_addendum_changes**\\n- id, bid_sheet_id, tenant_id\\n- line_item_id (nullable; null for \\\"add\\\" actions)\\n- action (delete/modify/add)\\n- rationale, addendum_source, addendum_doc_id\\n- scope_exclusion (contract-grade bullet for SoW)\\n- payload (JSON: new values for modify/add)\\n- status (pending/applied/dismissed)\\n- ai_cost (per-row cost allocation)\\n- applied_at, decided_by_user_id, decided_at\\n\\n**bid_audit_acks**\\n- tenant_id, bid_sheet_id, check_id\\n- line_item_id (nullable; null = whole-finding ignore)\\n- addendum_doc_at_ignore (snapshot of line's addendum_source_doc_id at ignore time; used to invalidate stale acks)\\n- ignored_by, ignored_at, note\\n\\n**bid_spec_documents**\\n- id, bid_sheet_id, tenant_id\\n- file_name, document_type (spec/scope_of_work/addendum/drawings)\\n- extracted_text (OCR/pdftotext output)\\n- processing_status (pending/complete/failed)\\n\\n**drawings**\\n- id, bid_id, tenant_id, spec_document_id\\n- page_number, sheet_number, discipline\\n- title, extracted_text\\n- included_in_takeoff, takeoff_status (pending/complete/failed)\\n- superseded_by_drawing_id (when an addendum sheet replaces a base drawing)\\n\\n**bid_audit_acks** (migration 234)\\n- Tracks acknowledgments of audit findings\\n- Supports whole-finding and per-line ignores\\n- Stale acks auto-invalidate when addendum modifies the line\\n\\n## Integration Points\\n\\n### AI Services (`internal/ai`)\\n- `CallClaudeRawWithCost` \u2014 used by addendum apply, takeoff, scope summary, reconciliation\\n- `GetAPIKey` \u2014 retrieve tenant's API key for Claude\\n\\n### Auth (`internal/auth`)\\n- `TenantIDFromContext`, `UserIDFromContext` \u2014 extract from request context\\n- All handlers validate tenant isolation\\n\\n### UI (`internal/ui`)\\n- `Render` \u2014 server-side template rendering (bid detail, audit page, drawing selection)\\n- `IsHTMXContent` \u2014 detect HTMX requests for partial updates\\n\\n### Database (`pgx`)\\n- All data persisted to PostgreSQL\\n- Transactions used for multi-step operations (e.g., apply addendum change + update SoW)\\n\\n## Workflow Example: Addendum Processing\\n\\n```\\n1. Estimator uploads Addendum 3 (PDF)\\n   \u2192 bid_spec_documents row created with document_type='addendum'\\n   \u2192 pdftotext extracts text \u2192 extracted_text populated\\n   \u2192 splitAddendumPagesIntoDrawings mirrors each page as drawing candidate\\n\\n2. Estimator clicks \\\"Apply Addendums\\\"\\n   \u2192 handleApplyAddendums marks bid as addendum_diff_status='running'\\n   \u2192 runAddendumApply gathers unprocessed addendums + current line items + SoW\\n   \u2192 AI prompt includes:\\n      - System prompt (recognize patterns: OFCI, field-side terminations, etc.)\\n      - Current line items (id, trade, description, qty, material, labor)\\n      - Current SoW narrative\\n      - Addendum text\\n   \u2192 AI returns JSON array of proposed changes\\n   \u2192 Each change inserted into bid_addendum_changes with status='pending'\\n\\n3. Estimator reviews pending changes in bid detail page\\n   \u2192 Sees rationale, scope_exclusion, current line item values\\n   \u2192 Clicks \\\"Apply\\\" on change for \\\"Terminations field-side only\\\"\\n      \u2192 applyOneAddendumChange executes the modify action\\n      \u2192 Payload overlays onto line item (man_hours_each reduced by 25%)\\n      \u2192 Totals recalculated\\n      \u2192 scope_exclusion appended to ai_scope_summary\\n      \u2192 Change marked status='applied'\\n\\n4. Estimator clicks \\\"Apply All\\\" for remaining changes\\n   \u2192 All pending changes applied in order\\n   \u2192 Bid totals recalculated once at the end\\n\\n5. Estimator runs Pre-Submission Audit\\n   \u2192 Audit sees addendum_source_doc_id on modified lines\\n   \u2192 Acks from prior runs are checked: if line's current addendum_source_doc_id\\n     differs from snapshot at ignore time, the ack is stale and the finding resurfaces\\n```\\n\\n## Workflow Example: Pre-Submission Audit\\n\\n```\\n1. Estimator clicks \\\"Run Audit\\\"\\n   \u2192 RunAudit loads bid + trade profile + exclusion report + prior acks\\n   \u2192 Runs 6 checks:\\n      - OFCI candidates (description matches profile defaults, not flagged)\\n      - Trade violations (description matches profile excludes)\\n      - Duplicate labor (pairs of lines with overlapping activity keywords)\\n      - Low confidence (AI lines with confidence &lt; 0.7)\\n      - Required items (missing permits, as-builts, testing, fiber, bond)\\n      - Quantity sanity (unusual drop/camera counts)\\n\\n2. Audit returns report with findings\\n   \u2192 Red findings (OFCI, trade violations) block submission\\n   \u2192 Yellow findings (duplicate labor, low confidence, required items) are warnings\\n   \u2192 Info findings (mobilization, quantity sanity) are FYI\\n\\n3. Estimator sees \\\"OFCI Candidates\\\" red finding\\n   \u2192 Lists 3 items matching \\\"IP camera\\\" from trade profile\\n   \u2192 Clicks \\\"Resolve All\\\" \u2192 applyResolveAction marks all 3 as scope_in=false, ofci_flag=true\\n   \u2192 Bid totals drop by $15,000\\n   \u2192 Finding downgrades to green\\n\\n4. Estimator sees \\\"Missing: Permits\\\" yellow finding\\n   \u2192 Clicks \\\"Add Suggested Line Item\\\"\\n   \u2192 Modal pre-fills description, qty=1, material_each=0, man_hours_each=0\\n   \u2192 Estimator enters permit allowance ($500)\\n   \u2192 Clicks \\\"Add\\\"\\n   \u2192 New line item inserted\\n   \u2192 Finding acked (whole-finding ignore) so it doesn't resurface\\n\\n5. Estimator sees \\\"Possible Duplicate Labor\\\" yellow finding\\n   \u2192 Lists pair: \\\"Camera installation\\\" + \\\"Camera mount labor\\\"\\n   \u2192 Manually deletes the \\\"Camera mount labor\\\" line\\n   \u2192 Re-runs audit\\n   \u2192 Finding is gone (no more pairs)\\n```\\n\\n## Testing\\n\\nThe module includes comprehensive unit tests:\\n\\n- **engine_test.go** \u2014 three real bids from Excel workbook verify exact calculation match\\n- **exclusion_extractor_test.go** \u2014 pattern matching for OFCI, by-owner, by-trade, N.I.C.\\n- **audit.go** \u2014 inline checks for OFCI candidates, trade violations, duplicate labor, etc.\\n\\nAll calculation is deterministic and fully testable without database dependency.\\n\\n## Performance Considerations\\n\\n- **AI calls** \u2014 addendum apply, takeoff, scope summary, reconciliation are async (background goroutines)\\n- **Polling** \u2014 UI polls status endpoints (addendum-diff-status, vision-progress, processing-status) with exponential backoff\\n- **Batch operations** \u2014 Resolve-All applies multiple findings in a single transaction\\n- **Lazy drawing mirror** \u2014 pages are mirrored on-demand when estimator selects drawings, not upfront\\n\\n## Security &amp; Isolation\\n\\n- All handlers validate `TenantIDFromContext` \u2014 no cross-tenant data leakage\\n- Addendum changes, audit acks, and line items are scoped to (tenant_id, bid_sheet_id)\\n- User ID captured on audit acks and addendum change decisions for audit trail\",\"internal-billing\":\"# internal \u2014 billing\\n\\n# internal/billing Module\\n\\nThe billing module provides a complete invoicing and quoting system for NexusOS PSA, including invoice lifecycle management, client-facing quote approval, payment recording, credit memos, file attachments, and email delivery via Microsoft 365 or SMTP.\\n\\n## Overview\\n\\nThis module handles:\\n\\n- **Invoices**: Create, edit, send, record payments, void, and duplicate invoices. Track line items, discounts, taxes, and aging.\\n- **Quotes**: Draft, send, and manage client approval workflows with e-signature support. Support optional line items with client-selected quantities.\\n- **Payments &amp; Credits**: Record invoice payments, apply credit memos, and automatically update invoice status.\\n- **Attachments**: Upload, download, and delete files on invoices and quotes with audit trails.\\n- **Email**: Send invoices and quotes to clients via Microsoft 365 Graph API or traditional SMTP, with header injection protection.\\n- **Reporting**: AR aging reports grouped by company and due date buckets.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    A[\\\"HTTP Handlers(invoice_handler, quote_handler)\\\"]\\n    B[\\\"Database(pgx pool)\\\"]\\n    C[\\\"Email Sender(Graph/SMTP)\\\"]\\n    D[\\\"File Storage(uploads/)\\\"]\\n    E[\\\"Auth Context(tenant, user)\\\"]\\n    \\n    A --&gt;|Query/Exec| B\\n    A --&gt;|Send via| C\\n    A --&gt;|Upload/Download| D\\n    A --&gt;|Extract| E\\n    C --&gt;|Store tokens| B\\n```\\n\\n## Core Components\\n\\n### Handler\\n\\nThe `Handler` struct is the entry point for all billing HTTP endpoints:\\n\\n```go\\ntype Handler struct {\\n    pool             *pgxpool.Pool\\n    renderer         *ui.Renderer\\n    Email            *EmailSender\\n    publicURL        string\\n    OnQuoteApproved  func(ctx context.Context, tenantID, quoteID string)\\n    GenerateContract func(ctx context.Context, tenantID, quoteID string) (string, error)\\n}\\n```\\n\\n- **pool**: PostgreSQL connection pool for all database operations.\\n- **renderer**: Template renderer for HTML pages.\\n- **Email**: Sends invoices and quotes to clients.\\n- **publicURL**: Canonical origin for client-facing approval links (e.g., `https://nexus.example.com`).\\n- **OnQuoteApproved**: Async callback wired in `main.go` to generate contracts and push to QuickBooks.\\n- **GenerateContract**: Wired to `legalHandler.CreateContractFromQuote` for contract generation.\\n\\n### EmailSender\\n\\nProvides a unified abstraction for sending emails via Microsoft 365 Graph API or SMTP:\\n\\n```go\\ntype EmailSender struct {\\n    pool *pgxpool.Pool\\n}\\n```\\n\\n**Key methods:**\\n\\n- `SendEmail(ctx, tenantID, to, subject, htmlBody)` \u2014 Sends via the tenant's default email account.\\n- `SendEmailFromAccount(ctx, tenantID, accountID, to, subject, htmlBody)` \u2014 Sends via a specific account.\\n- `sendViaGraph(...)` \u2014 Microsoft 365 Graph API delivery with token refresh.\\n- `sendViaSMTP(...)` \u2014 Traditional SMTP delivery.\\n- `refreshMSToken(...)` \u2014 Obtains new M365 access tokens using client credentials flow.\\n\\n**Security:** All public entry points validate the `To` and `Subject` headers via `validateHeaderField()` to reject CR/LF characters (audit finding H7, 2026-04-29). This prevents header injection attacks like `Subject: x\\\\r\\\\nBcc: attacker@evil`.\\n\\n### Invoice &amp; Quote Types\\n\\n**Invoice** represents a billable document:\\n\\n```go\\ntype Invoice struct {\\n    ID              string\\n    InvoiceNumber   int\\n    TenantID        string\\n    CompanyID       string\\n    Status          string // draft, sent, overdue, partial, paid, void, write_off\\n    Subtotal        float64\\n    DiscountAmount  float64\\n    TaxRate         float64\\n    TaxAmount       float64\\n    Total           float64\\n    AmountPaid      float64\\n    BalanceDue      float64\\n    CreditsApplied  float64\\n    // ... timestamps, contact info, terms, etc.\\n}\\n```\\n\\n**Quote** represents a client-facing price proposal:\\n\\n```go\\ntype Quote struct {\\n    ID               string\\n    QuoteNumber      int\\n    TenantID         string\\n    CompanyID        string\\n    Status           string // draft, sent, viewed, approved, rejected, accepted\\n    Subtotal         float64\\n    DiscountType     string // \\\"percentage\\\" or \\\"fixed\\\"\\n    DiscountValue    float64\\n    TaxRate          float64\\n    Total            float64\\n    ApprovalToken    string // Token-secured public approval endpoint\\n    Version          int\\n    RequirePO        bool\\n    // ... timestamps, contact info, line items, etc.\\n}\\n```\\n\\n### Line Items\\n\\n**InvoiceLineItem** and **QuoteLineItem** represent individual rows on invoices and quotes. Quotes support optional items and client-adjustable quantities:\\n\\n```go\\ntype QuoteLineItem struct {\\n    ID              string\\n    QuoteID         string\\n    Description     string\\n    ProductID       string\\n    Quantity        float64\\n    UnitPrice       float64\\n    LineTotal       float64\\n    BillingType     string // \\\"one_time\\\", \\\"monthly\\\", \\\"quarterly\\\", \\\"yearly\\\"\\n    IsOptional      bool\\n    QtyAdjustable   bool\\n    QtyMin, QtyMax  *float64\\n    ClientSelected  bool   // Client opts into optional items on approval page\\n    ClientQty       *float64 // Client's chosen quantity if adjustable\\n}\\n```\\n\\nWhen generating an invoice from an approved quote, the system honors the client's scope:\\n- Skips optional lines the client did not select.\\n- Uses `client_qty` (if adjustable) or falls back to the authored quantity.\\n- Recomputes `line_total` to match the effective quantity.\\n\\n### Attachments\\n\\n**InvoiceAttachment** and **QuoteAttachment** represent files uploaded to invoices and quotes:\\n\\n```go\\ntype InvoiceAttachment struct {\\n    ID           string\\n    InvoiceID    string\\n    FileName     string\\n    FilePath     string\\n    FileSize     int64\\n    MimeType     string\\n    UploadedBy   string\\n    UploaderName string\\n    CreatedAt    time.Time\\n}\\n```\\n\\nFiles are stored in `uploads/invoices/{tenantID}/` or `uploads/quotes/{tenantID}/` with collision-safe names (8-byte random hex prefix). Allowed types: PDF, DOCX, DOC, TXT, MD, XLSX, XLS, CSV, PNG, JPG, JPEG. Max 50 MB per file.\\n\\n## Invoice Workflow\\n\\n### Creating an Invoice\\n\\n**Form submission** (`handleNewInvoiceSubmit`):\\n1. Parse form fields including inline line items (`lines[0][description]`, `lines[0][quantity]`, etc.).\\n2. Calculate subtotal, discount, tax, and total.\\n3. Insert invoice and line items in a transaction.\\n4. Redirect to invoice detail page.\\n\\n**JSON API** (`handleCreateInvoice`):\\n1. Decode JSON body.\\n2. Calculate due date from `invoice_date` + `payment_terms` (via `PaymentTermsDays()`).\\n3. Insert and return the new invoice ID.\\n\\n### Generating from a Quote\\n\\n`handleGenerateFromQuote`:\\n1. Fetch the approved quote.\\n2. Create a new draft invoice with the same company, contact, and totals.\\n3. Copy quote line items to invoice line items, honoring the client's accepted scope:\\n   - Skip optional lines where `client_selected = false`.\\n   - Use `client_qty` if the line is adjustable, otherwise use the authored quantity.\\n   - Recompute `line_total`.\\n4. Update the quote status to `accepted`.\\n5. Add an audit log entry.\\n\\n### Sending an Invoice\\n\\n`handleSendInvoice`:\\n1. Determine status: if `due_date &lt; today`, set to `overdue`; otherwise `sent`.\\n2. Update the invoice and set `sent_at = now()`.\\n\\n### Recording Payments\\n\\n`handleRecordPayment`:\\n1. Insert a payment record with amount, date, method, and reference.\\n2. Update invoice `amount_paid` and `balance_due`.\\n3. If `balance_due &lt;= 0`, set status to `paid` and `paid_at = now()`.\\n4. If `amount_paid &gt; 0` but balance remains, set status to `partial`.\\n\\n### Applying Credits\\n\\n`handleCreateCreditMemo`:\\n1. Create a credit memo record.\\n2. Increment invoice `credits_applied` and decrement `balance_due`.\\n3. If fully paid after credit, set status to `paid`.\\n\\n### Editing an Invoice\\n\\n`handleEditInvoiceSubmit`:\\n1. Parse form fields (title, description, line items, etc.).\\n2. Recalculate totals.\\n3. Update invoice and delete/reinsert line items in a transaction.\\n4. Add audit log entry.\\n\\n## Quote Workflow\\n\\n### Creating a Quote\\n\\n**Form submission** (`handleNewQuoteSubmit`):\\n1. Pre-generate a quote ID so attachments can be uploaded before save.\\n2. Parse form fields including inline line items with optional/adjustable flags.\\n3. Calculate subtotal, discount (percentage or fixed), tax, and total.\\n4. Generate an approval token (32-byte random hex).\\n5. Insert quote and line items in a transaction.\\n6. Add audit log entry.\\n\\n**JSON API** (`handleCreateQuote`):\\n1. Decode JSON body.\\n2. Generate approval token and insert.\\n\\n### Sending a Quote\\n\\n`handleSendQuote`:\\n1. Generate or reuse the approval token.\\n2. Set status to `sent` and `sent_at = now()`.\\n3. Add audit log entry.\\n4. Asynchronously send an approval email to the client contact with a link to `/quotes/approve/{token}`.\\n\\n### Client Approval Portal\\n\\n`handleClientApprovalPage` (public, token-secured):\\n1. Fetch quote by approval token (no auth required).\\n2. If status is `sent`, mark as `viewed` and add audit log.\\n3. Render the approval page with line items, optional item toggles, and quantity adjusters.\\n4. Client can approve, reject, or adjust quantities.\\n\\n`handleClientApprove` (public, token-secured):\\n1. Validate the token.\\n2. Parse client selections (which optional items to include, adjusted quantities).\\n3. Update quote line items with `client_selected` and `client_qty`.\\n4. Set quote status to `approved` and `approved_at = now()`.\\n5. Add audit log entry.\\n6. Call `OnQuoteApproved` callback (async) to generate contract and push to QuickBooks.\\n\\n`handleClientReject` (public, token-secured):\\n1. Set quote status to `rejected` and `rejected_at = now()`.\\n2. Add audit log entry.\\n\\n### Editing a Quote\\n\\n`handleEditQuoteForm` / `handleEditQuoteSubmit`:\\n1. Load existing quote and line items.\\n2. Allow editing title, summary, company, contact, discount, tax, terms, notes, etc.\\n3. Recalculate totals.\\n4. Update quote and line items in a transaction.\\n\\n### Duplicating a Quote\\n\\n`handleDuplicateQuote`:\\n1. Load the source quote.\\n2. Create a new draft quote with title suffix \\\" (Copy)\\\".\\n3. Copy all line items.\\n4. Return the new quote ID.\\n\\n### Recalculating Totals\\n\\n`handleRecalculate`:\\n1. Sum all line items' `line_total` values.\\n2. Apply discount (percentage or fixed).\\n3. Apply tax to the taxable amount.\\n4. Update quote totals in the database.\\n\\n## Attachment Workflow\\n\\n### Upload\\n\\n`handleUploadInvoiceAttachment` / `handleUploadQuoteAttachment`:\\n1. Verify the invoice/quote belongs to the tenant.\\n2. Parse multipart form (max 50 MB).\\n3. Validate file extension against whitelist.\\n4. Generate collision-safe filename (8-byte random hex + original name).\\n5. Write to disk in `uploads/{invoices|quotes}/{tenantID}/`.\\n6. Detect MIME type from extension.\\n7. Insert into database.\\n8. Add audit log entry.\\n9. Redirect to invoice/quote detail page.\\n\\n### Download\\n\\n`handleDownloadInvoiceAttachment` / `handleDownloadQuoteAttachment`:\\n1. Fetch attachment metadata by ID, invoice/quote ID, and tenant ID.\\n2. Open file from disk.\\n3. Set `Content-Type` and `Content-Disposition` headers.\\n4. Stream file to client.\\n\\n### Delete\\n\\n`handleDeleteInvoiceAttachment` / `handleDeleteQuoteAttachment`:\\n1. Fetch attachment metadata.\\n2. Delete from database.\\n3. Remove file from disk (best-effort).\\n4. Add audit log entry.\\n5. Redirect to invoice/quote detail page.\\n\\n## Email Delivery\\n\\n### Configuration\\n\\nEmail accounts are stored in the `email_accounts` table with provider-specific credentials:\\n\\n- **SMTP**: `smtp_host`, `smtp_port`, `smtp_user`, `smtp_pass_enc` (encrypted).\\n- **Microsoft 365**: `ms_tenant_id`, `ms_client_id`, `ms_client_secret_enc`, `ms_access_token`, `ms_refresh_token`, `ms_token_expires_at`.\\n\\n### Sending via SMTP\\n\\n`sendViaSMTP`:\\n1. Format `From` and `To` addresses via `net/mail.Address` to ensure proper quoting.\\n2. Build MIME message with HTML body.\\n3. Connect to SMTP server and send.\\n4. Log success.\\n\\n### Sending via Microsoft 365\\n\\n`sendViaGraph`:\\n1. Check if access token is expired (with 5-minute buffer).\\n2. If expired, call `refreshMSToken()` to obtain a new token via client credentials flow.\\n3. Build Graph API `sendMail` request payload.\\n4. POST to `https://graph.microsoft.com/v1.0/users/{from}/sendMail`.\\n5. Handle 202 (Accepted) or 200 (OK) responses.\\n6. Log success.\\n\\n`refreshMSToken`:\\n1. POST to `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token` with client credentials.\\n2. Parse response for `access_token` and `expires_in`.\\n3. Update the email account record with new token and expiration.\\n4. Return the token.\\n\\n### Header Injection Protection\\n\\n`validateHeaderField`:\\n1. Scan the field value for CR (`\\\\r`), LF (`\\\\n`), or NUL (`\\\\0`) characters.\\n2. Reject with an error if found.\\n3. Applied to `To` and `Subject` at public entry points before either SMTP or Graph paths see the value.\\n\\nAdditionally, `sendViaSMTP` uses `net/mail.Address` to format addresses, which applies RFC 5322 quoting rules on the wire.\\n\\n## Reporting\\n\\n### AR Aging Report\\n\\n`handleAgingReport`:\\n1. Query invoices grouped by company.\\n2. Bucket outstanding balance by due date:\\n   - **Current**: Not yet due + 0\u201330 days overdue.\\n   - **30 Day**: 31\u201360 days overdue.\\n   - **60 Day**: 61\u201390 days overdue.\\n   - **90+ Day**: 90+ days overdue.\\n3. Calculate grand total across all buckets.\\n4. Render aging report page.\\n\\n## Audit Logging\\n\\nBoth invoices and quotes maintain audit trails:\\n\\n- `invoice_audit_log`: Records events like `created`, `edited`, `sent`, `payment_recorded`, `void`, etc.\\n- `quote_audit_log`: Records events like `created`, `sent`, `viewed`, `approved`, `rejected`, `attachment_added`, etc.\\n\\nEach entry includes:\\n- Event type\\n- Actor (user, system, or client)\\n- Actor name and email\\n- Detailed description of the change\\n- Timestamp\\n\\n## Integration Points\\n\\n### With Auth Module\\n\\n- `TenantIDFromContext()`: Extracts tenant ID from request context.\\n- `UserIDFromContext()`: Extracts user ID from request context.\\n- `ClaimsFromContext()`: Extracts JWT claims (used for role-based UI rendering).\\n\\n### With UI Module\\n\\n- `Renderer.Render()`: Renders HTML templates with data.\\n\\n### With AI Module\\n\\n- Quotes can be AI-generated from transcripts (via `transcript_handler.go`).\\n- AI usage is tracked in `ai_usage_log` and displayed on quote detail pages.\\n\\n### With Legal Module\\n\\n- `GenerateContract()` callback creates legal contracts from approved quotes.\\n- Contracts are linked to quotes via `legal_contract_id`.\\n\\n### With Onboarding Module\\n\\n- Welcome emails are sent via `EmailSender` during tenant onboarding.\\n\\n### With Tenant Module\\n\\n- Test emails are sent via `EmailSender` to verify email account configuration.\\n\\n### With Project Module\\n\\n- Change requests can be sent for signing via `EmailSender`.\\n\\n## Database Schema (Key Tables)\\n\\n- **invoices**: Core invoice records with status, totals, dates, and payment tracking.\\n- **invoice_line_items**: Line items on invoices with product, quantity, unit price, and tax flags.\\n- **invoice_payments**: Payment records with amount, date, method, and reference.\\n- **invoice_attachments**: File attachments on invoices.\\n- **invoice_audit_log**: Audit trail for invoice events.\\n- **quotes**: Core quote records with approval token, version, and client scope.\\n- **quote_line_items**: Line items on quotes with optional/adjustable flags and client selections.\\n- **quote_attachments**: File attachments on quotes.\\n- **quote_audit_log**: Audit trail for quote events.\\n- **quote_revisions**: Historical revisions of quotes.\\n- **credit_memos**: Credit memos applied to invoices.\\n- **email_accounts**: Email provider credentials (SMTP or M365) per tenant.\\n\\n## Common Patterns\\n\\n### Form Line Item Parsing\\n\\nBoth invoices and quotes parse inline line items from form fields using a naming convention:\\n\\n```\\nlines[0][description]=...\\nlines[0][quantity]=...\\nlines[0][unit_price]=...\\nlines[1][description]=...\\n...\\n```\\n\\nThe `parseFormLineItems()` and `parseQuoteFormLineItems()` helpers iterate until a blank description and product ID are found.\\n\\n### Graceful Fallback for Missing Email Account\\n\\nIf no email account is configured for a tenant, `SendEmailFromAccount()` logs a warning and returns `nil` (no error). This allows the system to continue without blocking invoice/quote operations.\\n\\n### Transaction Safety\\n\\nMulti-step operations (create invoice with line items, record payment and update balance, etc.) use database transactions to ensure atomicity.\\n\\n### Redirect-on-Success Pattern\\n\\nForm submissions redirect via `HX-Redirect` header (HTMX convention) to the detail page, allowing the browser to fetch the updated state.\\n\\n## Testing &amp; Debugging\\n\\n- Email sending is logged with recipient and subject (subject redacted in logs).\\n- Database errors are logged with context (operation, invoice/quote ID, error message).\\n- File operations log success and errors.\\n- Audit log entries are added for all significant state changes.\",\"internal-certs\":\"# internal \u2014 certs\\n\\n# Certificate Management Module\\n\\nThe `internal/certs` module provides centralized certificate lifecycle management for NexusOS. It handles generation, signing, storage, encryption, and revocation of X.509 certificates used across SIEM, RMM, web services, and device enrollment flows.\\n\\n## Overview\\n\\nThis module serves three primary functions:\\n\\n1. **Certificate Authority (CA) Management** \u2014 Generate and maintain a tenant's internal CA\\n2. **Certificate Lifecycle** \u2014 Sign server/client certificates, store them with encrypted private keys, and manage revocation\\n3. **Device Enrollment** \u2014 Accept Certificate Signing Requests (CSRs) from agents, sign them, and distribute certificates + CA chain\\n\\nAll certificates are tenant-isolated and stored in PostgreSQL with optional AES-256-GCM encryption of private keys.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    Handler[\\\"Handler(HTTP API)\\\"]\\n    Store[\\\"Store(Data Access)\\\"]\\n    CA[\\\"CA Operations(GenerateCA, SignServerCert)\\\"]\\n    DB[(PostgreSQL)]\\n    \\n    Handler --&gt;|CRUD, enrollment| Store\\n    Handler --&gt;|Sign, generate| CA\\n    CA --&gt;|Save, GetCA| Store\\n    Store --&gt;|Encrypt/decrypt| Store\\n    Store --&gt;|Query| DB\\n```\\n\\n## Core Components\\n\\n### Store\\n\\nThe `Store` type is the data access layer for all certificate operations. It wraps a PostgreSQL connection pool and manages encryption.\\n\\n**Initialization:**\\n```go\\nstore := NewStore(pool, \\\"64-char-hex-aes256-key\\\")\\n```\\n\\nIf the encryption key is empty or invalid, private keys are stored plaintext. The key must be exactly 32 bytes (64 hex characters).\\n\\n**Key Methods:**\\n\\n- **`List(ctx, tenantID)`** \u2014 Fetch all certificates for a tenant, ordered by CA status and creation date\\n- **`Get(ctx, tenantID, certID)`** \u2014 Retrieve a single certificate with all fields including encrypted private key\\n- **`GetCA(ctx, tenantID)`** \u2014 Get the active internal CA (there is at most one per tenant)\\n- **`GetByUsage(ctx, tenantID, usage)`** \u2014 Find the most recent active certificate tagged with a usage label (e.g., \\\"siem\\\", \\\"rmm\\\")\\n- **`Save(ctx, tenantID, cert)`** \u2014 Insert a new certificate, auto-parsing PEM to extract metadata (CN, fingerprint, serial, validity dates), and encrypting the private key\\n- **`Delete(ctx, tenantID, certID)`** \u2014 Remove a certificate\\n- **`AssignDevice(ctx, certID, assignment)`** \u2014 Link a certificate to a device (agent)\\n- **`UnassignDevice(ctx, assignmentID)`** \u2014 Remove a device assignment\\n- **`ListDevices(ctx, certID)`** \u2014 Get all devices assigned to a certificate\\n- **`EncryptKey(keyPEM)`** \u2014 Encrypt a private key with AES-256-GCM; returns hex string\\n- **`DecryptKey(encHex)`** \u2014 Decrypt a private key; gracefully falls back to plaintext if decryption fails\\n\\n### Certificate\\n\\nThe `Certificate` struct represents a stored certificate with metadata:\\n\\n```go\\ntype Certificate struct {\\n    ID            string        // UUID\\n    TenantID      string        // Tenant isolation\\n    Name          string        // Display name\\n    CertType      string        // \\\"ca\\\", \\\"server\\\", \\\"client\\\"\\n    CommonName    string        // Extracted from PEM\\n    Domain        string        // For server certs\\n    CertPEM       string        // PEM-encoded certificate\\n    PrivateKeyEnc string        // Encrypted private key (hex)\\n    CACertPEM     string        // Issuing CA cert (for chains)\\n    Issuer        string        // Issuer CN\\n    IssuerCertID  *string       // Reference to issuing cert\\n    Fingerprint   string        // SHA-256 hex\\n    SerialNumber  string        // Decimal string\\n    KeyAlgorithm  string        // \\\"rsa-4096\\\", \\\"rsa-2048\\\", \\\"ecdsa-p256\\\"\\n    IsCA          bool          // True if this is a CA\\n    NotBefore     *time.Time    // Validity start\\n    NotAfter      *time.Time    // Validity end\\n    AutoRenew     bool          // Reserved for future use\\n    UsageTags     []string      // e.g., [\\\"siem\\\", \\\"rmm\\\"]\\n    Status        string        // \\\"active\\\", \\\"revoked\\\", \\\"pending\\\"\\n    CompanyID     *string       // Optional client assignment\\n    CreatedAt     time.Time\\n}\\n```\\n\\nMetadata fields (CommonName, Fingerprint, SerialNumber, NotBefore, NotAfter, IsCA) are automatically extracted from the PEM certificate during `Save()`.\\n\\n### CA Operations\\n\\n#### GenerateCA\\n\\nCreates a self-signed root CA certificate for a tenant.\\n\\n```go\\nca, err := store.GenerateCA(ctx, tenantID, \\\"My Internal CA\\\", 10)\\n```\\n\\n- Generates a 4096-bit RSA key\\n- Creates a self-signed certificate valid for `validYears` (default 10)\\n- Sets `MaxPathLen: 1` to allow one level of subordinate CAs\\n- Stores the CA with `IsCA: true` and `Status: \\\"active\\\"`\\n- Only one CA per tenant is expected; the handler rejects generation if one already exists\\n\\n#### SignServerCert\\n\\nSigns a new server or client certificate using the tenant's CA.\\n\\n```go\\ncert, err := store.SignServerCert(ctx, tenantID, SignRequest{\\n    CommonName:   \\\"siem.nexusos.local\\\",\\n    DNSNames:     []string{\\\"siem.nexusos.local\\\", \\\"siem\\\"},\\n    CertType:     \\\"server\\\",\\n    KeyAlgorithm: \\\"rsa-2048\\\",  // or \\\"ecdsa-p256\\\"\\n    ValidDays:    365,\\n    UsageTags:    []string{\\\"siem\\\"},\\n    CompanyID:    &amp;companyID,\\n})\\n```\\n\\n**Behavior:**\\n- Loads the tenant's CA certificate and private key\\n- Generates a new key pair (RSA-2048 or ECDSA-P256)\\n- Creates a certificate template with the requested CN, SANs, and key usage\\n- Signs with the CA's private key\\n- Stores the signed certificate with a reference to the issuing CA (`IssuerCertID`)\\n\\n**Key Usage:**\\n- Server certs: `KeyUsageDigitalSignature | KeyUsageKeyEncipherment` + `ExtKeyUsageServerAuth`\\n- Client certs: `KeyUsageDigitalSignature | KeyUsageKeyEncipherment` + `ExtKeyUsageClientAuth`\\n\\n### Handler\\n\\nThe `Handler` type exposes certificate operations via HTTP routes and serves the certificate management UI.\\n\\n**Routes:**\\n\\n| Method | Path | Purpose |\\n|--------|------|---------|\\n| GET | `/settings/certificates` | Certificate management page (HTML) |\\n| POST | `/api/certificates` | Upload/import an existing certificate |\\n| GET | `/api/certificates/{id}` | Fetch certificate details (JSON) |\\n| DELETE | `/api/certificates/{id}` | Delete a certificate |\\n| POST | `/api/certificates/generate-ca` | Generate a new internal CA |\\n| POST | `/api/certificates/sign` | Sign a new certificate |\\n| GET | `/api/certificates/{id}/export` | Download certificate PEM |\\n| POST | `/api/certificates/{id}/devices` | Assign a device to a certificate |\\n| DELETE | `/api/certificates/devices/{id}` | Unassign a device |\\n| POST | `/api/ca/enroll` | Device enrollment (CSR \u2192 signed cert) |\\n| GET | `/api/ca/crl` | Fetch Certificate Revocation List |\\n| POST | `/api/ca/revoke/{id}` | Revoke a certificate |\\n\\n#### Device Enrollment (`/api/ca/enroll`)\\n\\nAccepts a PEM-encoded CSR from an agent, signs it with the tenant's CA, and returns the signed certificate + CA chain.\\n\\n**Request:**\\n```json\\n{\\n  \\\"csr\\\": \\\"-----BEGIN CERTIFICATE REQUEST-----\\\\n...\\\",\\n  \\\"device_name\\\": \\\"agent-001\\\",\\n  \\\"agent_id\\\": \\\"uuid-of-agent\\\"\\n}\\n```\\n\\n**Response:**\\n```json\\n{\\n  \\\"certificate\\\": \\\"-----BEGIN CERTIFICATE-----\\\\n...\\\",\\n  \\\"ca_cert\\\": \\\"-----BEGIN CERTIFICATE-----\\\\n...\\\",\\n  \\\"cert_id\\\": \\\"uuid-of-stored-cert\\\"\\n}\\n```\\n\\n**Flow:**\\n1. Parse and validate the CSR signature\\n2. Load the tenant's CA certificate and private key\\n3. Generate a random 128-bit serial number\\n4. Create a certificate template from the CSR subject, valid for 1 year, with `ExtKeyUsageClientAuth`\\n5. Sign the certificate with the CA's private key\\n6. Store the signed certificate in the database\\n7. Optionally assign the certificate to the device (agent)\\n\\n#### Certificate Revocation List (`/api/ca/crl`)\\n\\nReturns a DER-encoded X.509 CRL for the tenant's CA. Agents and auth-gate poll this to reject revoked certificates.\\n\\n**Behavior:**\\n- Queries all certificates with `status = 'revoked'` for the tenant\\n- Creates a CRL valid for 24 hours\\n- Includes revocation time for each entry\\n- Cached by clients for up to 1 hour\\n\\n#### Revocation (`/api/ca/revoke/{id}`)\\n\\nMarks a certificate as revoked. The next CRL fetch will include it.\\n\\n```\\nPOST /api/ca/revoke/cert-uuid\\n```\\n\\nUpdates the certificate status to `'revoked'` and records the revocation timestamp.\\n\\n## Encryption\\n\\nPrivate keys are encrypted at rest using AES-256-GCM if an encryption key is configured.\\n\\n**Encryption Flow:**\\n1. When a certificate is saved, `Save()` calls `EncryptKey()` if a key is configured\\n2. `EncryptKey()` generates a random nonce, seals the plaintext key with GCM, and returns `nonce || ciphertext` as hex\\n3. The hex string is stored in the `private_key_enc` column\\n\\n**Decryption Flow:**\\n1. `DecryptKey()` decodes the hex string\\n2. Extracts the nonce (first `NonceSize()` bytes)\\n3. Opens the ciphertext with GCM\\n4. If decryption fails, gracefully returns the input as plaintext (for backward compatibility)\\n\\n**Key Management:**\\n- The encryption key is passed to `NewStore()` as a 64-character hex string (32 bytes)\\n- If no key is provided or it's invalid, keys are stored plaintext\\n- The key should be stored securely (e.g., environment variable, secrets manager)\\n\\n## Private Key Handling\\n\\nThe module supports multiple private key formats:\\n\\n- **RSA PRIVATE KEY** (PKCS#1)\\n- **EC PRIVATE KEY** (SEC1)\\n- **PRIVATE KEY** (PKCS#8)\\n\\n**Parsing (`parsePrivateKey`):**\\nAttempts to decode PEM and parse based on the block type. Falls back to PKCS#8 if the declared type doesn't match.\\n\\n**Marshaling (`marshalPrivateKey`):**\\nEncodes private keys back to PEM:\\n- RSA keys \u2192 PKCS#1 format\\n- ECDSA keys \u2192 SEC1 format\\n\\n## Integration Points\\n\\n### Outbound Dependencies\\n\\n- **`internal/auth`** \u2014 `TenantIDFromContext()` extracts tenant ID from request context\\n- **`internal/ui`** \u2014 `Renderer.Render()` serves the certificate management page\\n\\n### Inbound Consumers\\n\\n- **`cmd/psa/main.go`** \u2014 Initializes the store and handler; uses `GetByUsage()` to load certificates for services\\n- **`internal/devices`** \u2014 Calls `GetCA()` to retrieve the CA for FortiGate syslog TLS configuration\\n- **Other modules** \u2014 Can call `Store.GetByUsage()` to retrieve certificates by usage tag\\n\\n## Database Schema\\n\\nThe module expects two tables:\\n\\n**`certificates`**\\n```sql\\nid UUID PRIMARY KEY\\ntenant_id UUID NOT NULL\\nname TEXT\\ncert_type TEXT\\ncommon_name TEXT\\ndomain TEXT\\ncertificate_pem TEXT\\nprivate_key_enc TEXT  -- AES-256-GCM encrypted, hex-encoded\\nca_certificate_pem TEXT\\nissuer TEXT\\nissuer_cert_id UUID REFERENCES certificates(id)\\nfingerprint_sha256 TEXT\\nserial_number TEXT\\nkey_algorithm TEXT\\nis_ca BOOLEAN\\nnot_before TIMESTAMP\\nnot_after TIMESTAMP\\nauto_renew BOOLEAN\\nusage_tags TEXT[]\\nstatus TEXT  -- 'active', 'revoked', 'pending'\\ncompany_id UUID\\ncreated_at TIMESTAMP\\nupdated_at TIMESTAMP\\n```\\n\\n**`certificate_devices`**\\n```sql\\nid UUID PRIMARY KEY\\ncertificate_id UUID REFERENCES certificates(id) ON DELETE CASCADE\\nagent_id UUID\\ndevice_name TEXT\\ndevice_ip TEXT\\nassigned_at TIMESTAMP\\n```\\n\\n## Usage Examples\\n\\n### Generate an Internal CA\\n\\n```go\\nca, err := handler.store.GenerateCA(ctx, tenantID, \\\"NexusOS Internal CA\\\", 10)\\nif err != nil {\\n    log.Fatal(err)\\n}\\nlog.Printf(\\\"CA created: %s (fingerprint=%s)\\\", ca.CommonName, ca.Fingerprint)\\n```\\n\\n### Sign a Server Certificate\\n\\n```go\\ncert, err := handler.store.SignServerCert(ctx, tenantID, SignRequest{\\n    CommonName:   \\\"siem.example.com\\\",\\n    DNSNames:     []string{\\\"siem.example.com\\\", \\\"siem\\\"},\\n    CertType:     \\\"server\\\",\\n    KeyAlgorithm: \\\"rsa-2048\\\",\\n    ValidDays:    365,\\n    UsageTags:    []string{\\\"siem\\\"},\\n})\\nif err != nil {\\n    log.Fatal(err)\\n}\\n```\\n\\n### Retrieve a Certificate by Usage\\n\\n```go\\ncert, err := store.GetByUsage(ctx, tenantID, \\\"siem\\\")\\nif err != nil {\\n    log.Fatal(\\\"no SIEM certificate found\\\")\\n}\\nkeyPEM, err := store.DecryptKey(cert.PrivateKeyEnc)\\nif err != nil {\\n    log.Fatal(err)\\n}\\n// Use cert.CertPEM and keyPEM for TLS configuration\\n```\\n\\n### Revoke and Fetch CRL\\n\\n```go\\n// Revoke a certificate\\nerr := store.Delete(ctx, tenantID, certID)\\n\\n// Or mark as revoked (keeps the record)\\nstore.pool.Exec(ctx, \\\"UPDATE certificates SET status = 'revoked' WHERE id = $1\\\", certID)\\n\\n// Agents fetch the CRL\\n// GET /api/ca/crl?tenant_id=...\\n```\\n\\n## Error Handling\\n\\nThe module uses wrapped errors with context:\\n\\n```go\\nif err != nil {\\n    return nil, fmt.Errorf(\\\"sign cert: %w\\\", err)\\n}\\n```\\n\\nCommon error scenarios:\\n\\n- **No CA exists** \u2014 `GetCA()` returns `pgx.ErrNoRows`; handlers check and return 412 Precondition Failed\\n- **Invalid PEM** \u2014 `ParseCertPEM()` returns \\\"no PEM block found\\\"\\n- **Decryption failure** \u2014 `DecryptKey()` gracefully falls back to plaintext\\n- **CSR validation** \u2014 `csr.CheckSignature()` validates the request signature\\n\\n## Security Considerations\\n\\n1. **Private Key Encryption** \u2014 Always configure an encryption key in production\\n2. **Tenant Isolation** \u2014 All queries filter by `tenant_id`; no cross-tenant leakage\\n3. **CA Constraints** \u2014 CA certificates have `MaxPathLen: 1` to prevent deep subordinate chains\\n4. **CSR Validation** \u2014 Device enrollment validates CSR signatures before signing\\n5. **Revocation** \u2014 Certificates can be revoked and excluded from TLS handshakes via CRL\\n6. **Key Algorithm Support** \u2014 RSA-4096 for CAs, RSA-2048 or ECDSA-P256 for issued certs\\n\\n## Future Extensions\\n\\n- **Auto-renewal** \u2014 The `AutoRenew` field is reserved for automatic certificate renewal before expiry\\n- **OCSP** \u2014 Could supplement CRL with OCSP responder\\n- **Subordinate CAs** \u2014 Support issuing intermediate CAs (currently `MaxPathLen: 1`)\\n- **Key Rotation** \u2014 Encrypt existing keys with a new key during rotation\",\"internal-cmmc\":\"# internal \u2014 cmmc\\n\\n# CMMC Module Documentation\\n\\n## Overview\\n\\nThe **cmmc** module implements a complete CMMC Level 2 (Cybersecurity Maturity Model Certification) compliance management system for NexusOS. It provides tools for tracking security control implementation, managing evidence, documenting gaps via POA&amp;M (Plan of Action &amp; Milestones), and generating compliance artifacts like System Security Plans and branded policy binders.\\n\\nThe module is built around the NIST SP 800-171 Rev 2 framework (110 security requirements across 14 control families) and integrates with other NexusOS modules\u2014RBAC, SIEM, RMM, Metasploit, Helpdesk, and Orchestrator\u2014to automatically collect evidence from operational systems.\\n\\n---\\n\\n## Architecture\\n\\n### Core Components\\n\\n#### Handler (`handler.go`)\\nThe main HTTP request dispatcher. Serves all CMMC pages and APIs:\\n- **Company enrollment &amp; dashboard** \u2014 select companies, view compliance scores\\n- **Control management** \u2014 list all 110 controls, drill into individual control detail with evidence and POA&amp;M\\n- **Evidence CRUD** \u2014 manual entry, auto-collection from integrated modules\\n- **POA&amp;M board** \u2014 track gaps, assign owners, schedule remediation\\n- **CUI scope mapping** \u2014 define systems/data in scope for compliance\\n- **SSP generation** \u2014 auto-generate System Security Plan sections\\n- **Assessments** \u2014 record self-assessments and third-party audits\\n\\n#### Nexie AI Integration (`nexie.go`)\\nAsync AI-powered operations for compliance acceleration:\\n- **Document ingestion** \u2014 upload company policies/letterhead; Claude extracts control mappings and branding\\n- **Gap analysis** \u2014 comprehensive AI review of all evidence against control requirements\\n- **Auto-fill gaps** \u2014 generate branded policy documents for unmet controls\\n- **Auto-remediate** \u2014 push RMM fixes for technical controls (disk encryption, antivirus, etc.)\\n\\nUses a job-tracking system (`nexieJob`) with in-memory state for long-running operations.\\n\\n#### Disposition &amp; Risk Acceptance (`disposition.go`)\\nGap-review workflow for handling identified weaknesses:\\n- **Disposition options** \u2014 remediate, accept (with justification), transfer, or defer\\n- **Risk acceptance statements** \u2014 formal signed documents for accepted risks\\n- **Dual-write to risk service** \u2014 accepted risks sync to the cross-module Risk Register for client portal sign-off\\n\\n#### Binder Rendering (`binder_render.go`)\\nGenerates a print-ready, branded compliance binder containing:\\n- Cover page with company branding and compliance metrics\\n- Table of contents organized by CMMC domain\\n- All policy documents (evidence marked as \\\"document\\\" type)\\n- Risk Acceptance Register (all formally accepted risks)\\n\\nUses inline CSS for print optimization; supports both screen and PDF output.\\n\\n#### One-Click Connect (`connect.go`)\\nActivation pipeline for rapid compliance setup:\\n1. Lists verified devices from the infra module\\n2. Runs RMM compliance scans on workstations/servers\\n3. Configures SIEM sources (firewalls, switches, servers)\\n4. Schedules vulnerability scans\\n5. Auto-populates CUI scope from device inventory\\n6. Collects initial evidence from all modules\\n7. Creates self-assessment record\\n\\nEach step is best-effort; failures don't abort the pipeline.\\n\\n---\\n\\n## Data Model\\n\\n### Key Tables\\n\\n**cmmc_evidence**\\n- Stores evidence items (manual, auto-collected, or AI-generated)\\n- Links to control_ref, source_module (rbac, siem, rmm, metasploit, helpdesk, orchestrator, nexie)\\n- Tracks status (current, stale, expired) and collection timestamp\\n\\n**cmmc_poam**\\n- Plan of Action &amp; Milestones \u2014 one row per identified gap\\n- Includes disposition (remediate, accept, transfer, defer), severity, assigned owner\\n- Risk acceptance fields: risk_accepted_by, risk_accepted_at, risk_expires_at\\n\\n**cmmc_cui_scope**\\n- Defines systems, data, and networks in scope for CUI protection\\n- Boundary classification: inside, boundary, outside\\n- Links to agents for automated monitoring\\n\\n**cmmc_ssp_sections**\\n- System Security Plan sections (auto-generated or manually overridden)\\n- Tracks auto_generated flag and last_generated timestamp\\n\\n**cmmc_assessments**\\n- Records self-assessments and third-party audits\\n- Stores assessment_type (self, third-party), status, findings/POA&amp;M counts\\n\\n**cmmc_company_branding**\\n- Extracted from letterhead or manually configured\\n- Colors, contact info, header/footer text for document generation\\n\\n**cmmc_ingested_docs**\\n- Metadata for uploaded documents (title, doc_type, summary, control mappings)\\n- Tracks AI cost and control count\\n\\n---\\n\\n## Evidence Collection Pipeline\\n\\nThe module automatically collects evidence from six integrated sources via `collectAutoEvidence()`:\\n\\n```\\ncollectAutoEvidence()\\n\u251c\u2500\u2500 collectRBACEvidence()      \u2192 AC.L1-3.1.1, IA.L1-3.5.1, IA.L1-3.5.2, IA.L2-3.5.3, AC.L2-3.1.4, AC.L2-3.1.5\\n\u251c\u2500\u2500 collectSIEMEvidence()      \u2192 AU.L2-3.3.1, AU.L2-3.3.2, SI.L1-3.14.5\\n\u251c\u2500\u2500 collectRMMEvidence()       \u2192 CM.L2-3.4.1, AC.L2-3.1.19, SC.L2-3.13.16, SI.L1-3.14.2, SI.L1-3.14.4, SI.L1-3.14.1\\n\u251c\u2500\u2500 collectMetasploitEvidence()\u2192 RA.L2-3.11.1, RA.L2-3.11.2\\n\u251c\u2500\u2500 collectHelpdeskEvidence()  \u2192 IR.L2-3.6.1, IR.L2-3.6.2\\n\u2514\u2500\u2500 collectOrchestratorEvidence()\u2192 CM.L2-3.4.3\\n```\\n\\nEach helper queries its source module's tables, formats evidence, and upserts via `upsertEvidence()` with ON CONFLICT logic to avoid duplicates.\\n\\n---\\n\\n## Dashboard Scoring\\n\\nThe dashboard computes compliance metrics via `buildDashboard()`:\\n\\n1. **aggregateControlScores()** \u2014 For each of 110 controls:\\n   - Count evidence items (status = 'current')\\n   - Check for open POA&amp;M items\\n   - Determine if control is \\\"met\\\" (has evidence) or \\\"not met\\\"\\n   - Group by domain and implementation group (L1 vs L2)\\n\\n2. **loadDashboardCounts()** \u2014 Aggregate POA&amp;M, evidence, and CUI scope counts\\n\\n3. **loadRecentAssessments()** \u2014 Fetch last 5 assessments\\n\\n**Scoring logic:**\\n- **Overall Score** = (MetControls / TotalControls) \u00d7 100\\n- **L1 Score** = (L1Met / L1Total) \u00d7 100\\n- **L2 Score** = (L2Met / L2Total) \u00d7 100\\n- **Domain Score** = (DomainMet / DomainTotal) \u00d7 100\\n\\n---\\n\\n## Nexie AI Workflow\\n\\n### Document Ingestion\\n\\n1. User uploads a document (PDF, DOCX, TXT, etc.) or pastes content\\n2. If letterhead, Claude extracts branding (company name, colors, address, logo description)\\n3. If policy/procedure, Claude maps content to CMMC controls and creates evidence items\\n4. Results stored in `cmmc_ingested_docs` and linked evidence rows\\n\\n### Gap Analysis\\n\\n1. Loads all controls and current evidence\\n2. Sends to Claude with prompt: \\\"For each control, assess implementation maturity based on evidence\\\"\\n3. Returns gap findings with severity and remediation recommendations\\n4. Creates POA&amp;M items for identified gaps\\n\\n### Fill Gaps\\n\\n1. Identifies all unmet controls\\n2. For each, generates a branded policy document via Claude\\n3. Stores as evidence with source_module = 'nexie'\\n4. Applies company branding (colors, header/footer, contact info)\\n\\n### Auto-Remediate\\n\\n1. Identifies technical controls (disk encryption, antivirus, patch management)\\n2. Generates RMM remediation scripts\\n3. Pushes to RMM module for execution on verified devices\\n4. Tracks remediation status via audit log\\n\\n---\\n\\n## Disposition &amp; Risk Acceptance\\n\\nWhen a gap is identified, the assessor chooses a disposition:\\n\\n- **Remediate** \u2014 Fix the gap; status = 'open'\\n- **Accept** \u2014 Accept the risk; creates a Risk Acceptance Statement\\n- **Transfer** \u2014 Transfer to third party; records transferred_to\\n- **Defer** \u2014 Defer remediation; records milestone and scheduled_completion\\n\\nFor **accept** disposition:\\n1. Updates cmmc_poam with disposition, reason, expiration date\\n2. Dual-writes to the cross-module `risk_acceptances` table (source of truth for Risk Register)\\n3. Generates a branded Risk Acceptance Statement for signature\\n\\n---\\n\\n## Compiled Binder\\n\\n`handleCompiledBinder()` generates a single, print-ready HTML document containing:\\n\\n1. **Cover page** \u2014 Company branding, compliance metrics (overall score, controls met, POA&amp;M count)\\n2. **Table of Contents** \u2014 All policy documents grouped by CMMC domain\\n3. **Policy Documents** \u2014 Each evidence item marked as \\\"document\\\" type, with control requirement box and formatted body\\n4. **Risk Acceptance Register** \u2014 All formally accepted risks with signature blocks\\n\\nThe binder uses inline CSS optimized for print (page breaks, margins, color handling). Supports both screen viewing and PDF export via browser print dialog.\\n\\n---\\n\\n## One-Click Connect Activation\\n\\n`handleActivate()` orchestrates rapid compliance setup in six steps:\\n\\n| Step | Action | Target Devices | Audit Event |\\n|------|--------|---|---|\\n| 1 | RMM compliance scan | Workstations, servers | `rmm_compliance_scan` |\\n| 2 | SIEM source config | Firewalls, switches, servers | `siem_configured` |\\n| 3 | Vulnerability scan | All verified devices | `scan_scheduled` |\\n| 4 | CUI scope auto-populate | All verified devices | `cui_scope_populated` |\\n| 5 | Evidence collection | All modules | `evidence_collected` |\\n| 6 | Assessment record | Company | `assessment_initiated` |\\n\\nEach step writes an audit log entry; failures don't abort subsequent steps. Final entry records aggregate outcome.\\n\\n---\\n\\n## Control Status &amp; Evidence Mapping\\n\\nControls are marked \\\"met\\\" when:\\n- At least one evidence item exists with status = 'current' for that control_ref\\n\\nControls are marked \\\"partial\\\" when:\\n- No evidence exists, but an open POA&amp;M item exists\\n\\nControls can be overridden to \\\"accepted_risk\\\", \\\"transferred\\\", or \\\"deferred\\\" via disposition.\\n\\n---\\n\\n## Integration Points\\n\\n### Incoming Data\\n- **RBAC** \u2014 User count, MFA status, role inventory\\n- **SIEM** \u2014 Audit log event counts, alert rules\\n- **RMM** \u2014 Device compliance data (disk encryption, antivirus, patch status)\\n- **Metasploit** \u2014 Vulnerability scan counts\\n- **Helpdesk** \u2014 Incident counts and lifecycle tracking\\n- **Orchestrator** \u2014 Workflow execution logs\\n- **Infra** \u2014 Device inventory, verification status\\n\\n### Outgoing Data\\n- **Risk Service** \u2014 Risk acceptances (dual-write from disposition)\\n- **Audit Log** \u2014 All CMMC actions (evidence collection, disposition changes, etc.)\\n- **RMM** \u2014 Remediation scripts and compliance scan requests\\n- **SIEM** \u2014 Source registration for log collection\\n\\n---\\n\\n## Key Functions\\n\\n### Public API Handlers\\n\\n| Handler | Method | Purpose |\\n|---------|--------|---------|\\n| `handleSelectCompany` | GET /cmmc | List enrolled companies |\\n| `handleEnrollCompany` | POST /api/cmmc/enroll | Add company to CMMC tracking |\\n| `handleDashboard` | GET /cmmc/{company_id} | Compliance dashboard |\\n| `handleControls` | GET /cmmc/{company_id}/controls | Control list with status |\\n| `handleControlDetail` | GET /cmmc/{company_id}/controls/{control_ref} | Evidence, POA&amp;M, module mappings |\\n| `handlePOAM` | GET /cmmc/{company_id}/poam | POA&amp;M board |\\n| `handleCUIScope` | GET /cmmc/{company_id}/cui-scope | CUI scope map |\\n| `handleSSP` | GET /cmmc/{company_id}/ssp | System Security Plan |\\n| `handleAssessments` | GET /cmmc/{company_id}/assessments | Assessment records |\\n| `handleCompiledBinder` | GET /cmmc/{company_id}/binder | Print-ready compliance binder |\\n| `handleDispositions` | GET /cmmc/{company_id}/dispositions | Gap disposition workflow |\\n| `handleRiskAcceptancePrint` | GET /cmmc/{company_id}/risk-acceptance/print | Risk Acceptance Statements |\\n| `handleConnect` | GET /cmmc/{company_id}/connect | One-Click Connect device grid |\\n| `handleActivate` | POST /api/cmmc/{company_id}/activate | Activate compliance pipeline |\\n\\n### Evidence Collection\\n\\n| Function | Source | Controls |\\n|----------|--------|----------|\\n| `collectRBACEvidence` | users table | AC.L1-3.1.1, IA.L1-3.5.1, IA.L1-3.5.2, IA.L2-3.5.3, AC.L2-3.1.4, AC.L2-3.1.5 |\\n| `collectSIEMEvidence` | siem_events | AU.L2-3.3.1, AU.L2-3.3.2, SI.L1-3.14.5 |\\n| `collectRMMEvidence` | agents.compliance_data | CM.L2-3.4.1, AC.L2-3.1.19, SC.L2-3.13.16, SI.L1-3.14.2, SI.L1-3.14.4, SI.L1-3.14.1 |\\n| `collectMetasploitEvidence` | vulnerability_scans | RA.L2-3.11.1, RA.L2-3.11.2 |\\n| `collectHelpdeskEvidence` | tickets | IR.L2-3.6.1, IR.L2-3.6.2 |\\n| `collectOrchestratorEvidence` | orchestrator_action_log | CM.L2-3.4.3 |\\n\\n### Nexie AI\\n\\n| Handler | Purpose |\\n|---------|---------|\\n| `handleNexieIngest` | Upload docs; extract branding &amp; control mappings |\\n| `handleNexieGapAnalysis` | AI-powered gap assessment |\\n| `handleNexieFillGaps` | Generate branded policy docs for unmet controls |\\n| `handleNexieAutoRemediate` | Push RMM fixes for technical controls |\\n| `handleNexieJobStatus` | Poll async job status |\\n\\n---\\n\\n## Utility Functions\\n\\n- **`extractDomain(ref string)`** \u2014 Extract domain code from control ref (e.g., \\\"AC.L2-3.1.3\\\" \u2192 \\\"AC\\\")\\n- **`DomainMap()`** \u2014 Return map of domain codes to domain metadata\\n- **`Domains()`** \u2014 Return slice of all 14 CMMC domains\\n- **`policyBodyToHTML(body string)`** \u2014 Convert markdown-like policy text to HTML\\n- **`renderRiskAcceptanceSection()`** \u2014 Render Risk Acceptance Statements for binder\\n- **`renderRiskAcceptanceHTML()`** \u2014 Standalone Risk Acceptance binder\\n- **`extractJSON(response string)`** \u2014 Extract JSON from Claude response (handles markdown code blocks)\\n- **`extractDOCXTextCMMC(path string)`** \u2014 Extract text from DOCX files\\n\\n---\\n\\n## Configuration &amp; Dependencies\\n\\n### Required\\n- PostgreSQL database with CMMC schema\\n- `compliance_controls` table (NIST 800-171 control definitions)\\n- Auth middleware for tenant/user context\\n\\n### Optional\\n- Claude AI provider (for Nexie features)\\n- Infra module (for One-Click Connect)\\n- Risk service (for Risk Acceptance dual-write)\\n- SIEM, RMM, Metasploit, Helpdesk, Orchestrator modules (for evidence collection)\\n\\n### Initialization\\n\\n```go\\n// Create handler\\nhandler := cmmc.NewHandler(pool, renderer)\\n\\n// Inject AI provider (optional)\\nhandler.SetAI(aiProvider)\\n\\n// Register routes\\nhandler.RegisterRoutes(mux)\\n\\n// Register One-Click Connect (requires infra handler)\\nhandler.RegisterConnectRoutes(mux, infraHandler)\\n```\\n\\n---\\n\\n## Common Workflows\\n\\n### Enroll a Company\\n1. User selects company from \\\"Available\\\" list\\n2. POST `/api/cmmc/enroll` creates initial self-assessment record\\n3. Redirect to dashboard (empty state)\\n\\n### Collect Evidence\\n1. User clicks \\\"Collect Evidence\\\" on dashboard\\n2. POST `/api/cmmc/{company_id}/evidence/collect` triggers `collectAutoEvidence()`\\n3. Each module helper queries its source and upserts evidence\\n4. Dashboard refreshes with new evidence counts\\n\\n### Generate SSP\\n1. User clicks \\\"Generate SSP\\\" on SSP page\\n2. POST `/api/cmmc/{company_id}/ssp/generate` builds sections from dashboard data\\n3. Upserts sections into `cmmc_ssp_sections` with auto_generated = true\\n4. User can override sections with custom content\\n\\n### Accept a Risk\\n1. User reviews gap in Dispositions page\\n2. Selects \\\"Accept\\\" disposition, enters justification, sets expiration date\\n3. POST `/api/cmmc/{company_id}/poam/{id}/disposition` updates cmmc_poam\\n4. Dual-writes to risk_acceptances table\\n5. Risk Acceptance Statement generated for signature\\n\\n### Generate Compliance Binder\\n1. User clicks \\\"Download Binder\\\" on dashboard\\n2. GET `/cmmc/{company_id}/binder` renders HTML with:\\n   - Company branding (from cmmc_company_branding)\\n   - All policy documents (evidence with type = 'document')\\n   - Risk Acceptance Register (all accepted risks)\\n3. User prints to PDF or saves as HTML\\n\\n---\\n\\n## Error Handling\\n\\nThe module follows a best-effort pattern:\\n- Evidence collection failures don't block other sources\\n- One-Click Connect steps continue even if earlier steps fail\\n- AI operations fail gracefully with user-facing error messages\\n- Database errors are logged but don't crash the handler\\n\\n---\\n\\n## Performance Considerations\\n\\n- **Dashboard scoring** \u2014 Queries all 110 controls; consider caching for large tenants\\n- **Evidence collection** \u2014 Runs sequentially; could be parallelized with goroutines\\n- **Binder rendering** \u2014 Builds large HTML string; suitable for on-demand generation\\n- **Nexie operations** \u2014 Async with in-memory job tracking; consider persistent job queue for production\\n\\n---\\n\\n## Testing Notes\\n\\nKey test scenarios:\\n- Evidence collection from each module (mock data in source tables)\\n- Control scoring with various evidence/POA&amp;M combinations\\n- Disposition workflow (remediate, accept, transfer, defer)\\n- Risk Acceptance dual-write to risk service\\n- Binder rendering with and without branding\\n- One-Click Connect with mixed device verification states\\n- Nexie document ingestion and gap analysis\",\"internal-compliance\":\"# internal \u2014 compliance\\n\\n# Compliance Module Documentation\\n\\n## Overview\\n\\nThe `internal/compliance` module manages compliance document lifecycle, framework tracking, device compliance scoring, and AI-powered gap analysis for the NexusOS PSA platform. It bridges policy documentation with automated device compliance checks, enabling MSPs to track their clients' adherence to industry frameworks (SOC2, HIPAA, NIST, PCI-DSS, etc.).\\n\\nThe module handles:\\n- **Document management**: Upload, analyze, and compile compliance documents\\n- **Framework tracking**: Define and assign compliance frameworks to companies\\n- **Device compliance**: Persist and score device check results from RMM agents\\n- **Gap analysis**: AI-powered identification of compliance gaps and auto-generation of remediation policies\\n- **Approval workflows**: Public token-based approval for compliance documents\\n- **Unified scoring**: Combine device checks and document analysis into a single compliance score\\n\\n## Architecture\\n\\n### Core Components\\n\\n#### Handler\\nThe `Handler` struct is the main entry point, managing HTTP endpoints and orchestrating database operations:\\n\\n```go\\ntype Handler struct {\\n    pool       *pgxpool.Pool      // Database connection pool\\n    renderer   *ui.Renderer       // HTML template renderer\\n    aiProvider *ai.Provider       // Claude API integration\\n    uploadDir  string             // Disk storage for uploaded documents\\n    remJobs    map[string]*remediationJob  // In-progress remediation tracking\\n}\\n```\\n\\nThe handler is initialized via `NewHandler()` and routes are registered with `RegisterRoutes()`. AI capabilities are optional and set via `SetAI()`.\\n\\n#### Data Types\\nThe module defines domain types in `types.go`:\\n\\n- **Document**: Uploaded compliance documents with metadata, status, and framework linkage\\n- **Framework**: Industry compliance frameworks (SOC2, HIPAA, etc.) with control definitions\\n- **ComplianceAssignment**: Links frameworks to companies for tracking\\n- **DeviceComplianceResult**: Parsed compliance report from an agent, including per-framework scores and individual check results\\n- **ComplianceControl**: A single control requirement with device check mapping\\n- **GapAnalysis**: AI-generated gap findings with remediation suggestions\\n\\n### Database Schema Integration\\n\\nThe module assumes these tables:\\n- `compliance_documents` \u2014 uploaded documents, analysis results, approval status\\n- `compliance_frameworks` \u2014 framework definitions and metadata\\n- `compliance_controls` \u2014 control requirements with device check type mappings\\n- `compliance_assignments` \u2014 company-to-framework assignments\\n- `compliance_evidence` \u2014 device check results (upserted during RMM sync)\\n- `compliance_gap_analyses` \u2014 gap analysis records\\n- `compliance_audit_log` \u2014 audit trail for compliance actions\\n- `agents` \u2014 RMM agents with metadata containing compliance reports\\n- `companies` \u2014 client companies\\n- `tenants` \u2014 MSP tenants\\n\\n## Key Workflows\\n\\n### 1. Document Upload &amp; Text Extraction\\n\\n**Endpoint**: `POST /api/compliance/documents/upload`\\n\\n```\\nUser uploads file (PDF, DOCX, TXT, MD)\\n    \u2193\\nsaveUploadedFile() \u2192 disk storage\\n    \u2193\\nextractDocumentText() \u2192 format-specific extraction\\n    \u251c\u2500 .txt/.md \u2192 read file directly\\n    \u251c\u2500 .docx \u2192 extractDOCXText() \u2192 parse word/document.xml\\n    \u2514\u2500 .pdf \u2192 stub message (extraction not yet implemented)\\n    \u2193\\nInsert into compliance_documents with text_extracted flag\\n```\\n\\nDOCX extraction uses Go's `archive/zip` to read the embedded XML, then parses `` elements while preserving paragraph structure.\\n\\n### 2. AI Gap Analysis\\n\\n**Endpoint**: `POST /api/compliance/documents/{id}/analyze`\\n\\n```\\nLoad document content + framework controls\\n    \u2193\\nMark as ai_analysis_status = 'running'\\n    \u2193\\nReturn 202 Accepted immediately\\n    \u2193\\nBackground goroutine:\\n    \u251c\u2500 Call aiProvider.AnalyzeComplianceGaps()\\n    \u251c\u2500 Parse result (gaps, covered controls, score)\\n    \u251c\u2500 Save to gap_analysis JSONB column\\n    \u2514\u2500 Update status to 'complete'\\n```\\n\\nThe AI provider receives the document text, framework name, and list of control references, then returns structured gap findings.\\n\\n### 3. AI Remediation (Batch Processing)\\n\\n**Endpoint**: `POST /api/compliance/documents/{id}/remediate`\\n\\nThis is the most complex workflow, split into helpers for clarity:\\n\\n```\\nLoad gap analysis results\\n    \u2193\\nCreate remediationJob tracker\\n    \u2193\\nReturn 202 Accepted with job status\\n    \u2193\\nBackground goroutine (runRemediationJob):\\n    \u251c\u2500 Build system prompt (company + framework context)\\n    \u251c\u2500 Split gaps into batches (3 per batch)\\n    \u251c\u2500 For each batch:\\n    \u2502   \u251c\u2500 processRemediationBatch()\\n    \u2502   \u251c\u2500 Call Claude with system prompt + gap descriptions\\n    \u2502   \u251c\u2500 Parse JSON response (title, control_ref, content)\\n    \u2502   \u251c\u2500 Insert each policy as child document\\n    \u2502   \u2514\u2500 Update job.BatchesDone\\n    \u251c\u2500 updateParentGapScore()\\n    \u2502   \u251c\u2500 Move remediated gaps to \\\"Covered\\\" list\\n    \u2502   \u251c\u2500 Recalculate compliance score\\n    \u2502   \u2514\u2500 Save updated gap_analysis\\n    \u2514\u2500 Log audit entry\\n```\\n\\n**Frontend polling**: `GET /api/compliance/documents/{id}/remediate-status` returns current job progress.\\n\\nThe system prompt is carefully crafted to ensure Claude returns valid JSON with exact control references, which are then used to reconcile the parent document's gap analysis.\\n\\n### 4. Device Compliance Persistence\\n\\n**Called during RMM sync** (not a direct HTTP endpoint)\\n\\n```\\nPersistDeviceEvidence(tenantID, companyID):\\n    \u251c\u2500 loadAssignedFrameworks() \u2192 get active frameworks for company\\n    \u251c\u2500 For each agent in company:\\n    \u2502   \u251c\u2500 parseAgentCompliance() \u2192 extract checks from agent metadata\\n    \u2502   \u2514\u2500 persistAgentDeviceEvidence():\\n    \u2502       \u2514\u2500 For each assigned framework:\\n    \u2502           \u2514\u2500 upsertAgentFrameworkEvidence():\\n    \u2502               \u251c\u2500 Load controls with agent_check_type\\n    \u2502               \u251c\u2500 Match device checks to controls\\n    \u2502               \u2514\u2500 Upsert compliance_evidence rows\\n    \u2514\u2500 ComputeCompanyScores() \u2192 aggregate into cybersecurity_scores\\n```\\n\\nThis creates an audit trail of device compliance checks, enabling historical tracking and unified scoring.\\n\\n### 5. Unified Compliance Scoring\\n\\n**Endpoint**: `GET /api/compliance/unified-score/{company_id}/{framework_id}`\\n\\n```\\nGetUnifiedComplianceScore():\\n    \u251c\u2500 Query compliance_evidence for device checks\\n    \u2502   \u2514\u2500 deviceScore = (passed / total) * 100\\n    \u251c\u2500 Query compliance_documents for document analysis\\n    \u2502   \u2514\u2500 docScore = stored score from gap analysis\\n    \u2514\u2500 Compute weighted average:\\n        \u2514\u2500 if both exist: (device * 40% + doc * 60%)\\n           else: whichever exists\\n```\\n\\nThis combines automated device checks (40% weight) with policy documentation analysis (60% weight) for a holistic compliance view.\\n\\n### 6. Document Compilation &amp; Approval\\n\\n**Endpoint**: `GET /api/compliance/documents/{id}/compile`\\n\\n```\\nloadCompileDocData():\\n    \u251c\u2500 Load parent document (title, framework, company, score)\\n    \u251c\u2500 Load company metadata (address, phone, email)\\n    \u251c\u2500 Load framework metadata (source org, version, URL)\\n    \u251c\u2500 Load MSP/tenant name\\n    \u2514\u2500 Load all child policy documents\\n        \u2193\\nrenderCompileDocHead() \u2192 DOCTYPE + inline CSS + print button\\n        \u2193\\nrenderCompileDocBody():\\n    \u251c\u2500 Cover page (company name, score badge, metadata)\\n    \u251c\u2500 Table of Contents (linked to policies)\\n    \u251c\u2500 Per-policy sections:\\n    \u2502   \u251c\u2500 Title + control reference\\n    \u2502   \u2514\u2500 simpleMarkdownToHTML() \u2192 convert markdown to HTML\\n    \u251c\u2500 Framework source information block\\n    \u2514\u2500 Footer with generation timestamp\\n```\\n\\nThe compiled HTML is print-optimized with inline CSS and page breaks. It can be saved as PDF via browser print dialog.\\n\\n**Approval flow**:\\n```\\nhandleSendForApproval():\\n    \u251c\u2500 Generate random approval token\\n    \u251c\u2500 Save token to compliance_documents\\n    \u2514\u2500 Return approval URL (/compliance/approve/{token})\\n        \u2193\\nhandleApprovalPage() [no auth required]:\\n    \u251c\u2500 Load document by token\\n    \u251c\u2500 compileDocumentHTML() \u2192 generate HTML\\n    \u2514\u2500 Render approval form\\n        \u2193\\nhandleApproveDocument():\\n    \u251c\u2500 Validate action (approve/decline)\\n    \u251c\u2500 Require name + signature\\n    \u2514\u2500 Update approval_status + approved_at + approved_by\\n```\\n\\n## Device Compliance Check Mapping\\n\\nThe module maps device checks to framework controls via the `agent_check_type` field in `compliance_controls`. Standard device checks include:\\n\\n- `firewall` \u2014 firewall enabled/configured\\n- `disk_encryption` \u2014 full-disk encryption status\\n- `screen_lock` \u2014 screen lock policy\\n- `audit_logging` \u2014 audit logging enabled\\n- `auto_update` \u2014 automatic updates enabled\\n- `antivirus` \u2014 antivirus/EDR installed\\n- `secure_boot` \u2014 secure boot enabled\\n- `admin_accounts` \u2014 admin account hardening\\n\\nWhen an agent reports compliance metadata, `parseAgentCompliance()` extracts these checks and `PersistDeviceEvidence()` matches them to controls, creating an evidence trail.\\n\\n## AI Integration\\n\\nThe module integrates with `internal/ai` for gap analysis and remediation:\\n\\n```go\\naiProvider.AnalyzeComplianceGaps(ctx, tenantID, frameworkName, title, content, controls)\\n    \u2192 ComplianceGapResult { Gaps, Covered, Score }\\n\\naiProvider.CallClaudeRaw(ctx, apiKey, systemPrompt, userMsg, maxTokens)\\n    \u2192 raw response string\\n```\\n\\nThe AI provider is optional; endpoints return 503 if not configured.\\n\\n## HTTP Routes\\n\\n### Pages (HTML)\\n- `GET /compliance/documents` \u2014 document list\\n- `GET /compliance/frameworks` \u2014 framework list\\n- `GET /compliance/gap-analysis` \u2014 gap analysis history\\n- `GET /compliance/client/{company_id}` \u2014 company compliance dashboard\\n- `GET /compliance/devices/{company_id}/{agent_id}` \u2014 device detail\\n- `GET /compliance/approve/{token}` \u2014 public approval page (no auth)\\n\\n### API (JSON)\\n- `POST /api/compliance/documents` \u2014 create document\\n- `PUT /api/compliance/documents/{id}` \u2014 update document\\n- `DELETE /api/compliance/documents/{id}` \u2014 delete document\\n- `POST /api/compliance/documents/upload` \u2014 upload file\\n- `POST /api/compliance/documents/{id}/analyze` \u2014 trigger gap analysis\\n- `GET /api/compliance/documents/{id}/gap-results` \u2014 fetch gap analysis\\n- `POST /api/compliance/documents/{id}/remediate` \u2014 start remediation\\n- `GET /api/compliance/documents/{id}/remediate-status` \u2014 poll remediation progress\\n- `GET /api/compliance/documents/{id}/compile` \u2014 generate compiled HTML\\n- `POST /api/compliance/documents/{id}/send-approval` \u2014 generate approval token\\n- `POST /compliance/approve/{token}` \u2014 submit approval/decline\\n- `GET /api/compliance/scores` \u2014 tenant compliance scores\\n- `POST /api/compliance/gap-analysis` \u2014 create gap analysis record\\n- `POST /api/compliance/frameworks` \u2014 create framework\\n- `PUT /api/compliance/frameworks/{id}` \u2014 update framework\\n- `POST /api/compliance/assignments` \u2014 assign framework to company\\n- `DELETE /api/compliance/assignments/{id}` \u2014 remove assignment\\n- `GET /api/compliance/device-scores/{company_id}` \u2014 device scores JSON\\n- `GET /api/compliance/unified-score/{company_id}/{framework_id}` \u2014 unified score\\n- `GET /api/compliance/device-evidence/{company_id}/{framework_id}` \u2014 evidence details\\n- `GET /api/compliance/documents/{company_id}/list` \u2014 company documents JSON\\n\\n## Helper Functions\\n\\n### Text Extraction\\n- `extractDOCXText(filePath)` \u2014 reads DOCX ZIP, parses word/document.xml\\n- `extractTextFromWordXML(data)` \u2014 XML parsing with paragraph preservation\\n- `extractDocumentText(ext, destPath, filename)` \u2014 dispatcher by file type\\n\\n### Compilation &amp; Rendering\\n- `loadCompileDocData(ctx, tenantID, docID)` \u2014 5 DB queries for compile data\\n- `renderCompileDocHead(sb, data)` \u2014 DOCTYPE + CSS + print button\\n- `renderCompileDocBody(sb, data)` \u2014 cover + TOC + policies + footer\\n- `simpleMarkdownToHTML(md)` \u2014 basic markdown to HTML (headings, lists, bold)\\n- `boldify(s)` \u2014 convert `**text**` to `text&lt;\\/strong&gt;`\\n- `compileDocumentHTML(ctx, docID, tenantID)` \u2014 reusable compile for approval page\\n\\n### Device Compliance\\n- `parseAgentCompliance(metadataJSON)` \u2014 extract compliance report from agent metadata\\n- `getCompanyDeviceCompliance(r, tenantID, companyID)` \u2014 load all agents + parse\\n- `getCompanyAssignments(r, tenantID, companyID)` \u2014 load active framework assignments\\n- `getFrameworkControls(r, frameworkID)` \u2014 load controls for a framework\\n- `getAvailableFrameworks(r, tenantID)` \u2014 load all frameworks (tenant + global)\\n\\n### Remediation\\n- `buildRemediationSystemPrompt(companyName, frameworkName)` \u2192 system prompt template\\n- `processRemediationBatch(ctx, job, apiKey, systemPrompt, ...)` \u2014 one Claude call + parse + insert\\n- `updateParentGapScore(ctx, docID, generated)` \u2014 reconcile parent doc after remediation\\n- `runRemediationJob(job, apiKey, ...)` \u2014 orchestrate batch loop + audit log\\n\\n### Evidence &amp; Scoring\\n- `loadAssignedFrameworks(ctx, tenantID, companyID)` \u2192 active framework IDs\\n- `persistAgentDeviceEvidence(ctx, tenantID, companyID, agentID, hostname, metaJSON, frameworks)` \u2014 parse + dispatch\\n- `upsertAgentFrameworkEvidence(ctx, tenantID, companyID, frameworkID, agentID, hostname, checkResults)` \u2014 match controls + upsert\\n- `GetUnifiedComplianceScore(ctx, tenantID, companyID, frameworkID)` \u2192 (device, doc, unified)\\n- `ComputeCompanyScores(tenantID, companyID)` \u2014 aggregate device scores into cybersecurity_scores\\n- `GetClientComplianceSummary(r, tenantID, companyID)` \u2192 summary for CRM client detail\\n\\n### Approval\\n- `generateApprovalToken()` \u2014 random 64-char hex string\\n- `handleSendForApproval()` \u2014 create token + return URL\\n- `handleApprovalPage()` \u2014 render public approval form\\n- `handleApproveDocument()` \u2014 process approval/decline\\n\\n## Tenant Isolation\\n\\nAll endpoints enforce tenant isolation via `auth.TenantIDFromContext()`. Database queries filter by `tenant_id` to prevent cross-tenant data leakage. The approval flow is an exception: the approval page is public (secured by the random token), but the token lookup still validates the document belongs to the correct tenant.\\n\\n## Error Handling\\n\\n- **Missing AI provider**: Returns 503 Service Unavailable\\n- **Missing document**: Returns 404 Not Found\\n- **Invalid file type**: Returns 400 Bad Request\\n- **Database errors**: Returns 500 Internal Server Error with generic message\\n- **Parse failures**: Logged but don't block; fallback to stub messages (e.g., \\\"PDF extraction coming soon\\\")\\n\\n## Integration Points\\n\\n### With `internal/auth`\\n- `TenantIDFromContext()` \u2014 extract tenant from request context\\n- `UserIDFromContext()` \u2014 extract user for audit logging\\n\\n### With `internal/ui`\\n- `Renderer.Render()` \u2014 render HTML templates with data\\n\\n### With `internal/ai`\\n- `Provider.AnalyzeComplianceGaps()` \u2014 AI gap analysis\\n- `Provider.CallClaudeRaw()` \u2014 raw Claude API calls for remediation\\n\\n### With RMM Sync\\n- `PersistDeviceEvidence()` \u2014 called during agent metadata sync to persist compliance checks\\n- `ComputeCompanyScores()` \u2014 called to aggregate scores into cybersecurity_scores\\n\\n## Future Enhancements\\n\\n- PDF text extraction (currently stubbed)\\n- Async gap analysis job queue (currently synchronous)\\n- Control evidence linking (manual document uploads as evidence)\\n- Compliance timeline/history view\\n- Bulk framework import from standards bodies\\n- Custom control definitions per tenant\",\"internal-config\":\"# internal \u2014 config\\n\\n# internal/config Module\\n\\n## Overview\\n\\nThe `config` module is responsible for loading and validating NexusOS PSA server configuration at startup. It reads settings from environment variables with sensible defaults for local development, ensuring the application has all required parameters before any subsystems initialize.\\n\\nConfiguration is loaded once during application startup (from `cmd/psa/main.go`) and passed to components that need it. This single-load pattern prevents configuration drift and makes the system's runtime state predictable.\\n\\n## Core Components\\n\\n### Config Struct\\n\\n`Config` is the central data structure holding all PSA server settings:\\n\\n```go\\ntype Config struct {\\n    ListenAddr        string  // HTTP server bind address (e.g., \\\":3000\\\")\\n    DatabaseURL       string  // PostgreSQL connection string\\n    JWTPrivateKeyPath string  // RSA private key for JWT signing\\n    JWTPublicKeyPath  string  // RSA public key for JWT verification\\n    RMMListenAddr     string  // mTLS agent listener bind address (e.g., \\\":8443\\\")\\n    RMMCertPath       string  // Server TLS certificate for agent listener\\n    RMMKeyPath        string  // Server TLS private key for agent listener\\n    RMMCACertPath     string  // CA certificate for verifying agent clients\\n    RMMCAKeyPath      string  // CA private key for signing agent certificates\\n    SIEMTLSCertPath   string  // TLS certificate for encrypted syslog (port 6514)\\n    SIEMTLSKeyPath    string  // TLS private key for encrypted syslog\\n    SIEMEncryptionKey string  // 32-byte hex-encoded AES-256-GCM key\\n    DevMode           bool    // Auto-authenticate as first active user (dev only)\\n    PublicURL         string  // Canonical public origin (scheme + host[:port])\\n    RecorderStorageRoot string // Directory for RMM session recordings\\n}\\n```\\n\\nEach field corresponds to a server subsystem or feature. The struct is immutable after loading\u2014there is no runtime mutation.\\n\\n### Load Function\\n\\n`Load()` is the entry point for configuration initialization:\\n\\n```go\\nfunc Load() (*Config, error)\\n```\\n\\n**Behavior:**\\n1. Reads environment variables using `envOr()` helper\\n2. Applies sensible defaults for local development\\n3. Validates critical constraints (e.g., `DATABASE_URL` is required)\\n4. Enforces security guardrails (e.g., `DevMode` forbidden in production)\\n5. Returns a fully initialized `Config` or an error\\n\\n**Validation Rules:**\\n- `DATABASE_URL` must be set (no default)\\n- `DevMode=true` is rejected if `ENVIRONMENT=production` (audit finding H4, 2026-04-29)\\n\\nThe function is called once at application startup before any subsystems initialize.\\n\\n### envOr Helper\\n\\n```go\\nfunc envOr(key, defaultVal string) string\\n```\\n\\nA simple utility that returns an environment variable's value if set and non-empty, otherwise returns the provided default. This pattern centralizes the fallback logic and makes defaults explicit in the code.\\n\\n## Configuration Sources &amp; Defaults\\n\\nSettings are resolved in this order:\\n1. Environment variable (if set and non-empty)\\n2. Hardcoded default (if provided)\\n3. Error (if required and missing)\\n\\n### Development Defaults\\n\\nFor local development, sensible defaults are provided:\\n\\n| Setting | Default |\\n|---------|---------|\\n| `LISTEN_ADDR` | `:3000` |\\n| `DATABASE_URL` | `postgres://nexusos:nexusos-dev@localhost:5432/nexusos?sslmode=disable` |\\n| `JWT_PRIVATE_KEY_PATH` | `keys/jwt.pem` |\\n| `JWT_PUBLIC_KEY_PATH` | `keys/jwt.pub` |\\n| `RMM_LISTEN_ADDR` | `:8443` |\\n| `RMM_CERT_PATH` | `../rmm-lite/certs/server.crt` |\\n| `RMM_KEY_PATH` | `../rmm-lite/certs/server.key` |\\n| `RMM_CA_CERT_PATH` | `../rmm-lite/certs/ca.crt` |\\n| `RMM_CA_KEY_PATH` | `../rmm-lite/certs/ca.key` |\\n| `RECORDER_STORAGE_ROOT` | `/opt/nexusos/recordings` |\\n\\nSIEM settings (`SIEM_TLS_CERT_PATH`, `SIEM_TLS_KEY_PATH`, `SIEM_ENCRYPTION_KEY`) default to empty strings and are optional.\\n\\n## Key Features\\n\\n### PublicURL Handling\\n\\nThe `PublicURL` field stores the canonical public origin (e.g., `https://nexus.horizonmanaged.com`) that the PSA answers on. When set, it is preferred over request-derived `Host` headers for URLs baked into agent artifacts\u2014most notably the per-token MSI installer.\\n\\n**Why this matters:** If the server origin is wrong, agents download from the wrong location (e.g., localhost instead of the production domain). Setting `PublicURL` explicitly prevents this misconfiguration.\\n\\nIn development, leave `PUBLIC_URL` empty to let request inspection drive the value.\\n\\n### DevMode Security Guardrail\\n\\n`DevMode` enables auto-authentication as the first active user when no JWT token is present. This is convenient for local development but catastrophic in production.\\n\\nThe module enforces a critical safety check:\\n```go\\nif cfg.DevMode &amp;&amp; strings.EqualFold(envOr(\\\"ENVIRONMENT\\\", \\\"\\\"), \\\"production\\\") {\\n    return nil, fmt.Errorf(\\\"DEV_MODE=true is forbidden when ENVIRONMENT=production \u2014 refusing to start\\\")\\n}\\n```\\n\\nOperators set `ENVIRONMENT=production` in the systemd unit, ensuring a stray `DEV_MODE=true` never silently opens the platform to unauthenticated access.\\n\\n### Recorder Storage\\n\\n`RecorderStorageRoot` specifies where the RMM recorder writes uploaded session segments. The layout is:\\n```\\n{root}/{tenantID}/{sessionID}/segment-NNNN.webm\\n```\\n\\nDefaults to `/opt/nexusos/recordings` on production; override (e.g., `./var/recordings`) for local development.\\n\\n## Integration with the Application\\n\\nThe configuration flow is straightforward:\\n\\n```\\ncmd/psa/main.go\\n    \u2193\\nconfig.Load()\\n    \u2193\\nConfig struct (passed to subsystems)\\n```\\n\\n`main()` calls `Load()` at startup. If `Load()` returns an error, the application exits immediately. Otherwise, the returned `Config` is passed to subsystems that need it (HTTP server, database, JWT handlers, RMM listener, SIEM, recorder, etc.).\\n\\nBecause configuration is loaded once and never mutated, all subsystems see a consistent view of settings throughout the application's lifetime.\\n\\n## Error Handling\\n\\n`Load()` returns an error in two cases:\\n\\n1. **Missing required setting:** `DATABASE_URL` is required and has no default.\\n2. **Security violation:** `DevMode=true` with `ENVIRONMENT=production`.\\n\\nBoth errors are fatal and prevent the application from starting. This fail-fast approach ensures the system never runs in an invalid state.\\n\\n## Environment Variables Reference\\n\\n| Variable | Type | Required | Default | Purpose |\\n|----------|------|----------|---------|---------|\\n| `LISTEN_ADDR` | string | No | `:3000` | HTTP server bind address |\\n| `DATABASE_URL` | string | **Yes** | \u2014 | PostgreSQL connection string |\\n| `JWT_PRIVATE_KEY_PATH` | string | No | `keys/jwt.pem` | JWT signing key path |\\n| `JWT_PUBLIC_KEY_PATH` | string | No | `keys/jwt.pub` | JWT verification key path |\\n| `RMM_LISTEN_ADDR` | string | No | `:8443` | mTLS agent listener address |\\n| `RMM_CERT_PATH` | string | No | `../rmm-lite/certs/server.crt` | Agent listener TLS cert |\\n| `RMM_KEY_PATH` | string | No | `../rmm-lite/certs/server.key` | Agent listener TLS key |\\n| `RMM_CA_CERT_PATH` | string | No | `../rmm-lite/certs/ca.crt` | CA cert for agent verification |\\n| `RMM_CA_KEY_PATH` | string | No | `../rmm-lite/certs/ca.key` | CA key for agent enrollment |\\n| `SIEM_TLS_CERT_PATH` | string | No | (empty) | Syslog listener TLS cert |\\n| `SIEM_TLS_KEY_PATH` | string | No | (empty) | Syslog listener TLS key |\\n| `SIEM_ENCRYPTION_KEY` | string | No | (empty) | AES-256-GCM key (32 bytes, hex) |\\n| `DEV_MODE` | bool | No | `false` | Enable dev auto-auth (forbidden in prod) |\\n| `PUBLIC_URL` | string | No | (empty) | Canonical public origin |\\n| `RECORDER_STORAGE_ROOT` | string | No | `/opt/nexusos/recordings` | Session recording directory |\\n| `ENVIRONMENT` | string | No | (empty) | Environment name (checked against `production`) |\",\"internal-crm\":\"# internal \u2014 crm\\n\\n# internal/crm Module\\n\\nThe CRM module provides a multi-tenant customer relationship management system for managing companies, contacts, activities, deals, campaigns, and related business entities. It handles the full lifecycle of client data\u2014from initial contact through deal closure\u2014with strict tenant isolation and comprehensive audit controls.\\n\\n## Overview\\n\\nThe module is organized around **entity handlers** (accounts, contacts, activities, deals, campaigns) and **cross-cutting concerns** (email security, compliance, form rendering). Each handler exposes HTTP endpoints for CRUD operations and list views, with dynamic SQL filtering, tenant scoping, and role-based access control.\\n\\nKey characteristics:\\n- **Multi-tenant by design**: Every query filters by `tenant_id`; tenant gates are enforced at both application and database levels\\n- **Audit-hardened**: Tenant isolation is documented in `docs/AUDIT_CRM_TENANT_SCOPE.md`; critical operations include pre-checks before writes\\n- **Flexible filtering**: List endpoints support dynamic SQL with optional filters (search, company, stage, etc.)\\n- **Rich data aggregation**: Detail pages (Client 360\u00b0, Contact Detail) dispatch to specialized loaders that populate 20+ fields from multiple tables\\n\\n## Core Entities\\n\\n### Companies (Accounts)\\n\\n**Tables**: `companies`, `company_tags`, `client_locations`, `client_surveys`, `client_roadmap_items`\\n\\nA company represents a client or prospect. The core record holds:\\n- Basic info: name, website, industry, employee count, annual revenue\\n- Lifecycle: stage (prospect/customer/churned), health score, contract value\\n- Relationships: account manager, escalation policy, SLA policy\\n- Compliance: assigned frameworks, onboarding status\\n\\n**Key handlers**:\\n- `handleClientList` \u2014 paginated list with filters (search, lifecycle stage, industry, tags, company type)\\n- `handleClientDetail` \u2014 Client 360\u00b0 dashboard aggregating 8 data blocks (contacts, tickets, commercial, activities, deals, agents, locations, compliance)\\n- `handleUpdateClient` \u2014 dynamic field updates via allowlist (`clientUpdateAllowedFields`)\\n- `handleAddTag` / `handleRemoveTag` \u2014 tag management with tenant gates\\n- `handleAssignEscalationPolicy` / `handleAssignSLAPolicy` \u2014 policy assignment\\n\\n**Location CRUD**:\\n- `handleListLocations`, `handleCreateLocation`, `handleUpdateLocation`, `handleDeleteLocation` \u2014 manage client sites/offices\\n\\n### Contacts\\n\\n**Tables**: `contacts`, `portal_users`, `portal_roles`, `agents` (assigned_contact_id)\\n\\nA contact is a person at a company. Fields include:\\n- Identity: first/last name, email, phone, mobile\\n- Role: title, department, contact type (general/technical/executive)\\n- Preferences: preferred contact method, channel (Teams/Slack/WhatsApp/Discord/Messenger)\\n- Portal access: enabled flag, portal user record, role assignment\\n- EPA: Nexie EPA telemetry flag, assigned RMM agent\\n\\n**Key handlers**:\\n- `handleContactList` \u2014 filtered list (search, company, contact type)\\n- `handleContactDetail` \u2014 detail page with activities, portal state, EPA device, available agents\\n- `handleCreateContact` / `handleUpdateContact` / `handleDeleteContact` \u2014 CRUD\\n- `handleToggleContactNexieEPA` \u2014 admin-only flag to enable EPA telemetry\\n- `handleAssignContactAgent` \u2014 bind an RMM agent to a contact (transactional, one-per-contact)\\n\\n### Activities\\n\\n**Tables**: `crm_activities`\\n\\nAn activity is a timestamped record of interaction: call, email, meeting, task, etc.\\n\\nFields:\\n- Type: activity_type (call/email/meeting/task/note)\\n- Content: subject, description, outcome, follow-up date\\n- Participants: performed_by (user), company_id, contact_id, deal_id, ticket_id\\n- Metadata: direction (inbound/outbound), phone_number, email_subject, duration_minutes\\n- Status: is_completed, completed_at\\n\\n**Key handlers**:\\n- `handleActivityList` \u2014 filtered list with type counts, company/team dropdowns\\n- `handleCreateActivity` \u2014 form or JSON; defaults performed_by to current user, activity_date to now\\n- `handleCompleteActivity` \u2014 mark as done with timestamp\\n\\n### Deals\\n\\n**Tables**: `deals`, `deal_stages`, `deal_lifecycle_events`\\n\\nA deal is a sales opportunity with a value, stage, and owner.\\n\\nFields:\\n- Core: title, description, company_id, contact_id, stage_id\\n- Value: value, currency, recurring_value, win_probability\\n- Lifecycle: owner_id, expected_close_date, closed_at, source, source_detail\\n- Loss tracking: loss_reason, competitor, quote_id\\n\\n**Key handlers**:\\n- `handleCreateDeal` \u2014 form or JSON; defaults owner to current user, currency to USD\\n- `handleUpdateDeal` \u2014 dynamic updates with stage-change detection; emits `OnDealStageChanged` event if stage moves to won/lost\\n- `handleDealTimeline` \u2014 returns lifecycle events (JSON)\\n\\n### Campaigns\\n\\n**Tables**: `email_campaigns`, `campaign_recipients`\\n\\nAn email campaign targets contacts with a message.\\n\\nFields:\\n- Content: name, subject, body_html, body_text, from_name, from_email, reply_to\\n- Type: campaign_type (one_time/recurring), status (draft/sent)\\n- Metrics: total_recipients, total_sent, total_opened, total_clicked, total_bounced, total_unsubscribed\\n- Scheduling: scheduled_at, sent_at\\n\\n**Key handlers**:\\n- `handleCampaignList` \u2014 list recent campaigns\\n- `handleCampaignDetail` \u2014 detail + recipients with status (pending/sent/opened/clicked/bounced/unsubscribed)\\n- `handleCreateCampaign` / `handleUpdateCampaign` \u2014 form or JSON\\n- `handleSendCampaign` \u2014 mark campaign as sent, update recipient statuses; includes tenant gate (RowsAffected check) before touching recipients\\n\\n## Email Security &amp; Compliance\\n\\n### Trusted Senders\\n\\n**Tables**: `trusted_sender_profiles`, `vendors`, `vendor_domains`\\n\\nTrusted senders are email addresses/domains approved for a company. Used to whitelist legitimate vendors and reduce false positives in email security scanning.\\n\\n**Key handlers**:\\n- `handleListTrustedSenders` \u2014 list for a company\\n- `handleCreateTrustedSender` \u2014 add with email, display name, SPF/DKIM/DMARC metadata, raw headers\\n- `handleDeleteTrustedSender` \u2014 remove\\n\\n### Vendor Domains\\n\\nVendor domains link a domain to a vendor (MSP-level, not company-specific).\\n\\n**Key handlers**:\\n- `handleListVendorDomains` \u2014 list for tenant\\n- `handleCreateVendorDomain` \u2014 add domain to vendor\\n- `handleDeleteVendorDomain` \u2014 remove\\n\\n### Email Security Events\\n\\n**Tables**: `email_security_events`\\n\\nEvents are inbound emails flagged for risk (phishing, spoofing, etc.). Each event includes:\\n- Sender: from_address, from_display, from_domain\\n- Content: subject, received_at\\n- Analysis: risk_score, risk_level, SPF/DKIM/DMARC results, Inky classification\\n- Decision: decision (pending/approved/blocked), decided_by, LLM verdict + confidence\\n\\n**Key handlers**:\\n- `handleListEmailSecurityEvents` \u2014 recent events for a company (50 limit)\\n- `handleGetSecurityProfile` \u2014 company security settings + counts (trusted senders, vendor domains, events in last 30d)\\n- `handleUpdateSecurityProfile` \u2014 update active_countries, approved/blocked sender domains, security notes, email security enabled flag, primary email domain, MS365 tenant ID\\n\\n### Client Vendors\\n\\n**Tables**: `client_vendors`\\n\\nClient vendors are the company's own vendors (not MSP vendors). Tracks relationship, contact info, approved domains, header capture status.\\n\\n**Key handlers**:\\n- `handleListCompanyVendors` \u2014 list for a company\\n- `handleAddCompanyVendor` \u2014 create with vendor name, domain, website, category, contact, relationship, approved domains\\n- `handleRemoveCompanyVendor` \u2014 delete\\n\\n## Data Loading Patterns\\n\\n### Filtered List Queries\\n\\nList handlers use dynamic SQL construction to apply optional filters:\\n\\n```go\\n// Example: loadFilteredClients\\nquery := `SELECT ... FROM companies c WHERE c.tenant_id = $1 AND c.is_active = true`\\nargs := []interface{}{tenantID}\\nargIdx := 2\\n\\nif filter.Search != \\\"\\\" {\\n    query += fmt.Sprintf(` AND (c.name ILIKE $%d OR c.contact_email ILIKE $%d)`, argIdx, argIdx)\\n    args = append(args, \\\"%\\\"+filter.Search+\\\"%\\\")\\n    argIdx++\\n}\\n// ... more filters\\nquery += ` ORDER BY c.name ASC LIMIT 100`\\n```\\n\\nThis pattern is used by:\\n- `loadFilteredClients` (companies)\\n- `loadFilteredContacts` (contacts)\\n- `loadFilteredActivities` (activities)\\n\\n### Detail Page Aggregation\\n\\nDetail handlers dispatch to specialized loaders, each populating a section of the dashboard:\\n\\n```go\\n// handleClientDetail\\nvar dash ClientDashboard\\nh.loadCompanyCore(ctx, tenantID, companyID, &amp;dash)        // ~40 fields\\nh.loadCompanyContacts(ctx, tenantID, companyID, &amp;dash)    // contacts + count\\nh.loadCompanyTicketsAndTime(ctx, tenantID, companyID, &amp;dash) // tickets, hours\\nh.loadCompanyCommercial(ctx, tenantID, companyID, &amp;dash)  // quotes, invoices, financial\\nh.loadCompanyActivitiesAndDeals(ctx, tenantID, companyID, &amp;dash) // activities, deals\\nh.loadCompanyAgentsAndTags(ctx, tenantID, companyID, &amp;dash) // agents, tags\\nh.loadCompanyLocationsAndExtras(ctx, tenantID, companyID, &amp;dash) // locations, surveys, roadmap\\nh.loadCompanyCompliance(ctx, tenantID, companyID, &amp;dash)  // compliance assignments\\n```\\n\\nEach loader:\\n1. Runs one or more queries (often with LEFT JOINs for optional data)\\n2. Scans into slices or individual fields on the dashboard struct\\n3. Stamps the company name onto related records for template convenience\\n4. Silently ignores query errors (returns empty slices/zero values)\\n\\n### Dropdown Population\\n\\nForm pages load dropdowns via simple queries:\\n\\n```go\\n// loadCompanies, loadUsers, loadDealStages, loadContacts\\nrows, err := h.pool.Query(ctx, \\\"SELECT id, name FROM ... WHERE tenant_id = $1 ORDER BY name\\\")\\n// Scan into []option or []DealStage\\n```\\n\\n## Tenant Isolation\\n\\nEvery query includes `WHERE ... AND tenant_id = $1` or `WHERE ... AND tenant_id = $2`. Critical write operations include pre-checks:\\n\\n**Example: handleAddTag**\\n```go\\n// Pre-check: verify company belongs to tenant before INSERT\\nif err := h.requireTenantOwnsCompany(ctx, tenantID, companyID); err != nil {\\n    http.Error(w, err.Error(), http.StatusNotFound)\\n    return\\n}\\n// Then INSERT with tenant_id\\n```\\n\\n**Example: handleSendCampaign**\\n```go\\n// UPDATE email_campaigns with tenant_id filter\\nct, err := h.pool.Exec(ctx, `UPDATE email_campaigns SET ... WHERE id = $1 AND tenant_id = $2`, ...)\\n// Check RowsAffected before touching campaign_recipients\\nif ct.RowsAffected() == 0 {\\n    http.Error(w, \\\"campaign not found\\\", http.StatusNotFound)\\n    return\\n}\\n// Then UPDATE recipients with tenant_id filter\\n```\\n\\nSee `docs/AUDIT_CRM_TENANT_SCOPE.md` for detailed findings and mitigations.\\n\\n## Form Handling\\n\\nForm handlers support both HTML form posts and JSON:\\n\\n```go\\n// handleCreateContact\\nisForm := r.Header.Get(\\\"X-Form-Post\\\") == \\\"true\\\"\\nif isForm {\\n    r.ParseForm()\\n    c.FirstName = r.FormValue(\\\"first_name\\\")\\n    // ...\\n} else {\\n    json.NewDecoder(r.Body).Decode(&amp;c)\\n}\\n// Validate, INSERT, then:\\nif isForm {\\n    w.Header().Set(\\\"HX-Redirect\\\", \\\"/crm/contacts/\\\"+c.ID)\\n    w.WriteHeader(http.StatusCreated)\\n} else {\\n    json.NewEncoder(w).Encode(c)\\n}\\n```\\n\\nThis dual-mode pattern allows:\\n- HTMX form submissions with server-side redirects\\n- JSON API calls with structured responses\\n\\n## Helper Functions\\n\\n### Utility Functions (handler.go)\\n\\n- `nullIfEmpty(s string) interface{}` \u2014 returns nil if empty, else the string (for optional fields)\\n- `nullableID(s string) interface{}` \u2014 converts empty string to nil for UUID fields\\n- `listTags(r *http.Request, tenantID string) []Tag` \u2014 loads all tags for tenant\\n- `requireTenantOwnsCompany(ctx, tenantID, companyID string) error` \u2014 pre-check for company ownership\\n- `requireTenantOwnsTag(ctx, tenantID, tagID string) error` \u2014 pre-check for tag ownership\\n- `buildClientUpdateSetClauses(updates map[string]interface{}, startIdx int) ([]string, []interface{})` \u2014 constructs dynamic UPDATE SET clause from allowlist\\n- `parseFormOrJSONUpdates(r *http.Request, isForm bool) (map[string]interface{}, error)` \u2014 parses form or JSON into update map\\n\\n### Data Types (types.go)\\n\\nCore structs:\\n- `ClientDashboard` \u2014 aggregates 20+ fields for Client 360\u00b0\\n- `Contact` \u2014 person record with portal/EPA fields\\n- `Activity` \u2014 timestamped interaction\\n- `Deal` \u2014 sales opportunity\\n- `Campaign` \u2014 email campaign\\n- `DealStage` \u2014 pipeline stage\\n- `Tag` \u2014 company tag\\n- `ClientLocation` \u2014 company site\\n- `ClientSurvey` \u2014 feedback survey\\n- `RoadmapItem` \u2014 planned work\\n- `ComplianceAssignmentSummary` \u2014 framework assignment\\n\\n## Integration Points\\n\\n### Authentication &amp; Authorization\\n\\n- `auth.TenantIDFromContext(r.Context())` \u2014 extracts tenant from JWT claims\\n- `auth.UserIDFromContext(r.Context())` \u2014 extracts user ID\\n- `auth.ClaimsFromContext(r.Context())` \u2014 full claims object (for role checks)\\n\\nUsed for:\\n- Tenant scoping on all queries\\n- Default owner/performer assignment\\n- Admin-only operations (EPA toggle, agent assignment)\\n\\n### Event Emission\\n\\n- `h.OnDealStageChanged(ctx, tenantID, dealID, companyID, stageID, stageName, isWon, isLost, dealValue)` \u2014 callback fired when deal moves to won/lost stage\\n- Allows downstream systems (notifications, revenue recognition, etc.) to react\\n\\n### Rendering\\n\\n- `h.renderer.Render(w, templatePath, data, isHTMX)` \u2014 renders HTML templates\\n- Supports HTMX partial updates (isHTMX flag)\\n\\n### Database\\n\\n- `h.pool` \u2014 pgx connection pool\\n- All queries use parameterized statements ($1, $2, etc.) to prevent SQL injection\\n- Transactions used for multi-step operations (e.g., `handleAssignContactAgent`)\\n\\n## Common Patterns\\n\\n### Dynamic SQL with Argument Indexing\\n\\n```go\\nquery := \\\"SELECT ... WHERE tenant_id = $1\\\"\\nargs := []interface{}{tenantID}\\nargIdx := 2\\n\\nif condition {\\n    query += fmt.Sprintf(\\\" AND field = $%d\\\", argIdx)\\n    args = append(args, value)\\n    argIdx++\\n}\\n```\\n\\n### Scan into Struct Slice\\n\\n```go\\nrows, err := h.pool.Query(ctx, query, args...)\\ndefer rows.Close()\\nvar items []Item\\nfor rows.Next() {\\n    var item Item\\n    rows.Scan(&amp;item.Field1, &amp;item.Field2, ...)\\n    items = append(items, item)\\n}\\n```\\n\\n### Null Coalescing in SQL\\n\\n```sql\\nSELECT COALESCE(field, ''), COALESCE(field, 0), COALESCE(field, false)\\n```\\n\\nEnsures non-null values in Go (empty string, 0, false) instead of nil pointers.\\n\\n### Transactional Updates\\n\\n```go\\ntx, err := h.pool.Begin(ctx)\\ndefer tx.Rollback(ctx)\\n\\n// Multiple operations\\ntx.Exec(ctx, ...)\\ntx.Exec(ctx, ...)\\n\\ntx.Commit(ctx)\\n```\\n\\nUsed in `handleAssignContactAgent` to atomically clear old assignment and set new one.\\n\\n## Testing\\n\\nThe module includes tenant isolation tests (`tenant_isolation_test.go`) that verify:\\n- Cross-tenant operations are blocked (404 or silent failure)\\n- Same-tenant operations succeed\\n- Audit findings are mitigated\\n\\nExample:\\n```go\\nTestAddTag_CrossTenantBlocked: // Tenant A tries to add tag to Tenant B's company \u2192 404\\nTestAddTag_SameTenantSucceeds: // Tenant A adds tag to own company \u2192 success\\n```\\n\\n## Performance Considerations\\n\\n- **LIMIT 100** on most list queries to prevent large result sets\\n- **LIMIT 50** on detail page sub-queries (tickets, quotes, invoices, activities, surveys)\\n- **LIMIT 5** on recent items (quotes, invoices, tickets)\\n- **Indexes** on tenant_id, company_id, contact_id, activity_date, created_at (assumed in schema)\\n- **LEFT JOINs** for optional relationships (account manager, escalation policy, etc.) to avoid filtering out records\\n\\n## Error Handling\\n\\n- **404 responses** for missing records or cross-tenant access attempts\\n- **400 responses** for invalid input (missing required fields, malformed JSON)\\n- **500 responses** for database errors (logged to stderr)\\n- **Silent failures** on optional queries (e.g., loading lifecycle counts, activity type counts) \u2014 returns empty maps/slices\\n\\n## Future Considerations\\n\\n- **Bulk operations**: Current design supports single-record CRUD; bulk import/export would require new endpoints\\n- **Soft deletes**: Companies and contacts use `is_active` flag; hard deletes are rare\\n- **Audit logging**: Tenant isolation is enforced; detailed change tracking (who changed what when) is not yet implemented\\n- **Caching**: No caching layer; all queries hit the database\\n- **Search**: Uses ILIKE for substring matching; full-text search could improve performance on large datasets\",\"internal-cybersec\":\"# internal \u2014 cybersec\\n\\n# Cybersecurity Module\\n\\n## Overview\\n\\nThe `internal/cybersec` module provides a unified security dashboard for NexusOS PSA that aggregates security posture data from multiple sources. It collects and displays:\\n\\n- **Compliance scores** from compliance frameworks (e.g., SOC 2)\\n- **Vulnerability data** from security scanners\\n- **Endpoint health** from RMM agents\\n- **Security alerts** from various detection sources\\n\\nThe module exposes both HTML dashboard views and JSON APIs for programmatic access to security metrics.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    A[\\\"HTTP Requests\\\"] --&gt;|GET /security/dashboard| B[\\\"handleDashboard\\\"]\\n    A --&gt;|GET /api/security/scores| C[\\\"handleScoresAPI\\\"]\\n    A --&gt;|GET /api/security/alerts| D[\\\"handleAlertsAPI\\\"]\\n    A --&gt;|\\\"PATCH /api/security/alerts/{id}\\\"| E[\\\"handleUpdateAlert\\\"]\\n    \\n    B --&gt; F[\\\"buildDashboard\\\"]\\n    C --&gt; F\\n    F --&gt; G[\\\"getAlerts\\\"]\\n    D --&gt; G\\n    \\n    F --&gt;|Query scores| H[\\\"Database\\\"]\\n    F --&gt;|Query agents| H\\n    G --&gt;|Query alerts| H\\n    E --&gt;|Update alert| H\\n    \\n    B --&gt;|Render HTML| I[\\\"UI Renderer\\\"]\\n```\\n\\n## Core Components\\n\\n### Handler\\n\\n`Handler` is the main entry point for all cybersecurity endpoints. It manages HTTP routing and coordinates data aggregation.\\n\\n```go\\ntype Handler struct {\\n    pool     *pgxpool.Pool      // Database connection pool\\n    renderer *ui.Renderer       // HTML template renderer\\n}\\n```\\n\\n**Constructor:**\\n```go\\nNewHandler(pool *pgxpool.Pool, renderer *ui.Renderer) *Handler\\n```\\n\\nCreates a new handler with database and rendering dependencies.\\n\\n**Route Registration:**\\n```go\\nRegisterRoutes(mux *http.ServeMux)\\n```\\n\\nRegisters four endpoints:\\n- `GET /security/dashboard` \u2014 HTML dashboard view\\n- `GET /api/security/scores` \u2014 JSON security scores\\n- `GET /api/security/alerts` \u2014 JSON recent alerts\\n- `PATCH /api/security/alerts/{id}` \u2014 Update alert status\\n\\n### Data Types\\n\\n#### SecurityScore\\nRepresents a single security measurement from a source system.\\n\\n| Field | Type | Purpose |\\n|-------|------|---------|\\n| `Source` | string | Origin system (e.g., `\\\"compliance_soc2\\\"`, `\\\"vulnerability_scan\\\"`, `\\\"agent_health\\\"`) |\\n| `Category` | string | Classification: `compliance`, `vulnerability`, `endpoint`, `identity` |\\n| `Score` | float64 | Current score (0\u2013100) |\\n| `MaxScore` | float64 | Maximum possible score (typically 100) |\\n| `Details` | string | JSON blob with source-specific metadata |\\n| `UpdatedAt` | time.Time | Last update timestamp |\\n\\n#### DashboardData\\nComplete security posture snapshot for a tenant.\\n\\n| Field | Type | Purpose |\\n|-------|------|---------|\\n| `OverallScore` | float64 | Weighted aggregate of all scores (0\u2013100) |\\n| `Scores` | []SecurityScore | Individual scores from all sources |\\n| `ScoresByCategory` | map[string]float64 | Aggregated scores per category |\\n| `AgentStats` | AgentSecurityStats | RMM agent health summary |\\n| `RecentAlerts` | []SecurityAlert | Latest 50 security alerts |\\n\\n#### AgentSecurityStats\\nSummary of endpoint security across RMM agents.\\n\\n| Field | Type | Purpose |\\n|-------|------|---------|\\n| `TotalAgents` | int | Total agents under management |\\n| `OnlineAgents` | int | Agents currently online |\\n| `ComplianceRate` | float64 | Percentage of online agents (0\u2013100) |\\n\\n#### SecurityAlert\\nA security event or finding requiring attention.\\n\\n| Field | Type | Purpose |\\n|-------|------|---------|\\n| `Severity` | string | `critical`, `high`, `medium`, `low`, `info` |\\n| `Source` | string | Detection system (e.g., `\\\"vulnerability_scanner\\\"`, `\\\"compliance_audit\\\"`) |\\n| `Title` | string | Human-readable alert name |\\n| `Details` | string | Additional context or remediation guidance |\\n| `Status` | string | `new`, `acknowledged`, `resolved` |\\n| `CreatedAt` | time.Time | Alert generation timestamp |\\n\\n## Request Handlers\\n\\n### handleDashboard\\nRenders the HTML cybersecurity dashboard page.\\n\\n**Flow:**\\n1. Extracts tenant ID from request context (via `auth.TenantIDFromContext`)\\n2. Detects HTMX requests via `HX-Request` header\\n3. Calls `buildDashboard` to aggregate all security data\\n4. Renders `cybersec/dashboard.html` template with aggregated data\\n\\n**Response:** HTML page with embedded security metrics and alerts\\n\\n### handleScoresAPI\\nReturns security scores as JSON.\\n\\n**Flow:**\\n1. Extracts tenant ID from context\\n2. Calls `buildDashboard` to fetch and aggregate scores\\n3. Encodes `DashboardData` as JSON\\n\\n**Response:** JSON object containing all security scores and statistics\\n\\n### handleAlertsAPI\\nReturns recent security alerts as JSON.\\n\\n**Flow:**\\n1. Extracts tenant ID from context\\n2. Calls `getAlerts` to fetch latest alerts\\n3. Encodes alert array as JSON\\n\\n**Response:** JSON array of `SecurityAlert` objects (max 50, ordered by recency)\\n\\n### handleUpdateAlert\\nUpdates the status of a security alert.\\n\\n**Flow:**\\n1. Extracts tenant ID and alert ID from request\\n2. Decodes JSON body to get new `status` value\\n3. Executes SQL UPDATE with tenant isolation\\n4. Returns 200 OK on success, 400/500 on error\\n\\n**Request Body:**\\n```json\\n{\\n  \\\"status\\\": \\\"acknowledged\\\"\\n}\\n```\\n\\n**Tenant Isolation:** All updates are scoped to the requesting tenant via `WHERE tenant_id=$3`\\n\\n## Data Aggregation\\n\\n### buildDashboard\\nOrchestrates the collection and aggregation of all security data for a tenant.\\n\\n**Process:**\\n\\n1. **Load Security Scores** \u2014 Queries `cybersecurity_scores` table\\n   - Aggregates scores by category\\n   - Calculates overall score as weighted average: `(totalScore / totalMax) * 100`\\n   - Stores individual scores for detailed breakdown\\n\\n2. **Load Agent Stats** \u2014 Queries `agents` table\\n   - Counts total agents and online agents\\n   - Calculates compliance rate: `(onlineAgents / totalAgents) * 100`\\n\\n3. **Load Recent Alerts** \u2014 Calls `getAlerts` to fetch latest 50 alerts\\n\\n**Error Handling:** Gracefully continues if individual queries fail; returns partial data rather than failing entirely.\\n\\n### getAlerts\\nFetches recent security alerts for a tenant, ordered by creation time (newest first).\\n\\n**Query:**\\n```sql\\nSELECT id, severity, source, title, COALESCE(details, ''), status, created_at\\nFROM security_alerts\\nWHERE tenant_id = $1\\nORDER BY created_at DESC\\nLIMIT 50\\n```\\n\\n**Returns:** Slice of `SecurityAlert` objects, or `nil` if query fails\\n\\n## Multi-Tenancy\\n\\nAll database queries include tenant isolation:\\n\\n```go\\nWHERE tenant_id = $1  // Tenant ID from request context\\n```\\n\\nTenant ID is extracted via `auth.TenantIDFromContext(r.Context())` before any data access. This ensures:\\n- Users can only view their own security data\\n- Alert updates are scoped to the requesting tenant\\n- No cross-tenant data leakage\\n\\n## Database Schema (Inferred)\\n\\nThe module expects three tables:\\n\\n**cybersecurity_scores**\\n```\\nid, tenant_id, source, category, score, max_score, details, updated_at\\n```\\n\\n**agents**\\n```\\nid, tenant_id, status, ...\\n```\\n\\n**security_alerts**\\n```\\nid, tenant_id, severity, source, title, details, status, created_at\\n```\\n\\n## Integration Points\\n\\n### Authentication\\n- Depends on `internal/auth` for tenant context extraction\\n- All handlers call `auth.TenantIDFromContext(r.Context())` to enforce multi-tenancy\\n\\n### UI Rendering\\n- Depends on `internal/ui.Renderer` for HTML template rendering\\n- Passes `DashboardData` to `cybersec/dashboard.html` template\\n- Supports HTMX partial updates via `HX-Request` header detection\\n\\n### Database\\n- Uses `pgx` connection pool for all queries\\n- Executes raw SQL with parameterized queries to prevent injection\\n- Handles query errors gracefully without crashing\\n\\n## Usage Example\\n\\n### Registering the Module\\n\\nIn `cmd/psa/main.go`:\\n```go\\npool := // ... pgx connection pool\\nrenderer := // ... ui.Renderer\\nhandler := cybersec.NewHandler(pool, renderer)\\nhandler.RegisterRoutes(mux)\\n```\\n\\n### Accessing the Dashboard\\n\\n- **HTML:** `GET /security/dashboard`\\n- **JSON Scores:** `GET /api/security/scores`\\n- **JSON Alerts:** `GET /api/security/alerts`\\n- **Update Alert:** `PATCH /api/security/alerts/{id}` with `{\\\"status\\\": \\\"acknowledged\\\"}`\\n\\n## Error Handling\\n\\n- **Invalid request body:** Returns 400 Bad Request\\n- **Database errors:** Returns 500 Internal Server Error with generic message\\n- **Query failures in buildDashboard:** Continues with partial data (no panic)\\n- **Missing alerts:** Returns empty slice rather than nil\\n\\n## Performance Considerations\\n\\n- **Score aggregation:** O(n) where n = number of scores; no indexing required if scores table is small\\n- **Alert fetching:** Limited to 50 most recent alerts to avoid large result sets\\n- **Agent stats:** Single COUNT query with filter; efficient with proper indexing on `tenant_id` and `status`\\n- **No caching:** All data is fetched fresh on each request; consider adding caching for frequently accessed dashboards\",\"internal-database\":\"# internal \u2014 database\\n\\n# Database Module\\n\\nThe `internal/database` package manages PostgreSQL connectivity, connection pooling, and schema migrations for NexusOS PSA. It uses pgx v5 as the native PostgreSQL driver\u2014no ORM or `database/sql` abstraction\u2014to access PostgreSQL-specific features like LISTEN/NOTIFY, COPY, and advisory locks.\\n\\n## Overview\\n\\nThis module serves two core responsibilities:\\n\\n1. **Connection Management**: Establishes and maintains a pgxpool connection pool with health checking and automatic reconnection.\\n2. **Schema Migrations**: Runs embedded SQL migration files in order, tracking applied migrations to ensure idempotency.\\n\\nThe module is intentionally minimal and focused. It does not provide query builders, transaction helpers, or data access patterns\u2014those belong in domain-specific packages. The `DB` type is a thin wrapper around `pgxpool.Pool` that other packages use directly.\\n\\n## Connection Pool\\n\\n### `Connect(ctx context.Context, dsn string) (*DB, error)`\\n\\nEstablishes a connection pool to PostgreSQL using the provided Data Source Name (DSN). The function:\\n\\n1. Parses the DSN into a pgxpool config\\n2. Applies tuned pool settings for a multi-tenant SaaS workload\\n3. Creates the pool with a 10-second timeout (fail-fast on startup)\\n4. Pings the database to verify connectivity\\n\\n**Pool Configuration:**\\n\\n```\\nMaxConns:           25    // Enough for concurrent requests without overwhelming PG\\nMinConns:           5     // Maintain baseline connections\\nMaxConnLifetime:    30m   // Recycle connections periodically\\nMaxConnIdleTime:    5m    // Close idle connections quickly\\nHealthCheckPeriod:  30s   // Periodic health checks\\n```\\n\\nThe 10-second startup timeout is intentional. During production deployments, if PostgreSQL is momentarily unavailable, the application fails fast (~5 seconds with systemd restart) rather than hanging for pgx's default ~2 minutes. This prevents deployment races where connection release and startup overlap.\\n\\n### `Close()`\\n\\nShuts down the connection pool cleanly. Call this during server shutdown to release all database connections.\\n\\n## Migrations\\n\\n### `Migrate(ctx context.Context) error`\\n\\nRuns all pending SQL migrations in order. Each migration:\\n\\n1. Is executed inside a transaction (all-or-nothing)\\n2. Is tracked in the `schema_migrations` table to prevent re-running\\n3. Is logged on successful application\\n\\n**Migration Discovery:**\\n\\nMigrations are embedded in the binary via `//go:embed migrations/*.sql`. Files are sorted alphabetically (001_, 002_, ...) to ensure correct execution order. Only `.sql` files are processed; directories are skipped.\\n\\n**Idempotency:**\\n\\nThe `schema_migrations` table tracks which migrations have been applied by filename. If a migration has already run, it is skipped. This allows safe re-running of `Migrate()` on every startup without duplicate execution.\\n\\n## Schema Design\\n\\nThe schema spans 19 migrations covering:\\n\\n- **Core**: Tenants, users, roles, permissions (RBAC)\\n- **RMM Integration**: Companies, agents, products, assignments\\n- **Helpdesk**: Tickets, notes, SLAs, contracts, workflows, knowledge base\\n- **ITIL**: Problems, changes, CMDB, service catalog\\n- **CRM**: Contacts, deals, activities, campaigns, tags\\n- **Financials**: Quotes, invoices, payments, credit memos\\n- **Compliance**: Frameworks, documents, gap analysis, security alerts\\n- **Integrations**: QuickBooks, SSO, email accounts, MCP servers, certificates\\n\\n### Multi-Tenancy\\n\\nThe schema uses a **shared-schema model** with tenant isolation via:\\n\\n- `tenant_id` columns on every table\\n- Row-Level Security (RLS) policies (enforced at the application layer)\\n- Per-tenant sequences for auto-incrementing IDs (ticket_number, invoice_number, etc.)\\n\\nThis approach is more efficient than schema-per-tenant for SaaS with many small tenants (typical MSP client base).\\n\\n### Key Design Patterns\\n\\n**UUIDs for Primary Keys:**\\nAll tables use UUID primary keys (via `uuid_generate_v4()`) instead of serial integers. UUIDs are globally unique across tenants, safe for distributed systems, and don't leak information about record counts.\\n\\n**Denormalization for Performance:**\\nSLA tracking is denormalized onto tickets (`sla_response_target`, `sla_resolve_target`, `sla_response_breach`, `sla_resolve_breach`) for fast dashboard queries without joins.\\n\\n**JSONB for Flexibility:**\\nTenant settings, custom fields, metadata, and configuration are stored as JSONB for extensibility without schema changes. Examples:\\n- `tenants.settings`: MFA requirements, session timeouts, timezone\\n- `agents.metadata`: RMM feature flags, metrics\\n- `tickets.custom_fields`: Tenant-specific fields\\n- `compliance_evidence.detail`: Raw device check results\\n\\n**Audit Trails:**\\nCritical tables have history/audit tables:\\n- `ticket_history`: Every field change on a ticket\\n- `quote_audit_log`: Quote lifecycle events (created, sent, viewed, signed, etc.)\\n- `invoice_audit_log`: Invoice state changes\\n- `qb_sync_log`: QuickBooks sync operations\\n\\n**Encrypted Columns:**\\nSensitive data (OAuth tokens, API keys, passwords) are encrypted at rest using AES-256-GCM. The encryption key is derived from the application's JWT private key. Encrypted columns are suffixed with `_enc`:\\n- `sso_providers.client_secret_enc`\\n- `email_accounts.imap_pass_enc`, `smtp_pass_enc`\\n- `qb_connections.access_token`, `refresh_token`\\n- `certificates.private_key_enc`\\n\\n**Sequences for Per-Tenant Numbering:**\\nAuto-incrementing IDs (ticket_number, invoice_number, problem_number, etc.) are per-tenant, not global. This is implemented via sequence tables and triggers:\\n\\n```sql\\nCREATE TABLE ticket_sequences (\\n    tenant_id   UUID PRIMARY KEY,\\n    next_number INT NOT NULL DEFAULT 1\\n);\\n\\nCREATE TRIGGER trg_ticket_number\\n    BEFORE INSERT ON tickets\\n    FOR EACH ROW\\n    EXECUTE FUNCTION assign_ticket_number();\\n```\\n\\nThis allows each tenant to have ticket #1, #2, #3, etc., without global coordination.\\n\\n## Integration Points\\n\\n### From `cmd/psa/main.go`\\n\\nThe main application entry point calls:\\n\\n```go\\ndb, err := database.Connect(ctx, dsn)\\nif err != nil {\\n    log.Fatal(err)\\n}\\ndefer db.Close()\\n\\nif err := db.Migrate(ctx); err != nil {\\n    log.Fatal(err)\\n}\\n```\\n\\nThis establishes the connection pool and runs migrations on startup.\\n\\n### From Other Packages\\n\\nDomain packages (helpdesk, crm, compliance, etc.) receive the `*DB` instance and use it directly:\\n\\n```go\\n// Example from a helpdesk service\\nfunc (s *Service) GetTicket(ctx context.Context, ticketID uuid.UUID) (*Ticket, error) {\\n    var ticket Ticket\\n    err := s.db.Pool.QueryRow(ctx, \\n        \\\"SELECT id, title, status FROM tickets WHERE id = $1 AND tenant_id = $2\\\",\\n        ticketID, s.tenantID,\\n    ).Scan(&amp;ticket.ID, &amp;ticket.Title, &amp;ticket.Status)\\n    return &amp;ticket, err\\n}\\n```\\n\\nThere is no query builder or ORM. Packages write SQL directly and use pgx's row scanning.\\n\\n## Testing\\n\\nThe module includes a test helper (`TestMain` in `internal/crm/tenant_isolation_test.go`) that:\\n\\n1. Calls `Connect()` to establish a test database connection\\n2. Calls `Migrate()` to set up the schema\\n3. Runs tests against the live schema\\n\\nTests use a real PostgreSQL instance (typically a Docker container or test database). There is no mocking of the database layer.\\n\\n## Error Handling\\n\\nConnection and migration errors are wrapped with context:\\n\\n```go\\nif err != nil {\\n    return nil, fmt.Errorf(\\\"parse database config: %w\\\", err)\\n}\\n```\\n\\nThis preserves the original error while adding context about what operation failed. Callers can use `errors.Is()` and `errors.As()` to inspect the underlying error if needed.\\n\\n## Performance Considerations\\n\\n- **Connection Pool Sizing**: 25 max connections is tuned for typical PSA workloads (mostly short-lived queries with occasional longer report generation). Adjust based on observed connection usage.\\n- **Health Checks**: 30-second health check period balances responsiveness to connection failures with overhead.\\n- **Idle Timeout**: 5-minute idle timeout closes unused connections quickly, reducing resource consumption.\\n- **Indexes**: Migrations create indexes on frequently queried columns (tenant_id, status, created_at, etc.). Review slow query logs and add indexes as needed.\\n- **JSONB Queries**: JSONB columns support GIN indexes for fast containment queries. Use `@&gt;` operator for efficient filtering.\\n\\n## Future Enhancements\\n\\n- **Connection Pooling Metrics**: Expose pool stats (active connections, idle connections, wait time) for monitoring.\\n- **Query Logging**: Optional query logging for debugging and performance analysis.\\n- **Prepared Statements**: Cache frequently used queries as prepared statements for slight performance gains.\\n- **Read Replicas**: Support read-only replicas for reporting queries to reduce load on the primary.\",\"internal-devices\":\"# internal \u2014 devices\\n\\n# internal/devices Module\\n\\nThe `devices` module manages network device credentials, FortiGate API interactions, and automation task execution. It provides HTTP handlers for credential management and a task execution engine that orchestrates remediation workflows on FortiGate firewalls.\\n\\n## Overview\\n\\nThis module has two main responsibilities:\\n\\n1. **Device Credential Management** \u2014 Store, retrieve, and test credentials for network devices (currently FortiGate)\\n2. **Nexie Task Automation** \u2014 Execute remediation and configuration tasks on devices, with approval workflows and audit trails\\n\\nThe module encrypts sensitive credentials at rest and communicates with FortiGate devices via their REST API.\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    Handler[\\\"Handler(HTTP routes)\\\"]\\n    FGClient[\\\"FortiGateClient(REST API)\\\"]\\n    DB[\\\"Database(pgxpool)\\\"]\\n    CertStore[\\\"CertStore(CA certs)\\\"]\\n    \\n    Handler --&gt;|creates| FGClient\\n    Handler --&gt;|queries| DB\\n    Handler --&gt;|loads CA| CertStore\\n    FGClient --&gt;|HTTPS| FG[\\\"FortiGate Device\\\"]\\n    \\n    style Handler fill:#e1f5ff\\n    style FGClient fill:#fff3e0\\n    style DB fill:#f3e5f5\\n    style FG fill:#ffebee\\n```\\n\\n## Core Components\\n\\n### FortiGateClient\\n\\n`FortiGateClient` is a low-level REST API client for FortiGate devices. It handles authentication, request formatting, and response parsing.\\n\\n**Constructor:**\\n```go\\nNewFortiGateClient(host string, port int, apiKey string, verifyTLS bool) *FortiGateClient\\n```\\n\\nCreates a client with a 15-second HTTP timeout and configurable TLS verification. The API key is appended to every request as a query parameter.\\n\\n**Core Methods:**\\n\\n- **`TestConnection(ctx)`** \u2014 Verifies authentication by querying `/api/v2/cmdb/system/global`. Returns error on 401/403 or non-200 status.\\n\\n- **`GetSystemInfo(ctx)`** \u2014 Retrieves hostname, firmware version, and serial number. Returns `FortiGateInfo` struct.\\n\\n- **`do(ctx, method, path, body)`** \u2014 Low-level authenticated request handler. Marshals body to JSON, appends API key to URL, and returns response body + status code.\\n\\n**CA Certificate Operations:**\\n\\n- **`UploadCACert(ctx, certName, certPEM)`** \u2014 Uploads a CA certificate using a fallback strategy:\\n  1. Multipart file upload to `/api/v2/monitor/vpn-certificate/ca/import` (FortiOS 7.x GUI method)\\n  2. JSON import with base64-encoded PEM\\n  3. CMDB endpoint with escaped newlines (`\\\\n` literals)\\n  4. CMDB with quoted PEM string\\n  5. Raw JSON with manual newline escaping\\n\\n  This multi-approach design handles FortiOS version differences and API quirks.\\n\\n- **`ListCACerts(ctx)`** \u2014 Returns names of installed CA certificates.\\n\\n**Syslog Configuration:**\\n\\n- **`ConfigureSyslog(ctx, cfg SyslogConfig)`** \u2014 Configures the primary syslog destination with TLS support. Sets encryption algorithm to \\\"high\\\" and minimum TLS version to 1.2. If a CA cert name is provided, it's referenced for TLS verification.\\n\\n- **`GetSyslogConfig(ctx)`** \u2014 Reads current syslog configuration and returns a `SyslogConfig` struct.\\n\\n- **`EnableSyslogFilter(ctx)`** \u2014 Enables forwarding of all traffic categories (forward, local, multicast, anomaly, VoIP) via syslog.\\n\\n**Firewall Policy &amp; Address Objects:**\\n\\n- **`CreateAddressObject(ctx, name, subnet, comment)`** \u2014 Creates a firewall address object. Checks for existence first to avoid duplicates.\\n\\n- **`DeleteAddressObject(ctx, name)`** \u2014 Removes an address object. Returns success even if object doesn't exist (404).\\n\\n- **`CreateDenyPolicy(ctx, name, srcAddr, dstAddr, comment)`** \u2014 Creates a deny policy. Handles comma-separated address lists and validates that referenced address objects exist. Falls back to \\\"all\\\" if validation fails.\\n\\n- **`BlockIP(ctx, ip, direction, reason)`** \u2014 Convenience method that creates an address object and deny policy for an IP. Direction can be \\\"outbound\\\", \\\"inbound\\\", or \\\"both\\\". Returns address object name and policy name.\\n\\n- **`GetFirewallPolicies(ctx)` / `GetAddressObjects(ctx)`** \u2014 List existing policies and address objects.\\n\\n**IPS Signatures:**\\n\\n- **`EnableIPSSensor(ctx, sensorName, signatureIDs, action)`** \u2014 Updates an IPS sensor with signature rules. Action is \\\"block\\\", \\\"pass\\\", or \\\"monitor\\\".\\n\\n### Handler\\n\\n`Handler` serves HTTP endpoints for credential and task management. It manages database connections, encryption keys, and task execution.\\n\\n**Constructor:**\\n```go\\nNewHandler(pool *pgxpool.Pool, renderer *ui.Renderer, certStore *certs.Store, encKeyHex string) *Handler\\n```\\n\\nThe `encKeyHex` parameter is a 64-character hex string representing a 32-byte AES-256 key. If invalid or missing, credentials are stored unencrypted.\\n\\n**Routes:**\\n\\n| Method | Path | Handler |\\n|--------|------|---------|\\n| GET | `/api/devices/credentials` | `handleListCredentials` |\\n| POST | `/api/devices/credentials` | `handleCreateCredential` |\\n| DELETE | `/api/devices/credentials/{id}` | `handleDeleteCredential` |\\n| POST | `/api/devices/credentials/{id}/test` | `handleTestConnection` |\\n| POST | `/api/devices/credentials/test-live` | `handleTestLive` |\\n| GET | `/api/nexie/tasks` | `handleListTasks` |\\n| POST | `/api/nexie/tasks` | `handleCreateTask` |\\n| POST | `/api/nexie/tasks/{id}/approve` | `handleApproveTask` |\\n| POST | `/api/nexie/tasks/{id}/reject` | `handleRejectTask` |\\n\\n#### Credential Management\\n\\n**`handleCreateCredential`** \u2014 Accepts JSON with device details and credentials:\\n```json\\n{\\n  \\\"device_name\\\": \\\"fw-prod-01\\\",\\n  \\\"device_type\\\": \\\"fortigate\\\",\\n  \\\"device_ip\\\": \\\"192.168.1.1\\\",\\n  \\\"device_port\\\": 443,\\n  \\\"auth_type\\\": \\\"api_key\\\",\\n  \\\"api_key\\\": \\\"...\\\",\\n  \\\"username\\\": \\\"admin\\\",\\n  \\\"password\\\": \\\"...\\\",\\n  \\\"verify_tls\\\": true,\\n  \\\"company_id\\\": \\\"...\\\"\\n}\\n```\\n\\nCredentials are encrypted using AES-256-GCM and stored in the database. The API key, username, and password are marshaled into a JSON object, encrypted, and stored as a hex string in `credential_enc`.\\n\\n**`handleTestLive`** \u2014 Tests a device connection before saving credentials. Useful for UI validation.\\n\\n**`handleTestConnection`** \u2014 Tests an already-saved credential. Updates `last_status` and `last_connected` in the database.\\n\\n#### Task Management\\n\\n**`handleCreateTask`** \u2014 Creates a new automation task in `pending_approval` status. Accepts:\\n```json\\n{\\n  \\\"task_type\\\": \\\"fortigate_syslog_tls\\\",\\n  \\\"description\\\": \\\"Enable syslog with TLS\\\",\\n  \\\"device_credential_id\\\": \\\"...\\\",\\n  \\\"certificate_id\\\": \\\"...\\\",\\n  \\\"company_id\\\": \\\"...\\\",\\n  \\\"syslog_server\\\": \\\"10.100.100.231\\\",\\n  \\\"syslog_port\\\": 6514\\n}\\n```\\n\\nTask parameters are stored as JSON in the `params` column.\\n\\n**`handleApproveTask`** \u2014 Marks a task as approved and launches background execution. Optionally filters remediation actions:\\n```json\\n{\\n  \\\"selected_actions\\\": [0, 2, 4]\\n}\\n```\\n\\nIf `selected_actions` is provided and the task is a `fortigate_remediation`, only those action indices are executed.\\n\\n**`handleRejectTask`** \u2014 Marks a task as rejected and sets `completed_at`.\\n\\n### Task Execution Engine\\n\\nTasks execute asynchronously in a goroutine via `executeTask()`. The engine dispatches to task-type-specific handlers.\\n\\n#### Syslog TLS Configuration (`executeFortiGateSyslogTLS`)\\n\\nWorkflow:\\n1. Load device credential from database\\n2. Test connection to FortiGate\\n3. Retrieve tenant's internal CA certificate\\n4. Upload CA certificate to FortiGate\\n5. Configure syslog with TLS and CA cert reference\\n6. Enable syslog filters\\n7. Verify configuration was applied\\n\\nEach step is logged with status (success/failed/warning) and detail. Steps are inserted into `siem_remediation_audit` table for compliance tracking.\\n\\n#### Remediation Execution (`executeFortiGateRemediation`)\\n\\nExecutes a remediation plan loaded from the task's `remediation_plan` JSON field. The plan contains an array of actions, each with:\\n- `action` \u2014 action type (e.g., \\\"block_ip\\\", \\\"create_address_object\\\")\\n- `description` \u2014 human-readable description\\n- `risk` \u2014 risk level\\n- `params` \u2014 action-specific parameters\\n- `rationale` \u2014 why this action is needed\\n\\n**Supported Actions:**\\n\\n- **`block_ip`** \u2014 Blocks traffic to/from an IP. Creates address object and deny policy. Direction can be \\\"inbound\\\", \\\"outbound\\\", or \\\"both\\\".\\n\\n- **`create_address_object`** \u2014 Creates a firewall address object with name and subnet.\\n\\n- **`create_deny_policy`** \u2014 Creates a deny policy with source/destination address lists.\\n\\n- **`enable_ips_signature`** \u2014 Enables IPS signatures on a sensor. Logs a warning if no specific signature IDs are provided (manual review recommended).\\n\\nEach action execution is audited with rich metadata: API endpoints called, object names created, parameters used, and rationale.\\n\\n**Post-Remediation Posture Snapshot:**\\n\\nAfter execution, if actions were successful, the engine computes a post-remediation risk score:\\n- Loads the original analysis findings\\n- Counts findings by severity\\n- Estimates risk reduction based on remediated action ratio (up to 40% per action)\\n- Inserts a `siem_posture_snapshots` record with type `post_remediation`\\n\\nThis allows tracking security posture improvement over time.\\n\\n## Data Models\\n\\n### DeviceCredential\\n\\n```go\\ntype DeviceCredential struct {\\n    ID            string     // UUID\\n    TenantID      string     // Multi-tenant isolation\\n    CompanyID     *string    // Optional company association\\n    CompanyName   string     // Denormalized for UI\\n    DeviceName    string     // User-friendly name\\n    DeviceType    string     // \\\"fortigate\\\", etc.\\n    DeviceIP      string     // Management IP\\n    DevicePort    int        // HTTPS port\\n    AuthType      string     // \\\"api_key\\\", etc.\\n    VerifyTLS     bool       // TLS certificate verification\\n    LastConnected *time.Time // Last successful connection\\n    LastStatus    string     // \\\"connected\\\", \\\"auth_failed\\\", etc.\\n    CreatedAt     time.Time\\n}\\n```\\n\\nActual credentials (API key, username, password) are stored encrypted in the database and never returned by the API.\\n\\n### NexieTask\\n\\n```go\\ntype NexieTask struct {\\n    ID                 string     // UUID\\n    TenantID           string\\n    CompanyID          *string\\n    CompanyName        string\\n    TaskType           string     // \\\"fortigate_syslog_tls\\\", \\\"fortigate_remediation\\\"\\n    Status             string     // \\\"pending_approval\\\", \\\"approved\\\", \\\"running\\\", \\\"complete\\\", \\\"failed\\\", \\\"rejected\\\"\\n    Description        string\\n    DeviceCredentialID *string    // Which device to target\\n    DeviceName         string     // Denormalized\\n    CertificateID      *string    // For syslog TLS, which CA cert to use\\n    Params             string     // JSON: task-specific parameters\\n    Result             *string    // JSON: execution result with steps\\n    RemediationPlan    *string    // JSON: AI-generated remediation plan (for remediation tasks)\\n    AICost             float64    // Cost of AI analysis\\n    ApprovedAt         *time.Time\\n    CompletedAt        *time.Time\\n    CreatedAt          time.Time\\n}\\n```\\n\\n## Encryption\\n\\nCredentials are encrypted using AES-256-GCM with a random nonce prepended to the ciphertext. The encrypted value is hex-encoded for storage.\\n\\n**`encrypt(data []byte) (string, error)`** \u2014 Encrypts data and returns hex string.\\n\\n**`decrypt(encHex string) ([]byte, error)`** \u2014 Decrypts hex string. Falls back to treating input as plaintext if decryption fails (for backward compatibility).\\n\\nIf no encryption key is configured, credentials are stored and returned as plaintext.\\n\\n## Integration Points\\n\\n### Authentication\\n\\nAll HTTP handlers extract `tenantID` and `userID` from request context via `auth.TenantIDFromContext()` and `auth.UserIDFromContext()`. This enforces multi-tenant isolation.\\n\\n### Certificate Store\\n\\nThe `certs.Store` is used to retrieve the tenant's internal CA certificate for syslog TLS configuration. The CA's PEM and fingerprint are loaded and uploaded to the FortiGate.\\n\\n### Database\\n\\nThe module uses `pgxpool.Pool` for all database operations. Key tables:\\n- `device_credentials` \u2014 Stores encrypted device credentials\\n- `nexie_tasks` \u2014 Stores task records and execution results\\n- `siem_remediation_audit` \u2014 Audit trail of remediation actions\\n- `siem_posture_snapshots` \u2014 Post-remediation risk snapshots\\n\\n## Error Handling\\n\\n- **Connection errors** \u2014 Logged and returned to client; device status updated to \\\"auth_failed\\\"\\n- **API errors** \u2014 HTTP status codes and response bodies are included in error messages\\n- **Remediation action failures** \u2014 Individual action failures don't stop the task; all actions are attempted and results summarized\\n- **Encryption failures** \u2014 Return 500 error; credentials are not stored\\n\\n## Logging\\n\\nThe module logs:\\n- Device connection tests and status changes\\n- Task creation, approval, and execution\\n- CA certificate upload attempts (all 5 approaches)\\n- Remediation action execution with API calls made\\n- Post-remediation posture snapshots\\n\\nLogs use `log.Printf()` with prefixes like `\\\"fortigate:\\\"`, `\\\"nexie:\\\"` for easy filtering.\\n\\n## Security Considerations\\n\\n1. **Credential Encryption** \u2014 API keys and passwords are encrypted at rest using AES-256-GCM\\n2. **TLS Verification** \u2014 Configurable per device; defaults to true\\n3. **Multi-Tenant Isolation** \u2014 All queries filtered by `tenant_id`\\n4. **Audit Trail** \u2014 All remediation actions logged to `siem_remediation_audit`\\n5. **API Key in URL** \u2014 FortiGate API key is appended to request URLs (not ideal, but required by FortiGate API)\\n\\n## Testing Credentials\\n\\nBefore saving a credential, use `handleTestLive` to validate:\\n```bash\\nPOST /api/devices/credentials/test-live\\n{\\n  \\\"device_ip\\\": \\\"192.168.1.1\\\",\\n  \\\"device_port\\\": 443,\\n  \\\"api_key\\\": \\\"...\\\",\\n  \\\"verify_tls\\\": true\\n}\\n```\\n\\nResponse includes device hostname, firmware version, and serial number if successful.\",\"internal-dispatch\":\"# internal \u2014 dispatch\\n\\n# Dispatch Module\\n\\nThe dispatch module is the command center for NexusOS PSA, providing a unified interface to assign technicians to both IT tickets and construction work orders. It powers multiple visualization modes (calendar, kanban, list, map) and integrates with Microsoft 365 calendars to keep tech schedules synchronized.\\n\\n## Overview\\n\\nThe dispatch system solves a core MSP/construction problem: **how do you efficiently assign work to the right person at the right time?** It unifies two different work types (tickets and work orders) into a single dispatch queue, provides multiple views optimized for different workflows, and automatically syncs assignments to technician calendars.\\n\\nKey responsibilities:\\n- **Slot management**: Create, update, delete dispatch assignments (slots)\\n- **Multi-view rendering**: Calendar (week grid), Kanban (by tech), List (filterable), Map (geographic clusters)\\n- **MS365 sync**: Automatically create/update/delete calendar events when slots change\\n- **AI suggestions**: Use Claude to recommend the best tech for a job based on skills, load, and location\\n- **Tech load tracking**: Monitor how many slots each technician has on a given date\\n\\n## Architecture\\n\\n```mermaid\\ngraph TB\\n    Handler[\\\"Handler(HTTP routes)\\\"]\\n    Calendar[\\\"CalendarClient(MS365 Graph API)\\\"]\\n    DB[\\\"Database(dispatch_slots, users, etc.)\\\"]\\n    AI[\\\"AI Provider(Claude)\\\"]\\n    \\n    Handler --&gt;|creates/updates/deletes| DB\\n    Handler --&gt;|syncs to| Calendar\\n    Calendar --&gt;|OAuth2 token| MS365[\\\"Microsoft 365Graph API\\\"]\\n    Handler --&gt;|queries| DB\\n    Handler --&gt;|calls| AI\\n    \\n    style Handler fill:#3b82f6,color:#fff\\n    style Calendar fill:#8b5cf6,color:#fff\\n    style AI fill:#f59e0b,color:#fff\\n    style DB fill:#10b981,color:#fff\\n```\\n\\n## Core Components\\n\\n### Handler\\n\\n`Handler` is the HTTP request router and business logic coordinator. It's initialized with a database pool, UI renderer, and optional AI provider.\\n\\n```go\\ntype Handler struct {\\n    pool     *pgxpool.Pool\\n    renderer *ui.Renderer\\n    ai       *ai.Provider\\n    calendar *CalendarClient\\n}\\n```\\n\\n**Key responsibilities:**\\n- Route HTTP requests to view handlers and API endpoints\\n- Fetch and filter dispatch slots from the database\\n- Coordinate calendar sync operations\\n- Gather context for AI suggestions\\n\\n**Routes registered:**\\n\\n| Method | Path | Purpose |\\n|--------|------|---------|\\n| GET | `/dispatch` | Main board page (with view selector) |\\n| GET | `/dispatch/calendar` | Week calendar view (HTMX partial) |\\n| GET | `/dispatch/kanban` | Tech-column kanban view (HTMX partial) |\\n| GET | `/dispatch/list` | Filterable list view (HTMX partial) |\\n| GET | `/dispatch/map` | Geographic cluster map (HTMX partial) |\\n| GET | `/dispatch/tv` | Full-screen TV display mode |\\n| POST | `/api/dispatch/slots` | Create a new dispatch slot |\\n| PUT | `/api/dispatch/slots/{id}` | Update slot details |\\n| POST | `/api/dispatch/slots/{id}/status` | Change slot status |\\n| DELETE | `/api/dispatch/slots/{id}` | Delete a slot |\\n| GET | `/api/dispatch/unassigned` | Fetch unassigned tickets/WOs |\\n| POST | `/api/dispatch/suggest` | Get AI tech recommendation |\\n| GET | `/api/dispatch/tech-load` | Get tech workload data (JSON) |\\n\\n### CalendarClient\\n\\n`CalendarClient` manages all interactions with Microsoft Graph API for calendar operations. It handles OAuth2 token management and CRUD operations on calendar events.\\n\\n```go\\ntype CalendarClient struct {\\n    pool       *pgxpool.Pool\\n    httpClient *http.Client\\n}\\n```\\n\\n**Methods:**\\n\\n- **`CreateEvent(ctx, tenantID, techEmail, slot)`** \u2192 `(eventID, error)`\\n  - Creates a calendar event on the tech's MS365 calendar\\n  - Returns the Graph event ID for future updates/deletes\\n  - Event subject: `[NexusOS] \ud83c\udfab T-123 \u2014 Printer Setup`\\n  - Event body: HTML with ticket/WO details, client, status, priority, notes, location\\n\\n- **`UpdateEvent(ctx, tenantID, techEmail, eventID, slot)`** \u2192 `error`\\n  - Updates an existing calendar event (PATCH)\\n  - Called when slot details change (time, date, priority, notes)\\n  - No-op if `eventID` is empty\\n\\n- **`DeleteEvent(ctx, tenantID, techEmail, eventID)`** \u2192 `error`\\n  - Removes a calendar event\\n  - Tolerates 404 (event already deleted)\\n  - No-op if `eventID` is empty\\n\\n**Token Management:**\\n\\nThe client caches access tokens and refreshes them when expired. Tokens are stored in the `email_accounts` table and refreshed via the Microsoft OAuth2 endpoint:\\n\\n```\\nPOST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token\\n```\\n\\nCredentials are fetched from `email_accounts` where `provider='microsoft365'` and `is_active=true`. The client uses `client_credentials` grant (service account, not user impersonation).\\n\\n### DispatchSlot\\n\\nA `DispatchSlot` represents a scheduled assignment of a technician to a ticket or work order.\\n\\n```go\\ntype DispatchSlot struct {\\n    ID              string    // UUID\\n    TenantID        string\\n    SourceType      string    // \\\"ticket\\\" or \\\"work_order\\\"\\n    SourceID        string    // UUID of the ticket or WO\\n    AssignedTo      string    // UUID of the tech (user)\\n    ScheduledDate   string    // \\\"2024-01-15\\\"\\n    StartTime       string    // \\\"09:00\\\" (optional, empty = all-day)\\n    EndTime         string    // \\\"11:30\\\" (optional)\\n    DurationMinutes int\\n    Status          string    // \\\"scheduled\\\", \\\"dispatched\\\", \\\"en_route\\\", \\\"on_site\\\", \\\"complete\\\", etc.\\n    Priority        string    // \\\"low\\\", \\\"normal\\\", \\\"high\\\", \\\"urgent\\\", \\\"emergency\\\"\\n    SiteAddress     string\\n    SiteCity        string\\n    SiteState       string\\n    SiteZip         string\\n    MSEventID       string    // Graph event ID (for sync)\\n    Notes           string\\n    DispatcherNotes string\\n    // ... timestamps and denormalized fields\\n}\\n```\\n\\n**Status flow:**\\n- `scheduled` \u2192 `dispatched` \u2192 `en_route` \u2192 `on_site` \u2192 `complete`\\n- Can jump to `cancelled` or `no_show` at any point\\n\\n**Display helpers:**\\n- `StatusLabel()`, `StatusColor()` \u2014 human-readable status with color\\n- `PriorityColor()` \u2014 color for priority badge\\n- `SourceIcon()`, `SourceLabel()` \u2014 emoji and text for ticket vs. WO\\n- `TimeDisplay()` \u2014 formatted time range or \\\"All Day\\\"\\n\\n### UnifiedItem\\n\\nA `UnifiedItem` is a ticket or work order presented as a dispatchable item in the unassigned queue. It merges fields from both sources so they can be displayed uniformly.\\n\\n```go\\ntype UnifiedItem struct {\\n    ID, SourceType, Number, Title, Priority, Status string\\n    CompanyID, CompanyName string\\n    AssignedTo, AssignedName string\\n    \\n    // WO-specific\\n    Trade, Phase, WOType string\\n    \\n    // Ticket-specific\\n    TicketType, Team string\\n    \\n    // Location\\n    SiteAddress, SiteCity, SiteState, SiteZip string\\n    \\n    // Schedule\\n    ScheduledStart, ScheduledEnd string\\n    \\n    CreatedAt time.Time\\n}\\n```\\n\\n## View Handlers\\n\\n### Calendar View\\n\\n**Handler:** `handleCalendarView`\\n\\nDisplays a 7-day week grid with slots organized by date. Each day shows all assigned slots for that date.\\n\\n**Query parameters:**\\n- `date` \u2014 base date (defaults to today); Monday of that week is calculated\\n- `tech` \u2014 filter by assigned technician (UUID)\\n- `type` \u2014 filter by source type (`ticket` or `work_order`)\\n\\n**Data structure:**\\n```go\\ntype CalendarDay struct {\\n    Date      string        // \\\"2024-01-15\\\"\\n    DayNum    int           // 15\\n    DayName   string        // \\\"Mon\\\"\\n    IsToday   bool\\n    IsWeekend bool\\n    Slots     []DispatchSlot\\n}\\n```\\n\\n**Rendering:** Calls `h.renderer.Render(w, \\\"dispatch/calendar.html\\\", data, true)` with HTMX flag.\\n\\n### Kanban View\\n\\n**Handler:** `handleKanbanView`\\n\\nDisplays columns for each technician with their assigned slots for a given date. Shows tech load (current slots vs. max slots) and unassigned items in a separate column.\\n\\n**Data structure:**\\n```go\\ntype TechLoad struct {\\n    UserID    string\\n    UserName  string\\n    Email     string\\n    SlotCount int\\n    MaxSlots  int           // from employee_availability\\n    Available bool\\n    Skills    []TechSkill\\n    Slots     []DispatchSlot\\n}\\n```\\n\\n**Load calculation:**\\n- Fetches `max_slots` from `employee_availability` for the day of week\\n- Counts active slots (not `complete` or `cancelled`)\\n- `LoadPercent()` returns percentage; `LoadColor()` returns color (green/yellow/red)\\n\\n### List View\\n\\n**Handler:** `handleListView`\\n\\nDisplays all slots in a filterable table format with date range, tech, type, and status filters.\\n\\n**Query parameters:**\\n- `from`, `to` \u2014 date range (defaults to today \u00b1 7 days)\\n- `tech`, `type`, `status` \u2014 filters\\n\\n### Map View\\n\\n**Handler:** `handleMapView`\\n\\nGroups slots by geographic location (city, state) and displays clusters. Useful for identifying geographic hotspots and optimizing routes.\\n\\n**Data structure:**\\n```go\\ntype MapCluster struct {\\n    City      string\\n    State     string\\n    Zip       string\\n    Label     string        // \\\"City, State\\\"\\n    SlotCount int\\n    Slots     []DispatchSlot\\n}\\n```\\n\\n### TV Display Mode\\n\\n**Handler:** `handleTVMode`\\n\\nFull-screen display mode for wall-mounted monitors in dispatch centers. Shows current date, tech list, and real-time slot updates.\\n\\n## Slot CRUD Operations\\n\\n### Create Slot\\n\\n**Endpoint:** `POST /api/dispatch/slots`\\n\\n**Form parameters:**\\n- `source_type` (required) \u2014 `\\\"ticket\\\"` or `\\\"work_order\\\"`\\n- `source_id` (required) \u2014 UUID of the source\\n- `assigned_to` (required) \u2014 UUID of the tech\\n- `scheduled_date` (required) \u2014 `\\\"2024-01-15\\\"`\\n- `start_time`, `end_time` (optional) \u2014 `\\\"09:00\\\"`, `\\\"11:30\\\"`\\n- `duration_minutes` (optional, default 60)\\n- `priority` (optional, default `\\\"normal\\\"`)\\n- `notes`, `dispatcher_notes` (optional)\\n\\n**Flow:**\\n1. Insert into `dispatch_slots` table\\n2. Asynchronously call `syncCalendarCreate()` to create MS365 event\\n3. Return success toast with HTMX trigger `dispatch-updated`\\n\\n**Site location:** Automatically fetched from the source work order (if WO); tickets don't have site info.\\n\\n### Update Slot\\n\\n**Endpoint:** `PUT /api/dispatch/slots/{id}`\\n\\n**Form parameters:** Same as create (all optional; only provided fields are updated)\\n\\n**Flow:**\\n1. Update `dispatch_slots` row\\n2. Asynchronously call `syncCalendarUpdate()` to sync changes to MS365\\n3. Return success toast\\n\\n### Update Slot Status\\n\\n**Endpoint:** `POST /api/dispatch/slots/{id}/status`\\n\\n**Form parameters:**\\n- `status` \u2014 new status value\\n\\n**Flow:**\\n1. Update status field\\n2. Sync to calendar (status changes may affect event display)\\n\\n### Delete Slot\\n\\n**Endpoint:** `DELETE /api/dispatch/slots/{id}`\\n\\n**Flow:**\\n1. Fetch `ms_event_id` and tech email before deletion\\n2. Delete from `dispatch_slots`\\n3. Asynchronously call `calendar.DeleteEvent()` to remove from MS365\\n4. Return success toast\\n\\n## Calendar Sync\\n\\nCalendar sync is **asynchronous** and non-blocking. If sync fails, the slot still exists in the database; the calendar just won't be updated.\\n\\n### Sync Create\\n\\n**Function:** `syncCalendarCreate(tenantID, slotID)`\\n\\n1. Fetch full slot details via `getSlotByID()`\\n2. Call `calendar.CreateEvent()` to create MS365 event\\n3. Store returned `eventID` in `dispatch_slots.ms_event_id`\\n\\n### Sync Update\\n\\n**Function:** `syncCalendarUpdate(tenantID, slotID)`\\n\\n1. Fetch full slot details\\n2. If `ms_event_id` is empty, skip (event was never created)\\n3. Call `calendar.UpdateEvent()` to update MS365 event\\n\\n### Sync Delete\\n\\nHandled inline in `handleDeleteSlot()`:\\n\\n1. Fetch `ms_event_id` and tech email\\n2. Delete slot from database\\n3. Call `calendar.DeleteEvent()` asynchronously\\n\\n## Unassigned Items\\n\\n**Endpoint:** `GET /api/dispatch/unassigned`\\n\\n**Function:** `getUnassignedItems(ctx, tenantID, limit)`\\n\\nReturns tickets and work orders that have no active dispatch slot. Uses a UNION query:\\n\\n```sql\\n(SELECT ... FROM tickets WHERE status NOT IN ('resolved','closed')\\n AND NOT EXISTS (SELECT 1 FROM dispatch_slots WHERE source_type='ticket' AND ...))\\nUNION ALL\\n(SELECT ... FROM work_orders WHERE status NOT IN ('complete','cancelled','invoiced')\\n AND NOT EXISTS (SELECT 1 FROM dispatch_slots WHERE source_type='work_order' AND ...))\\nORDER BY created_at DESC\\nLIMIT ?\\n```\\n\\nReturns `[]UnifiedItem` with merged ticket/WO fields.\\n\\n## AI-Powered Suggestions\\n\\n**Endpoint:** `POST /api/dispatch/suggest`\\n\\n**Form parameters:**\\n- `source_type` \u2014 `\\\"ticket\\\"` or `\\\"work_order\\\"`\\n- `source_id` \u2014 UUID\\n- `scheduled_date` (optional, defaults to today)\\n\\n**Flow:**\\n\\n1. Fetch item details (title, priority, trade/category, location, company)\\n2. Fetch all active techs with their skills and current load for the date\\n3. Build context string with tech names, skills, proficiency, and slot count\\n4. Call Claude with system prompt and user prompt\\n5. Parse JSON response: `{tech_id, tech_name, reasoning, confidence}`\\n6. Render HTML card with recommendation and \\\"Assign\\\" button\\n\\n**System prompt:** Instructs Claude to recommend the best tech considering skill match, workload, geographic proximity, and proficiency.\\n\\n**Cost tracking:** Uses `ai.UsageMeta` to track API costs per tenant and action.\\n\\n## Tech Load Endpoint\\n\\n**Endpoint:** `GET /api/dispatch/tech-load`\\n\\n**Query parameters:**\\n- `date` (optional, defaults to today)\\n\\n**Response:** JSON array of `TechLoad` objects with:\\n- `user_id`, `user_name`, `email`\\n- `slot_count` \u2014 active slots on that date\\n- `max_slots` \u2014 from `employee_availability`\\n- `available` \u2014 whether tech is available that day\\n\\nUsed by frontend to display load bars and availability status.\\n\\n## Database Schema (Relevant Tables)\\n\\n### dispatch_slots\\n\\n```sql\\nCREATE TABLE dispatch_slots (\\n    id UUID PRIMARY KEY,\\n    tenant_id UUID NOT NULL,\\n    source_type TEXT NOT NULL,  -- 'ticket' or 'work_order'\\n    source_id UUID NOT NULL,\\n    assigned_to UUID,           -- user (tech)\\n    scheduled_date DATE NOT NULL,\\n    start_time TIME,\\n    end_time TIME,\\n    duration_minutes INT DEFAULT 60,\\n    status TEXT DEFAULT 'scheduled',\\n    priority TEXT DEFAULT 'normal',\\n    site_address TEXT,\\n    site_city TEXT,\\n    site_state TEXT,\\n    site_zip TEXT,\\n    ms_event_id TEXT,           -- Graph event ID\\n    notes TEXT,\\n    dispatcher_notes TEXT,\\n    created_by UUID,\\n    created_at TIMESTAMP,\\n    updated_at TIMESTAMP\\n);\\n```\\n\\n### email_accounts\\n\\nUsed to fetch MS365 credentials:\\n\\n```sql\\nCREATE TABLE email_accounts (\\n    id UUID PRIMARY KEY,\\n    tenant_id UUID NOT NULL,\\n    email_address TEXT NOT NULL,\\n    provider TEXT,              -- 'microsoft365'\\n    is_active BOOLEAN,\\n    ms_tenant_id TEXT,\\n    ms_client_id TEXT,\\n    ms_client_secret_enc TEXT,  -- encrypted\\n    ms_access_token TEXT,\\n    ms_refresh_token TEXT,\\n    ms_token_expires_at TIMESTAMP,\\n    created_at TIMESTAMP,\\n    updated_at TIMESTAMP\\n);\\n```\\n\\n### employee_availability\\n\\nDefines weekly schedule template:\\n\\n```sql\\nCREATE TABLE employee_availability (\\n    id UUID PRIMARY KEY,\\n    user_id UUID NOT NULL,\\n    day_of_week INT,            -- 0=Sunday, 6=Saturday\\n    available BOOLEAN,\\n    start_time TIME,\\n    end_time TIME,\\n    max_slots INT DEFAULT 4\\n);\\n```\\n\\n## Integration Points\\n\\n### Authentication\\n\\nAll handlers extract `tenantID` and `userID` from context via `auth.TenantIDFromContext()` and `auth.UserIDFromContext()`. Multi-tenancy is enforced at the database query level.\\n\\n### UI Rendering\\n\\nUses `ui.Renderer` to render HTML templates. HTMX partials are rendered with `isHTMX=true` flag to skip layout wrapper.\\n\\n### AI Provider\\n\\nOptional `ai.Provider` is wired in via `SetAI()`. If not configured, the suggest endpoint returns 503.\\n\\n### Microsoft Graph API\\n\\nRequires:\\n- Tenant ID, Client ID, Client Secret (stored in `email_accounts`)\\n- OAuth2 token endpoint: `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token`\\n- Graph endpoint: `https://graph.microsoft.com/v1.0/users/{email}/calendar/events`\\n\\n## Error Handling\\n\\n- **Calendar sync failures** are logged but don't block slot creation/update\\n- **Graph API errors** (4xx, 5xx) are logged with truncated response body (first 500 chars)\\n- **Database errors** return 500 with generic message\\n- **Missing credentials** return 500 with specific error message\\n- **AI failures** return 503 (service unavailable)\\n\\n## Performance Considerations\\n\\n- **Calendar sync is async**: Slots are created immediately; calendar updates happen in background goroutines with 30-second timeout\\n- **Queries use indexes**: `dispatch_slots(tenant_id, scheduled_date, status)` and `dispatch_slots(assigned_to, scheduled_date)`\\n- **Denormalization**: Slot queries fetch tech name, email, source title, company name in a single query (no N+1)\\n- **Token caching**: MS365 access tokens are cached in memory and only refreshed when expired (with 5-minute buffer)\\n\\n## Common Workflows\\n\\n### Dispatch a Ticket\\n\\n1. User views unassigned tickets via `/api/dispatch/unassigned`\\n2. User clicks \\\"Assign\\\" or uses AI suggestion\\n3. POST to `/api/dispatch/slots` with ticket ID, tech, date, time\\n4. Slot is created; calendar event is created asynchronously\\n5. Tech's calendar is updated in MS365\\n\\n### Reschedule a Slot\\n\\n1. User clicks slot in calendar/kanban view\\n2. User updates date/time/tech in modal\\n3. PUT to `/api/dispatch/slots/{id}` with new values\\n4. Slot is updated; calendar event is updated asynchronously\\n\\n### Complete a Slot\\n\\n1. User changes status to `\\\"complete\\\"` via status dropdown\\n2. POST to `/api/dispatch/slots/{id}/status`\\n3. Slot status is updated; calendar event is updated (if needed)\\n\\n### Cancel a Slot\\n\\n1. User deletes slot via DELETE button\\n2. DELETE to `/api/dispatch/slots/{id}`\\n3. Slot is deleted; calendar event is deleted asynchronously\\n\\n## Testing Considerations\\n\\n- Mock `pgxpool.Pool` for database queries\\n- Mock `http.Client` for Graph API calls\\n- Mock `ai.Provider` for Claude calls\\n- Test calendar sync with fake Graph responses\\n- Test multi-tenancy isolation (queries filter by `tenant_id`)\\n- Test token refresh logic (expired token \u2192 new token)\",\"internal-distributor\":\"# internal \u2014 distributor\\n\\n# Distributor Module\\n\\nThe distributor module ingests and surfaces product catalogs, pricing, and subscription data from upstream distributors (PAX8, TD Synnex, Ingram, D&amp;H, Microsoft NCE). It powers three main UI surfaces: a cross-distributor product browser, a client subscriptions tab, and contract management with Contract Change Request (CCR) workflows.\\n\\n## Architecture Overview\\n\\nThe module is organized into three layers:\\n\\n1. **Adapter Layer** (`wrapper.go`, `provisioner.go`) \u2014 Contracts that per-distributor implementations satisfy\\n2. **Sync &amp; Orchestration** (`sync.go`, `scheduler.go`) \u2014 Nightly ingestion and data persistence\\n3. **Query &amp; Handler Layer** (`catalog_query.go`, `subs_query.go`, `contract_query.go`, `handler.go`) \u2014 Read-side surfaces and HTTP endpoints\\n\\nData flows one direction: distributors \u2192 NexusOS. The module never writes back to QBO or the financial ledger. All distributor data lives in `distributor_*` tables and is matched (best-effort) to the canonical `products` and `vendors` tables via manufacturer part number and display name.\\n\\n```mermaid\\ngraph LR\\n    A[\\\"DistributorMCP Servers\\\"] --&gt;|\\\"ListProductsGetPricingListSubscriptions\\\"| B[\\\"WrapperAdapters\\\"]\\n    B --&gt;|SyncOne| C[\\\"Syncer\\\"]\\n    C --&gt;|upsert| D[\\\"distributor_*Tables\\\"]\\n    D --&gt;|\\\"LoadCatalogLoadClientSubscriptions\\\"| E[\\\"Query Layer\\\"]\\n    E --&gt;|HTTP| F[\\\"HandlerUI Surfaces\\\"]\\n    G[\\\"Scheduler\\\"] --&gt;|runOnce| B\\n```\\n\\n## Core Types\\n\\n### Distributor Identifiers\\n\\n**`Key`** is the stable code for each distributor:\\n- `pax8`, `tdsynnex`, `ingram`, `dh`, `msnce`\\n\\nRows are joint-keyed on `(mcp_server_id, distributor_key)` because one MCP server can host multiple distributors.\\n\\n### Data Models\\n\\n**`Product`** mirrors one distributor catalog entry. It carries:\\n- `DistributorSKU` \u2014 the distributor's catalog identifier\\n- `MfgPartNumber` \u2014 manufacturer part number (canonical bridge across distributors)\\n- `VendorID`, `ProductID` \u2014 nullable FKs to QBO-backed tables (matched during sync)\\n- `Raw` \u2014 full JSONB payload from the distributor\\n\\n**`Pricing`** is one tier of pricing for a product:\\n- `Tier` \u2014 'standard', 'volume', 'partner', 'msrp'\\n- `QtyMin`, `QtyMax` \u2014 quantity breakpoints\\n- `BillingTerm` \u2014 'monthly', 'annual', 'one_time'\\n\\n**`Subscription`** is one active subscription a client has at a distributor:\\n- `DistributorSubID` \u2014 distributor's subscription identifier\\n- `ClientCompanyID` \u2014 nullable; populated when matched to a NexusOS company\\n- `ContractLineItemID` \u2014 nullable; set when linked into a contract\\n- `Status` \u2014 'active', 'pending', 'cancelled', 'paused'\\n\\n**`SyncRun`** records one sync attempt:\\n- `Status` \u2014 'running', 'success', 'partial', 'failed'\\n- `Errors` \u2014 JSONB array of `SyncError` entries (method, message, timestamp)\\n- Counters for products/pricing/subs added/updated\\n\\n## Sync Pipeline\\n\\n### Wrapper Interface\\n\\nEvery distributor adapter implements `Wrapper`:\\n\\n```go\\ntype Wrapper interface {\\n    Key() Key\\n    ListProducts(ctx context.Context) ([]Product, error)\\n    GetPricing(ctx context.Context, distributorSKU string) ([]Pricing, error)\\n    ListSubscriptions(ctx context.Context) ([]Subscription, error)\\n}\\n```\\n\\nAdapters live in `internal/mcpvendors//` and are instantiated by a `WrapperFactory` supplied at startup.\\n\\n### SyncOne Orchestration\\n\\n`Syncer.SyncOne()` runs one full cycle for a single `(tenant, mcp_server, distributor)` triple:\\n\\n1. **Open** a `distributor_sync_runs` row with status='running'\\n2. **ListProducts** \u2192 upsert each product, collecting IDs\\n3. **GetPricing** \u2192 for each product, fetch and upsert tiered pricing\\n4. **ListSubscriptions** \u2192 upsert subscriptions, resolving to product IDs via `lookupKey()`\\n5. **Close** the sync_run with final status (success/partial/failed) and error counts\\n\\nMatching rules (read-only):\\n- **vendor_id** \u2014 case-insensitive match on `vendors.display_name`\\n- **product_id** \u2014 exact match on `products.manufacturer_part_number`\\n\\nBoth FKs remain NULL if no match is found; they may be populated on a later sync.\\n\\n### Scheduler\\n\\n`Scheduler.Start()` runs nightly (configurable interval):\\n\\n1. Query all tenants with active MCP servers\\n2. For each tenant, call the `WrapperFactory` to get active wrappers\\n3. For each wrapper, call `SyncOne()` with `TriggerCron`\\n\\nThe factory is responsible for decrypting credentials and instantiating adapters; the scheduler only orchestrates.\\n\\n## Query Layer\\n\\n### Catalog Browser (Surface 2)\\n\\n**`LoadCatalog()`** returns the grouped catalog view:\\n- Groups rows by `mfg_part_number` so one logical product shows pricing across all distributors\\n- Filters by search (name, mfg_part_number, distributor_sku), vendor name, or distributor key\\n- Selects the **standard tier** with `qty_min = 1` for best-price logic\\n- Returns `CatalogRow` with:\\n  - `Sources` \u2014 one `CatalogSource` per distributor carrying the product\\n  - `BestPrice` \u2014 lowest unit_cost across sources\\n  - `BestVendor` \u2014 which distributor offers it\\n\\n**`LoadCatalogStats()`** returns header tile counts:\\n- Total products, unique vendors, connected distributors, stale products (&gt;7 days)\\n\\n### Client Subscriptions Tab (Surface 3)\\n\\n**`LoadClientSubscriptions()`** returns every subscription for a client, decorated with:\\n- Product name, vendor name, manufacturer part number\\n- Linked contract info (ID, number, name, lock_state)\\n- Ordered by status (active first) then renewal date (expiring soon rises to top)\\n\\n**`LoadDraftContractsForCompany()`** returns contracts in 'draft' state \u2014 the only ones that accept new lines directly.\\n\\n**`HasLockedContractsForCompany()`** checks if the client has locked contracts (used to explain why a subscription is pending).\\n\\n**`LinkSubscriptionToContract()`** wires a subscription into a draft contract:\\n1. Verify contract is 'draft' (using `FOR UPDATE` to prevent race)\\n2. Create a `legal_contract_line_items` row with product description\\n3. Back-link the subscription's `contract_line_item_id` FK\\n4. Refuse if contract is locked \u2014 that path requires a CCR\\n\\n### Contract Distributor Section (Surface 4)\\n\\n**`LoadContractLockInfo()`** returns the contract's lock_state and company_id.\\n\\n**`LoadContractDistributorLines()`** returns subscriptions linked to the contract's line items, ordered by status (active first) then product name.\\n\\n**`LoadCCRCandidates()`** returns unlinked active subscriptions for the same client \u2014 the menu for \\\"add\\\" actions in the CCR modal.\\n\\n**`LoadContractAudit()`** returns `contract_line_audit` rows for any line on the contract, joined to line description and changed-by user. Limited to 200 rows, ordered by timestamp descending.\\n\\n### Contract Change Requests\\n\\n**`CreateCCR()`** persists a new CCR and its proposed items:\\n1. Verify contract is 'draft' or 'locked' (using `FOR UPDATE`)\\n2. Insert `contract_change_requests` row with status='draft'\\n3. Insert `contract_change_request_items` rows (one per add/remove action)\\n4. Transition contract from 'locked' \u2192 'unlocked_pending_ccr' so future syncs queue rather than auto-add\\n5. Return the new CCR ID\\n\\nThe actual add/remove of contract lines happens later when the CCR is signed (Phase 2 ships data + UI; signature wiring is deferred).\\n\\n## HTTP Handler\\n\\n`Handler` serves four endpoints:\\n\\n| Endpoint | Method | Purpose |\\n|----------|--------|---------|\\n| `/distributors/catalog` | GET | Catalog Browser (Surface 2) |\\n| `/api/company-subscriptions/{companyID}` | GET | Subscriptions tab partial (Surface 3) |\\n| `/api/company-subscriptions/{subID}/link` | POST | Link subscription to draft contract |\\n| `/api/contracts/{id}/distributor-section` | GET | Contract Distributor panel (Surface 4) |\\n| `/api/contracts/{id}/ccr-modal` | GET | CCR modal (candidates + existing lines) |\\n| `/api/contracts/{id}/ccr` | POST | Create CCR |\\n\\nAll endpoints extract `tenantID` from context and render HTMX partials. The handler uses `ui.Renderer` to populate template data and stream HTML.\\n\\n### Catalog Browser Flow\\n\\n```\\nGET /distributors/catalog?q=...&amp;vendor=...&amp;dist=...\\n  \u2192 LoadCatalog(filter)\\n  \u2192 LoadCatalogStats()\\n  \u2192 Render(\\\"distributor/catalog.html\\\", {Rows, Stats, Filter, DistributorOptions})\\n```\\n\\nQuery string parameters are bookmarkable and shareable.\\n\\n### Subscriptions Tab Flow\\n\\n```\\nGET /api/company-subscriptions/{companyID}\\n  \u2192 LoadClientSubscriptions()\\n  \u2192 LoadDraftContractsForCompany()\\n  \u2192 HasLockedContractsForCompany()\\n  \u2192 Render(\\\"distributor/subs_partial.html\\\", {Subs, DraftContracts, HasLocked})\\n```\\n\\nThe template uses lock_state to decide whether to show \\\"Link to Contract\\\" (draft) or \\\"Generate CCR\\\" (locked) buttons.\\n\\n### Link Subscription Flow\\n\\n```\\nPOST /api/company-subscriptions/{subID}/link\\n  \u2192 LinkSubscriptionToContract(subID, contractID)\\n  \u2192 Re-render subscriptions partial (HTMX swap)\\n```\\n\\nIf the contract is locked, returns 409 Conflict with `ErrContractLocked`.\\n\\n### Contract Distributor Section Flow\\n\\n```\\nGET /api/contracts/{id}/distributor-section\\n  \u2192 LoadContractLockInfo()\\n  \u2192 LoadContractDistributorLines()\\n  \u2192 LoadContractAudit()\\n  \u2192 Render(\\\"distributor/contract_section.html\\\", {Contract, Lines, Audit})\\n```\\n\\n### CCR Modal Flow\\n\\n```\\nGET /api/contracts/{id}/ccr-modal\\n  \u2192 LoadContractLockInfo()\\n  \u2192 LoadCCRCandidates()\\n  \u2192 LoadContractDistributorLines()\\n  \u2192 Render(\\\"distributor/ccr_modal.html\\\", {Contract, Candidates, Existing})\\n```\\n\\nThe modal shows checkboxes for candidates (to add) and existing lines (to remove).\\n\\n### Create CCR Flow\\n\\n```\\nPOST /api/contracts/{id}/ccr\\n  Form: add_sub_id[], remove_sub_id[]\\n  \u2192 CreateCCR(items)\\n  \u2192 Re-render contract section (HTMX swap)\\n```\\n\\n## Provisioning (Phase 3)\\n\\nThe `Provisioner` interface extends `Wrapper` for adapters that can dispatch orders:\\n\\n```go\\ntype Provisioner interface {\\n    Wrapper\\n    Provision(ctx context.Context, req ProvisionRequest) (*ProvisionResult, error)\\n}\\n```\\n\\n**`ProvisionRequest`** carries:\\n- `IdempotencyKey` \u2014 caller-minted: `\\\"po:{po_id}:attempt:{seq}\\\"`\\n- `ClientCompanyID` \u2014 distributor's customer identifier\\n- `Lines` \u2014 SKU/qty list\\n- `StartDate` \u2014 optional SaaS subscription start date\\n- `Notes` \u2014 free-text annotation\\n\\n**`ProvisionResult`** returns:\\n- `SubscriptionID` \u2014 for SaaS lines\\n- `OrderID` \u2014 for materials-style dispatches\\n- `Status` \u2014 distributor's reported state\\n- `RawResponse` \u2014 full upstream payload for audit\\n\\n**`ProvisionError`** routes failures into the remediation queue:\\n- `ErrClassSKUInvalid` \u2014 SKU doesn't exist\\n- `ErrClassQtyUnavailable` \u2014 insufficient inventory\\n- `ErrClassAuth` \u2014 credential failure\\n- `ErrClassTransport` \u2014 network/5xx failure\\n- `ErrClassUnknown` \u2014 other\\n\\nIdempotency is caller-minted: transport retries reuse the same key; intentional retries bump `attempt_seq`.\\n\\n## Integration Points\\n\\n### Incoming\\n\\n- **`internal/tenant/handler.go`** \u2014 `handleDistributorSync()` triggers manual syncs via `SyncOne()`\\n- **`internal/procurement/service.go`** \u2014 `Dispatch()` calls `AsProvisioner()` to place orders\\n- **`internal/mcpvendors/factory.go`** \u2014 `NewDefaultFactory()` instantiates wrappers for the scheduler\\n- **`cmd/psa/main.go`** \u2014 wires `NewScheduler()` and `NewHandler()` at startup\\n\\n### Outgoing\\n\\n- **`internal/auth/middleware.go`** \u2014 `TenantIDFromContext()`, `UserIDFromContext()` for request context\\n- **`internal/ui/render.go`** \u2014 `Render()` to stream HTMX partials\\n- **Database** \u2014 `pgxpool.Pool` for all queries\\n\\n## Key Design Decisions\\n\\n1. **Go-side aggregation** \u2014 `LoadCatalog()` pulls flat rows and groups by mfg_part_number in Go rather than SQL CTEs. Simpler to maintain when best-tier rules evolve; catalog is bounded (~hundreds to low thousands per tenant).\\n\\n2. **Best-price on standard tier only** \u2014 Volume and partner tiers are surfaced on detail expand rows, not in the main catalog. Keeps the browser focused on the most common pricing.\\n\\n3. **Nullable FKs for matching** \u2014 `vendor_id` and `product_id` are best-effort matches. If a product doesn't match, it still gets synced and displayed; it may match on a later sync.\\n\\n4. **Idempotency at the caller** \u2014 Provisioners don't mint idempotency keys; the procurement service does. This allows transport retries to reuse the same key while intentional retries bump the sequence.\\n\\n5. **Contract lock_state gates CCR** \u2014 Draft contracts accept new lines directly; locked contracts require a signed CCR. This prevents accidental overwrites of finalized agreements.\\n\\n6. **Audit trail in contract_line_audit** \u2014 Every change to a contract line (sync, CCR, manual) is logged with source, field, old/new values, and who made it. Supports compliance and debugging.\",\"internal-drawing\":\"# internal \u2014 drawing\\n\\n# Drawing Module\\n\\nThe drawing module manages technical drawings, floor plans, and construction documents within NexusOS. It provides CRUD operations, version tracking, AI-powered analysis (floor plan extraction and takeoff quantification), and 3D isometric visualization.\\n\\n## Overview\\n\\nDrawings are central to construction projects. This module handles:\\n\\n- **Document management**: Create, update, delete drawings with metadata (type, status, building/floor location, scale, revision)\\n- **Version control**: Track multiple file revisions with upload history\\n- **Relationships**: Link drawings to surveys, bids, and projects\\n- **AI analysis**: \\n  - Floor plan vision analysis \u2192 room/door/feature extraction \u2192 3D isometric rendering\\n  - Takeoff extraction \u2192 device/cable/room quantity counts for bid reconciliation\\n- **Multi-format support**: PDF, DWG, PNG, JPG, SVG\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    Handler[\\\"Handler(CRUD + routes)\\\"]\\n    Vision[\\\"Vision Analysis(floor plan extraction)\\\"]\\n    Takeoff[\\\"Takeoff Analysis(quantity extraction)\\\"]\\n    Isometric[\\\"Isometric Renderer(3D SVG generation)\\\"]\\n    \\n    Handler --&gt;|analyzeFloorPlan| Vision\\n    Handler --&gt;|RunTakeoffAndPersist| Takeoff\\n    Handler --&gt;|generateIsometricSVG| Isometric\\n    Vision --&gt;|FloorPlanData| Isometric\\n    Takeoff --&gt;|TakeoffData| DB[\\\"Database(drawings table)\\\"]\\n    Handler --&gt;|CRUD| DB\\n```\\n\\n## Core Components\\n\\n### Handler (`handler.go`)\\n\\nThe HTTP request handler and business logic coordinator.\\n\\n**Key methods:**\\n\\n- **CRUD operations**\\n  - `handleList()` \u2014 Query drawings with optional type/status filters\\n  - `handleCreate()` \u2014 Insert new drawing + optional initial file upload\\n  - `handleDetail()` \u2014 Load drawing + version history\\n  - `handleUpdate()` \u2014 Update metadata (title, status, relationships)\\n  - `handleDelete()` \u2014 Soft or hard delete\\n\\n- **Version management**\\n  - `handleUploadVersion()` \u2014 Add a new revision to a drawing\\n  - `handleDeleteVersion()` \u2014 Remove a specific version file\\n  - `saveVersion()` \u2014 Persist file to disk, insert DB record with version number\\n\\n- **AI integration**\\n  - `handleAnalyzeDrawing()` \u2014 Trigger floor plan vision analysis\\n  - `handleRunTakeoff()` \u2014 Trigger takeoff extraction\\n  - `handleDrawing3DView()` \u2014 Render 3D isometric view from analysis\\n\\n- **Data loaders**\\n  - `loadDrawing()` \u2014 Fetch single drawing with denormalized company/user names\\n  - `loadVersions()` \u2014 Fetch all versions for a drawing\\n  - `loadCompanies()`, `loadSurveys()`, `loadBids()`, `loadProjects()` \u2014 Populate form dropdowns\\n\\n**Initialization:**\\n\\n```go\\nhandler := NewHandler(pool, renderer)\\nhandler.SetAI(aiProvider)  // Optional; required for vision/takeoff\\nhandler.RegisterRoutes(mux)\\n```\\n\\nRoutes are registered as:\\n- `GET /drawings` \u2014 List\\n- `GET /drawings/new` \u2014 New form\\n- `GET /drawings/{id}` \u2014 Detail\\n- `GET /drawings/{id}/edit` \u2014 Edit form\\n- `POST /api/drawings` \u2014 Create\\n- `PUT /api/drawings/{id}` \u2014 Update\\n- `DELETE /api/drawings/{id}` \u2014 Delete\\n- `POST /api/drawings/{id}/versions` \u2014 Upload version\\n- `DELETE /api/drawings/{id}/versions/{versionId}` \u2014 Delete version\\n- `POST /api/drawings/{id}/analyze` \u2014 Analyze floor plan\\n- `POST /api/drawings/{id}/takeoff` \u2014 Run takeoff\\n- `GET /drawings/{id}/3d` \u2014 3D view\\n\\n### Vision Analysis (`vision.go`)\\n\\nClaude Vision extracts structural data from floor plan images for 3D rendering.\\n\\n**Data types:**\\n\\n- `FloorPlanData` \u2014 Top-level extraction result\\n  - `BuildingName`, `FloorName` \u2014 Metadata\\n  - `Width`, `Height` \u2014 Overall dimensions in feet\\n  - `Rooms` \u2014 Array of `ExtractedRoom`\\n  - `Doors` \u2014 Array of `ExtractedDoor`\\n  - `Corridors` \u2014 Hallways/passages\\n  - `Features` \u2014 Fire exits, panels, stairs, elevators\\n\\n- `ExtractedRoom` \u2014 A space on the floor plan\\n  - `Name`, `Type` \u2014 Label and category (office, bathroom, conference, etc.)\\n  - `X`, `Y`, `W`, `H` \u2014 Position and size as percentages (0\u2013100)\\n  - `Color` \u2014 Optional override color\\n\\n- `ExtractedDoor` \u2014 Door location and type\\n  - `X`, `Y` \u2014 Position\\n  - `Direction` \u2014 north/south/east/west\\n  - `Type` \u2014 standard, double, fire_exit, emergency\\n\\n- `ExtractedFeature` \u2014 Notable infrastructure\\n  - `Type` \u2014 fire_extinguisher, fire_alarm, electrical_panel, exit_sign, stairs, elevator, sprinkler, smoke_detector\\n  - `X`, `Y` \u2014 Position\\n\\n**Key function:**\\n\\n```go\\nfloorPlan, cost, err := h.analyzeFloorPlan(ctx, tenantID, filePath)\\n```\\n\\nSends the image to Claude Vision with a structured extraction prompt. Returns parsed JSON or error. Cost is tracked for billing.\\n\\n**Workflow:**\\n\\n1. User uploads an image (PNG/JPG) to a drawing\\n2. User clicks \\\"Analyze Floor Plan\\\"\\n3. `handleAnalyzeDrawing()` calls `analyzeFloorPlan()`\\n4. Claude Vision extracts rooms, doors, features\\n5. Result is stored in `drawings.ai_analysis` (JSONB)\\n6. User can view 3D isometric rendering\\n\\n### Takeoff Analysis (`takeoff.go`)\\n\\nClaude Vision extracts quantity data (device counts, cable lengths, room areas) for bid reconciliation.\\n\\n**Data types:**\\n\\n- `TakeoffData` \u2014 Structured quantity extraction\\n  - `DrawingType` \u2014 floor_plan, riser_diagram, cable_path, network_diagram, rack_elevation, as_built, other\\n  - `Sheet`, `Scale` \u2014 Metadata\\n  - `Devices` \u2014 Array of `TakeoffDevice` (counted symbols)\\n  - `Cables` \u2014 Array of `TakeoffCableRun` (estimated lengths)\\n  - `Rooms` \u2014 Array of `TakeoffRoom` (area estimates)\\n  - `Totals` \u2014 Summary counts for UI display\\n  - `Notes` \u2014 Caveats or assumptions\\n\\n- `TakeoffDevice` \u2014 A counted device symbol\\n  - `Category` \u2014 data_drop, voice_drop, wap, camera, card_reader, speaker, motion, door_contact, panel, rack, fire_alarm, smoke_detector, other\\n  - `Label` \u2014 Human description\\n  - `Symbol` \u2014 Legend symbol if visible\\n  - `Qty` \u2014 Count\\n\\n- `TakeoffCableRun` \u2014 Aggregated cable path\\n  - `Category` \u2014 cat6, cat6a, fiber_sm, fiber_mm, coax, speaker_wire, fire_alarm, other\\n  - `Label` \u2014 Description\\n  - `LengthFeet` \u2014 Estimated total length\\n  - `RunCount` \u2014 Number of distinct runs\\n\\n- `TakeoffRoom` \u2014 Area estimate\\n  - `Name` \u2014 Room label\\n  - `AreaSqFt` \u2014 Estimated square footage\\n  - `DeviceCount` \u2014 Devices in this room\\n\\n- `TakeoffTotal` \u2014 Summary aggregate\\n  - `Label` \u2014 e.g., \\\"Total data drops\\\"\\n  - `Qty` \u2014 Count or length\\n  - `Unit` \u2014 \\\"ea\\\", \\\"ft\\\", etc.\\n\\n**Key functions:**\\n\\n```go\\ndata, cost, err := h.AnalyzeTakeoff(ctx, tenantID, filePath, inScopeTrades...)\\nerr := h.RunTakeoffAndPersist(ctx, tenantID, drawingID, filePath)\\n```\\n\\n`AnalyzeTakeoff()` sends the image to Claude Vision and returns parsed takeoff data. `RunTakeoffAndPersist()` runs the analysis and stores the result in `drawings.takeoff_json` + `takeoff_status`.\\n\\n**Bid-scoped extraction:**\\n\\nWhen a drawing is attached to a bid, the handler loads the bid's in-scope trades and passes them to the Vision prompt via `buildScopeAddendum()`. This hard-gates the model to only count devices belonging to those trades, preventing cross-trade contamination (e.g., not counting fire alarm devices on a cabling bid).\\n\\n```go\\ninScopeTrades := h.loadBidTradesForDrawing(ctx, tenantID, drawingID)\\ndata, cost, err := h.AnalyzeTakeoff(ctx, tenantID, filePath, inScopeTrades...)\\n```\\n\\n**Workflow:**\\n\\n1. User uploads a drawing (floor plan, riser, cable path, etc.)\\n2. User clicks \\\"Run Takeoff\\\"\\n3. `handleRunTakeoff()` calls `RunTakeoffAndPersist()`\\n4. Handler loads bid trades (if drawing is linked to a bid)\\n5. Claude Vision extracts quantities with trade-scoped prompt\\n6. Result is stored in `drawings.takeoff_json` and `takeoff_status='complete'`\\n7. Bid reconciliation engine (internal/bidengine) reads this data to diff against spec requirements\\n\\n### Isometric Renderer (`isometric.go`)\\n\\nPure server-side SVG generation of floor plans in 3D isometric projection. No JavaScript, no external libraries.\\n\\n**Projection math:**\\n\\nIsometric projection converts 2D floor coordinates (x, y) + height (z) to screen coordinates:\\n\\n```\\nscreenX = (x - y) * cos(30\u00b0) + offsetX\\nscreenY = (x + y) * sin(30\u00b0) - z + offsetY\\n```\\n\\n**Key function:**\\n\\n```go\\nsvg := generateIsometricSVG(floorPlan)\\n```\\n\\nGenerates an inline SVG string from `FloorPlanData`. The SVG includes:\\n\\n- **Background** \u2014 Dark theme (0a0e17)\\n- **Floor base** \u2014 Ground plane polygon\\n- **Corridors** \u2014 Rendered first (below rooms) with muted color\\n- **Rooms** \u2014 Extruded 3D boxes with color-coded types (office=blue, bathroom=cyan, kitchen=amber, etc.)\\n- **Doors** \u2014 Small diamond markers\\n- **Features** \u2014 Icons (fire extinguisher, stairs, elevator, etc.)\\n- **Labels** \u2014 Room names and dimensions on top faces\\n- **Legend** \u2014 Summary counts at bottom\\n\\n**Room rendering:**\\n\\nEach room is drawn as a 3D box with six faces:\\n\\n1. **Floor (bottom)** \u2014 Dark, low opacity\\n2. **Back-left wall** \u2014 Darkest (far side)\\n3. **Back-right wall** \u2014 Dark (far side)\\n4. **Right wall** \u2014 Medium shade (visible side)\\n5. **Front wall** \u2014 Brightest (visible side)\\n6. **Top face** \u2014 Brightest, with white edge stroke\\n\\nOpacity and shading create depth perception. Room names and dimensions are centered on the top face.\\n\\n**Color scheme:**\\n\\nRooms are color-coded by type (office, conference, bathroom, kitchen, server_room, etc.). Colors are muted and professional. Custom colors can override defaults.\\n\\n**Workflow:**\\n\\n1. User clicks \\\"View 3D\\\" on a drawing detail page\\n2. `handleDrawing3DView()` loads the drawing's `ai_analysis` (FloorPlanData)\\n3. `generateIsometricSVG()` renders the SVG\\n4. SVG is embedded in the HTML response\\n5. Browser displays the 3D floor plan\\n\\n## Data Model\\n\\n### Drawing\\n\\n```sql\\nCREATE TABLE drawings (\\n  id UUID PRIMARY KEY,\\n  tenant_id UUID NOT NULL,\\n  company_id UUID,\\n  survey_id UUID,\\n  bid_id UUID,\\n  project_id UUID,\\n  \\n  title TEXT NOT NULL,\\n  description TEXT,\\n  drawing_type VARCHAR(50),  -- floor_plan, network_diagram, riser_diagram, etc.\\n  status VARCHAR(50),        -- draft, in_review, approved, superseded, archived\\n  \\n  building TEXT,\\n  floor TEXT,\\n  area TEXT,\\n  scale TEXT,\\n  sheet_number TEXT,\\n  revision TEXT,\\n  \\n  ai_analysis JSONB,         -- FloorPlanData from vision analysis\\n  ai_cost NUMERIC,           -- Cumulative Claude Vision cost\\n  \\n  takeoff_json JSONB,        -- TakeoffData from takeoff analysis\\n  takeoff_status VARCHAR(50), -- processing, complete, failed\\n  takeoff_error TEXT,\\n  takeoff_ai_cost NUMERIC,\\n  takeoff_processed_at TIMESTAMP,\\n  \\n  created_by UUID,\\n  created_at TIMESTAMP,\\n  updated_at TIMESTAMP\\n);\\n```\\n\\n### DrawingVersion\\n\\n```sql\\nCREATE TABLE drawing_versions (\\n  id UUID PRIMARY KEY,\\n  tenant_id UUID NOT NULL,\\n  drawing_id UUID NOT NULL REFERENCES drawings(id),\\n  \\n  version_number INT,\\n  file_path TEXT,\\n  file_name TEXT,\\n  file_size BIGINT,\\n  content_type VARCHAR(100),\\n  width INT,\\n  height INT,\\n  \\n  notes TEXT,\\n  uploaded_by UUID,\\n  created_at TIMESTAMP\\n);\\n```\\n\\n## Types (`types.go`)\\n\\n**Drawing type constants:**\\n\\n- `TypeFloorPlan` \u2014 Floor plan layout\\n- `TypeNetworkDiagram` \u2014 Network topology\\n- `TypeRiserDiagram` \u2014 Vertical riser/distribution\\n- `TypeCablePath` \u2014 Cable routing\\n- `TypeAsBuilt` \u2014 As-built documentation\\n- `TypeSitePlan` \u2014 Site/property layout\\n- `TypeRackElevation` \u2014 Rack elevation\\n- `TypeConduitLayout` \u2014 Conduit routing\\n- `TypeDetail` \u2014 Detail drawing\\n- `TypeMarkup` \u2014 Markup/annotation\\n- `TypeOther` \u2014 Miscellaneous\\n\\n**Status constants:**\\n\\n- `StatusDraft` \u2014 Work in progress\\n- `StatusInReview` \u2014 Pending approval\\n- `StatusApproved` \u2014 Approved\\n- `StatusSuperseded` \u2014 Replaced by newer version\\n- `StatusArchived` \u2014 Archived\\n\\n**Helper methods on Drawing:**\\n\\n- `TypeLabel()` \u2014 Human-friendly type label\\n- `StatusLabel()` \u2014 Human-friendly status label\\n- `StatusColor()` \u2014 CSS badge class for UI\\n- `IsImage()` \u2014 True if latest file is PNG/JPG/GIF/WebP/SVG\\n- `IsPDF()` \u2014 True if latest file is PDF\\n\\n## Integration Points\\n\\n### Authentication\\n\\nAll handlers extract tenant and user IDs from request context:\\n\\n```go\\ntenantID := auth.TenantIDFromContext(r.Context())\\nuserID := auth.UserIDFromContext(r.Context())\\n```\\n\\nTenant ID is used to scope all queries. User ID is recorded on file uploads and drawing creation.\\n\\n### AI Provider\\n\\nVision and takeoff analysis require the AI provider to be configured:\\n\\n```go\\nhandler.SetAI(aiProvider)\\n```\\n\\nThe provider supplies Claude Vision API access. Costs are tracked per action (floor_plan_analysis, drawing_takeoff).\\n\\n### UI Renderer\\n\\nHTML templates are rendered via the UI renderer:\\n\\n```go\\nh.renderer.Render(w, \\\"drawing/list.html\\\", data, isHTMX)\\n```\\n\\nTemplates include:\\n- `drawing/list.html` \u2014 Drawing list with filters\\n- `drawing/form.html` \u2014 Create/edit form\\n- `drawing/detail.html` \u2014 Drawing detail + version history\\n- `drawing/view3d.html` \u2014 3D isometric view\\n\\n### Bid Reconciliation\\n\\nTakeoff data feeds the bid reconciliation engine. When a drawing is linked to a bid, the handler loads the bid's in-scope trades and passes them to the Vision prompt. The reconciliation engine then diffs the extracted quantities against spec requirements.\\n\\n```go\\n// In takeoff.go\\ninScopeTrades := h.loadBidTradesForDrawing(ctx, tenantID, drawingID)\\ndata, cost, err := h.AnalyzeTakeoff(ctx, tenantID, filePath, inScopeTrades...)\\n```\\n\\n## File Storage\\n\\nFiles are stored on disk in a tenant-scoped directory structure:\\n\\n```\\nuploads/drawings/{tenantID}/{drawingID}/v{versionNumber}_{randomHex}.{ext}\\n```\\n\\nExample:\\n```\\nuploads/drawings/550e8400-e29b-41d4-a716-446655440000/\\n  550e8400-e29b-41d4-a716-446655440001/\\n    v1_a1b2c3d4e5f6g7h8.pdf\\n    v2_f7e6d5c4b3a2h1g0.png\\n    v3_9z8y7x6w5v4u3t2s.jpg\\n```\\n\\nFile paths are stored in the database as `/uploads/drawings/...` (with leading slash). When reading, the leading slash is trimmed.\\n\\n## Common Workflows\\n\\n### Create a Drawing with Initial Upload\\n\\n```go\\nPOST /api/drawings\\n  title=Floor Plan\\n  drawing_type=floor_plan\\n  company_id=...\\n  file=\\n  version_notes=Initial upload\\n```\\n\\nHandler:\\n1. Insert drawing row\\n2. Call `saveVersion()` to persist file and create version record\\n3. Redirect to detail page\\n\\n### Analyze Floor Plan for 3D View\\n\\n```go\\nPOST /api/drawings/{id}/analyze\\n```\\n\\nHandler:\\n1. Load drawing\\n2. Check that latest file is an image\\n3. Call `analyzeFloorPlan()` (Claude Vision)\\n4. Store result in `drawings.ai_analysis`\\n5. Redirect to detail page\\n\\nUser then clicks \\\"View 3D\\\" to see the isometric rendering.\\n\\n### Run Takeoff for Bid Reconciliation\\n\\n```go\\nPOST /api/drawings/{id}/takeoff\\n```\\n\\nHandler:\\n1. Load drawing\\n2. Check that latest file is an image\\n3. Load bid trades (if drawing is linked to a bid)\\n4. Call `RunTakeoffAndPersist()` (Claude Vision with trade scoping)\\n5. Store result in `drawings.takeoff_json` and `takeoff_status`\\n6. Redirect to detail page\\n\\nBid reconciliation engine reads `takeoff_json` and diffs against spec requirements.\\n\\n### Upload a New Version\\n\\n```go\\nPOST /api/drawings/{id}/versions\\n  file=\\n  version_notes=Updated floor plan\\n```\\n\\nHandler:\\n1. Query max version number for drawing\\n2. Call `saveVersion()` to persist file with incremented version number\\n3. Redirect to detail page\\n\\n## Error Handling\\n\\n- **Missing drawing**: Return 404\\n- **Missing file**: Return 400 (bad request)\\n- **File too large**: Return 400 (multipart form limit)\\n- **AI provider not configured**: Return 503 (service unavailable)\\n- **AI analysis failure**: Log error, return 500 with error message\\n- **Database error**: Log error, return 500\\n\\nVision and takeoff analysis failures are persisted to the database:\\n- `takeoff_status='failed'`\\n- `takeoff_error=`\\n- `takeoff_ai_cost` is still incremented (partial cost)\\n\\n## Performance Considerations\\n\\n- **List queries**: Denormalize company/user names to avoid N+1 joins\\n- **Version queries**: Fetch latest version metadata in the main drawing query (subqueries)\\n- **AI analysis**: Async or background job recommended for large images (not currently implemented)\\n- **SVG generation**: Pure server-side, no client-side rendering overhead\\n- **File storage**: Consider CDN or S3 for production; current implementation uses local disk\\n\\n## Testing Notes\\n\\n- Mock the AI provider for unit tests (vision/takeoff analysis)\\n- Mock the database pool for handler tests\\n- Test file upload with various formats (PDF, PNG, JPG, DWG)\\n- Test isometric SVG generation with sample FloorPlanData\\n- Test takeoff extraction with bid-scoped trades\\n- Test tenant isolation (queries should filter by tenant_id)\",\"internal-emailsec\":\"# internal \u2014 emailsec\\n\\n# Email Security Module (`internal/emailsec`)\\n\\n## Overview\\n\\nThe `emailsec` module implements a complete email security triage pipeline for NexusOS. It fetches emails from Microsoft 365 mailboxes via the Graph API, analyzes them for phishing and social engineering threats, scores risk against client security profiles, escalates ambiguous cases to an LLM for human-like judgment, and integrates with the helpdesk and workflow systems for automated response.\\n\\nThe module is designed to run as a background service that continuously polls configured tenants for new messages, but also supports manual scans and provides a dashboard for security review.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    A[\\\"Graph APIMicrosoft 365\\\"] --&gt;|FetchFlaggedMessages| B[\\\"PipelineprocessTenant\\\"]\\n    B --&gt;|ParseHeaders| C[\\\"Header Parser\\\"]\\n    B --&gt;|ScoreEmail| D[\\\"Risk Scorer\\\"]\\n    D --&gt;|caution band| E[\\\"LLM TriageAIEmailAnalyzer\\\"]\\n    E --&gt;|AdjustScoreWithLLM| D\\n    D --&gt;|insertEvent| F[\\\"Databaseemail_security_events\\\"]\\n    D --&gt;|executeDecision| G{Decision}\\n    G --&gt;|allow| H[\\\"Pass Through\\\"]\\n    G --&gt;|escalate| I[\\\"Create Ticket+ Workflow\\\"]\\n    G --&gt;|block| J[\\\"Quarantine+ Critical Ticket\\\"]\\n    I --&gt;|EvaluateTicketEvent| K[\\\"Workflow Engine\\\"]\\n    J --&gt;|MoveToQuarantine| A\\n    F --&gt;|handleListEvents| L[\\\"HTTP HandlerDashboard &amp; API\\\"]\\n```\\n\\n## Core Components\\n\\n### 1. Graph Client (`graph.go`)\\n\\nHandles all communication with Microsoft Graph API for email retrieval and message actions.\\n\\n**Key Types:**\\n- `GraphClient` \u2014 Main API client with connection pooling and token management\\n- `GraphMessage` \u2014 Represents a message fetched from Graph API\\n- `msCredentials` \u2014 Holds Microsoft 365 tenant credentials and OAuth tokens\\n\\n**Key Methods:**\\n\\n- **`FetchFlaggedMessages(ctx, tenantID)`** \u2014 Retrieves up to 50 most recent messages from the configured mailbox. Requests full internet headers (`internetMessageHeaders`) for security analysis. Returns `[]GraphMessage`.\\n\\n- **`MoveToQuarantine(ctx, tenantID, messageID)`** \u2014 Moves a message to the Junk Email folder via Graph API. Called when a message is scored as \\\"danger\\\" and decision is \\\"block\\\".\\n\\n- **`GetEnabledTenants(ctx)`** \u2014 Returns list of tenant IDs that have email security enabled and Microsoft 365 configured. Used by the polling loop to determine which tenants to process.\\n\\n**Token Management:**\\n\\nThe client automatically refreshes OAuth tokens using the client_credentials flow. Tokens are cached in the database (`email_accounts` table) and refreshed when they expire (with a 5-minute buffer). This avoids repeated token requests and allows token state to persist across service restarts.\\n\\n### 2. Header Parser (`headers.go`)\\n\\nExtracts security-relevant signals from raw email headers.\\n\\n**Key Type:**\\n- `ParsedHeaders` \u2014 Structured representation of parsed headers with authentication results, infrastructure fingerprints, and derived mismatch checks\\n\\n**Key Function:**\\n\\n- **`ParseHeaders(rawHeaders)`** \u2014 Parses raw email headers into a structured format. Handles:\\n  - **Message identifiers** \u2014 Message-ID, In-Reply-To\\n  - **Sender addresses** \u2014 From (with display name and domain), Return-Path, Reply-To\\n  - **Authentication results** \u2014 SPF, DKIM, DMARC (parsed from `Authentication-Results` header per RFC 8601)\\n  - **Infrastructure** \u2014 Sending IP and hostname extracted from `Received` headers, X-Mailer, User-Agent\\n  - **Microsoft/Inky signals** \u2014 X-Inky-Classification, X-Inky-Score, X-MS-Exchange-Organization-SCL\\n  - **Derived checks** \u2014 Detects From/Reply-To domain mismatch and From/Envelope sender mismatch (spoofing indicators)\\n\\n**Helper Functions:**\\n\\n- `parseHeaderMap()` \u2014 Splits raw headers into a map, handling continuation lines (RFC 5322)\\n- `parseAuthResults()` \u2014 Parses Authentication-Results header per RFC 8601\\n- `extractIPFromReceived()` \u2014 Extracts sending IP and hostname from Received headers using regex\\n- `parseDKIMSignature()` \u2014 Extracts d= (domain) and s= (selector) from DKIM-Signature header\\n- `IsPrivateIP()` \u2014 Checks if an IP is in a private range (10.0.0.0/8, 172.16.0.0/12, etc.)\\n\\n### 3. Risk Scorer (`scoring.go`)\\n\\nEvaluates email risk against a client's security profile and approved sender lists.\\n\\n**Key Types:**\\n- `ClientProfile` \u2014 Security context loaded from the database for a company (approved domains, blocked domains, trusted senders, etc.)\\n- `TrustedSender` \u2014 A known-good email sender baseline with infrastructure fingerprints\\n- `ScoreResult` \u2014 Output of scoring: score (0-100), risk level, factors, and recommended decision\\n\\n**Key Function:**\\n\\n- **`ScoreEmail(headers, profile)`** \u2014 Computes a risk score (0-100) based on:\\n  1. **Blocked domain check** \u2014 Instant block (score 100) if sender domain is explicitly blocked\\n  2. **Authentication failures** \u2014 SPF/DKIM/DMARC results (30, 25, 25 points respectively for failures)\\n  3. **Header mismatches** \u2014 Reply-To domain differs from From domain (+20), Envelope sender differs (+15)\\n  4. **Microsoft Defender SCL** \u2014 High spam confidence level (+20)\\n  5. **Inky classification** \u2014 Malicious/dangerous (+35), caution/warning/suspicious (+20)\\n  6. **Lookalike domain detection** \u2014 Detects typosquatting and homoglyph attacks (+30)\\n  7. **Domain approval** \u2014 Sender domain not in any approved list (+10), or approved (-15)\\n  8. **Trusted sender match** \u2014 Full infrastructure match (-40), partial match (-20)\\n\\n  Returns a `ScoreResult` with:\\n  - **Score** \u2014 0-100 (clamped)\\n  - **Level** \u2014 \\\"safe\\\" (\u226420), \\\"caution\\\" (\u226450), \\\"warning\\\" (\u226475), \\\"danger\\\" (&gt;75)\\n  - **Decision** \u2014 \\\"allow\\\" (safe), \\\"triage\\\" (caution, send to LLM), \\\"escalate\\\" (warning), \\\"block\\\" (danger)\\n  - **Factors** \u2014 Human-readable list of scoring reasons\\n\\n**Lookalike Detection:**\\n\\nThe `isLookalikeDomain()` function detects domain impersonation using:\\n- **Levenshtein distance** \u2014 Edit distance \u22642 on the domain base name (e.g., \\\"gogle.com\\\" vs \\\"google.com\\\")\\n- **TLD swapping** \u2014 Same base name but different TLD (e.g., \\\"google.co\\\" vs \\\"google.com\\\")\\n- **Common substitutions** \u2014 Homoglyph attacks (rn\u2192m, l\u21921, 0\u2192o, vv\u2192w, cl\u2192d, nn\u2192m)\\n\\n**Domain Validation:**\\n\\n- `isDomainApproved()` \u2014 Checks if sender domain is in any approved list (primary email domain, approved sender domains, client vendor domains)\\n- `matchTrustedSender()` \u2014 Finds a matching trusted sender profile by exact email match or domain match\\n- `gatherKnownDomains()` \u2014 Aggregates all known-good domains from the profile\\n\\n### 4. LLM Triage (`llm_triage.go`)\\n\\nProvides structured LLM-based analysis for emails in the \\\"caution\\\" band (score 21-50).\\n\\n**Key Types:**\\n- `AIEmailAnalyzer` \u2014 Interface for LLM-based email analysis\\n- `LLMVerdict` \u2014 Structured response from the LLM (verdict, confidence, reasoning, indicators)\\n- `EmailAnalysisRequest` \u2014 Structured input sent to the LLM\\n\\n**Key Functions:**\\n\\n- **`BuildAnalysisRequest(msg, headers, score)`** \u2014 Creates a structured JSON request from pipeline data, including:\\n  - Header analysis (From, Reply-To, Envelope Sender, SPF/DKIM/DMARC results)\\n  - Current risk score and factors\\n  - Domain verification status\\n  - Body preview (truncated to 500 chars)\\n  - External flags (Inky, Defender SCL)\\n\\n- **`ParseLLMResponse(response)`** \u2014 Parses the LLM's JSON response into a `LLMVerdict`. Handles markdown code blocks and validates the verdict value.\\n\\n- **`AdjustScoreWithLLM(score, verdict)`** \u2014 Modifies the score based on LLM verdict:\\n  - **\\\"safe\\\" (high confidence \u22650.8)** \u2014 Reduce score by 20\\n  - **\\\"safe\\\" (moderate confidence)** \u2014 Reduce score by 10\\n  - **\\\"suspicious\\\"** \u2014 Increase score by 15\\n  - **\\\"malicious\\\"** \u2014 Increase score by 30\\n  \\n  Then re-evaluates the decision based on the adjusted score.\\n\\n**System Prompt:**\\n\\nThe LLM is instructed to analyze emails for phishing, social engineering, and credential harvesting. It's explicitly told that AI-generated phishing is common, perfect grammar doesn't mean legitimate, and to focus on intent and technical signals over writing quality.\\n\\n### 5. Pipeline (`pipeline.go`)\\n\\nThe main orchestrator that ties together header parsing, risk scoring, LLM triage, event logging, and workflow integration.\\n\\n**Key Type:**\\n- `Pipeline` \u2014 Main orchestrator with dependencies on Graph client, LLM analyzer, ticket creator, and workflow engine\\n\\n**Key Methods:**\\n\\n- **`StartPolling(ctx, pollInterval)`** \u2014 Begins the background polling loop. Runs immediately on start, then at regular intervals. Calls `pollAllTenants()` on each tick.\\n\\n- **`processTenant(ctx, tenantID)`** \u2014 Fetches all flagged messages for a tenant and processes each one. Skips messages already processed (by `internet_message_id`).\\n\\n- **`ProcessMessage(ctx, tenantID, msg)`** \u2014 The core pipeline for a single message:\\n  1. Parse headers\\n  2. Resolve company from recipient domain\\n  3. Load client profile\\n  4. Score email\\n  5. LLM triage (if caution band and LLM available)\\n  6. Insert security event to database\\n  7. Execute decision (allow, triage, escalate, block)\\n\\n- **`executeDecision(ctx, tenantID, companyID, eventID, msg, headers, score, verdict)`** \u2014 Acts on the scoring decision:\\n  - **\\\"allow\\\"** \u2014 No action\\n  - **\\\"triage\\\"** \u2014 Log only (LLM already reviewed)\\n  - **\\\"escalate\\\"** \u2014 Create helpdesk ticket with \\\"high\\\" priority\\n  - **\\\"block\\\"** \u2014 Quarantine message via Graph API + create \\\"critical\\\" priority ticket\\n\\n- **`createSecurityTicket()`** \u2014 Opens a helpdesk ticket with:\\n  - Title: \\\"[Email Security] {level} email from {sender}\\\"\\n  - Description: Risk factors, authentication results, lookalike warnings\\n  - Priority: \\\"high\\\" or \\\"critical\\\"\\n  - Source: \\\"email_security\\\"\\n  - Fires workflow event so automation rules can act\\n\\n**Company Resolution:**\\n\\nThe `resolveCompany()` method matches the email sender to a company by checking:\\n1. Company's `primary_email_domain`\\n2. Client vendor approved domains\\n3. Trusted sender profiles\\n\\nThis allows the pipeline to apply company-specific security policies.\\n\\n### 6. HTTP Handler (`handler.go`)\\n\\nProvides the dashboard UI and REST API for email security events.\\n\\n**Key Type:**\\n- `Handler` \u2014 HTTP handler with routes for dashboard, event listing, event details, and manual scans\\n\\n**Routes:**\\n\\n- **`GET /security/email`** \u2014 Dashboard page (HTML)\\n- **`GET /api/security/email/events`** \u2014 List events with filtering (JSON)\\n  - Query params: `limit`, `offset`, `level` (risk level), `decision`, `company_id`\\n- **`GET /api/security/email/events/{id}`** \u2014 Get single event details (JSON)\\n- **`PATCH /api/security/email/events/{id}`** \u2014 Update event (mark reviewed, set action taken)\\n- **`POST /api/security/email/scan`** \u2014 Trigger manual scan for tenant\\n- **`GET /api/security/email/stats`** \u2014 Get aggregate stats (JSON)\\n\\n**Dashboard Stats:**\\n\\nThe `emailStats` struct aggregates:\\n- Total events, last 24h, last 7 days\\n- Counts by decision (blocked, escalated, triaged, allowed)\\n- Lookalikes detected\\n- Average risk score\\n- High-risk count (score \u226576)\\n\\n## Data Flow\\n\\n```\\n1. Background Polling (StartPolling)\\n   \u2193\\n2. Fetch Enabled Tenants (GetEnabledTenants)\\n   \u2193\\n3. For Each Tenant: Fetch Messages (FetchFlaggedMessages)\\n   \u2193\\n4. For Each Message: ProcessMessage\\n   \u251c\u2500 Parse Headers (ParseHeaders)\\n   \u251c\u2500 Resolve Company (resolveCompany)\\n   \u251c\u2500 Load Profile (LoadClientProfile)\\n   \u251c\u2500 Score Email (ScoreEmail)\\n   \u251c\u2500 LLM Triage (if caution band)\\n   \u2502  \u2514\u2500 Adjust Score (AdjustScoreWithLLM)\\n   \u251c\u2500 Insert Event (insertEvent)\\n   \u2514\u2500 Execute Decision (executeDecision)\\n      \u251c\u2500 Allow: No action\\n      \u251c\u2500 Triage: Log only\\n      \u251c\u2500 Escalate: Create ticket + workflow event\\n      \u2514\u2500 Block: Quarantine + critical ticket\\n```\\n\\n## Database Schema (Key Tables)\\n\\n- **`email_accounts`** \u2014 Microsoft 365 credentials and OAuth tokens per tenant\\n- **`email_security_events`** \u2014 Logged security analysis results\\n- **`companies`** \u2014 Client companies with security policies (approved domains, blocked domains, etc.)\\n- **`client_vendors`** \u2014 Known vendors with approved sending domains\\n- **`trusted_sender_profiles`** \u2014 Known-good senders with infrastructure fingerprints\\n- **`tickets`** \u2014 Helpdesk tickets created by escalation/block decisions\\n\\n## Integration Points\\n\\n### Microsoft Graph API\\n- Fetches messages from configured mailbox\\n- Moves messages to quarantine folder\\n- Requires OAuth token refresh (client_credentials flow)\\n\\n### LLM Analyzer\\n- Optional dependency for caution-band triage\\n- Receives structured email analysis request\\n- Returns verdict with confidence and reasoning\\n\\n### Helpdesk / Ticket System\\n- Creates security tickets for escalate/block decisions\\n- Implements `TicketCreator` interface\\n\\n### Workflow Engine\\n- Fires events for automation rules\\n- Implements `WorkflowTrigger` interface\\n- Allows MSPs to define custom response workflows\\n\\n### Authentication &amp; Authorization\\n- Uses `auth.TenantIDFromContext()` to isolate data by tenant\\n- Uses `auth.UserIDFromContext()` for audit trail (reviewed_by)\\n\\n## Configuration &amp; Setup\\n\\nThe module requires:\\n\\n1. **Microsoft 365 credentials** in `email_accounts` table:\\n   - `ms_tenant_id`, `ms_client_id`, `ms_client_secret`\\n   - `email_address` (mailbox to monitor)\\n   - `provider = 'microsoft365'`, `is_active = true`\\n\\n2. **Company security policies** in `companies` table:\\n   - `email_security_enabled = true`\\n   - `primary_email_domain`, `approved_sender_domains`, `blocked_sender_domains`\\n\\n3. **Optional: Client vendors** in `client_vendors` table:\\n   - `vendor_domain`, `approved_domains` (array)\\n   - `relationship = 'active'`\\n\\n4. **Optional: Trusted senders** in `trusted_sender_profiles` table:\\n   - `email_address`, `domain`, `spf_domain`, `dkim_domain`, `sending_hostname`, `x_mailer`\\n   - `is_active = true`\\n\\n## Usage Example\\n\\n```go\\n// In main.go\\npool := pgxpool.New(ctx, dbURL)\\ngraph := emailsec.NewGraphClient(pool)\\npipeline := emailsec.NewPipeline(pool, graph)\\n\\n// Attach optional dependencies\\npipeline.SetAI(llmAnalyzer)\\npipeline.SetTickets(&amp;emailsec.HelpdeskTicketAdapter{Pool: pool})\\npipeline.SetWorkflow(workflowEngine)\\n\\n// Start background polling\\ngo pipeline.StartPolling(ctx, 5*time.Minute)\\n\\n// Register HTTP routes\\nhandler := emailsec.NewHandler(pool, renderer, pipeline)\\nhandler.RegisterRoutes(mux)\\n```\\n\\n## Error Handling &amp; Resilience\\n\\n- **Token refresh failures** \u2014 Logged but don't stop the pipeline\\n- **LLM triage failures** \u2014 Falls back to initial score, adds error to factors\\n- **Graph API errors** \u2014 Logged per tenant; other tenants continue processing\\n- **Database errors** \u2014 Logged; pipeline continues with next message\\n- **Duplicate messages** \u2014 Skipped by checking `internet_message_id`\\n\\nThe pipeline is designed to be fault-tolerant: a single message failure doesn't stop processing of other messages or tenants.\\n\\n## Performance Considerations\\n\\n- **Polling interval** \u2014 Configurable (default 5 minutes). Adjust based on message volume and latency tolerance.\\n- **Message batch size** \u2014 Fixed at 50 per Graph API call (configurable in `FetchFlaggedMessages`).\\n- **Token caching** \u2014 Reduces OAuth requests; tokens stored in database for persistence.\\n- **Lookalike detection** \u2014 Uses Levenshtein distance (O(n\u00b2) space, O(n\u00b2) time per domain pair). Acceptable for typical domain counts.\\n- **LLM triage** \u2014 Only for caution-band emails (21-50 score), reducing API calls.\\n\\n## Testing Considerations\\n\\n- Mock `AIEmailAnalyzer` for LLM-free testing\\n- Mock `TicketCreator` and `WorkflowTrigger` for isolated pipeline testing\\n- Use test fixtures for `ParsedHeaders` and `ScoreResult`\\n- Test lookalike detection with known homoglyph pairs\\n- Test header parsing with real-world email samples (RFC 5322 edge cases)\",\"internal-employee\":\"# internal \u2014 employee\\n\\n# Employee Module\\n\\nThe employee module manages HR data for NexusOS PSA: employee profiles, skills and certifications, compensation rates, weekly availability, and time-off requests. Other modules (dispatch, timesheets, job costing) depend on this data for scheduling, billing, and resource planning.\\n\\n## Overview\\n\\nThe module is organized around a single HTTP handler that serves both HTML pages and JSON APIs. All data is tenant-isolated and tied to user accounts via the `users` table.\\n\\n**Core responsibilities:**\\n- Employee profile CRUD (job title, department, employment type, hire/termination dates)\\n- Skills and certifications with proficiency levels and expiration tracking\\n- Pay rates (hourly, salary, per diem, flat) with effective/end dates\\n- Role rates (billable and cost rates for role+trade combinations)\\n- Weekly availability templates (days, hours, max slots)\\n- Time-off requests with approval workflow\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    Handler[\\\"Handler(pgxpool, renderer)\\\"]\\n    Pages[\\\"Pages(GET /employees)\\\"]\\n    ProfileAPI[\\\"Profile API(POST/PUT)\\\"]\\n    SkillsAPI[\\\"Skills API(POST/DELETE)\\\"]\\n    RatesAPI[\\\"Pay/Role Rates(POST/PUT/DELETE)\\\"]\\n    AvailAPI[\\\"Availability(POST)\\\"]\\n    TimeOffAPI[\\\"Time Off(POST/DELETE/approve)\\\"]\\n    \\n    Handler --&gt; Pages\\n    Handler --&gt; ProfileAPI\\n    Handler --&gt; SkillsAPI\\n    Handler --&gt; RatesAPI\\n    Handler --&gt; AvailAPI\\n    Handler --&gt; TimeOffAPI\\n```\\n\\n## Handler &amp; Routes\\n\\n**`Handler`** wraps a database connection pool and UI renderer. It's instantiated once at startup and registered with the HTTP mux.\\n\\n```go\\ntype Handler struct {\\n    pool     *pgxpool.Pool\\n    renderer *ui.Renderer\\n}\\n\\nfunc NewHandler(pool *pgxpool.Pool, renderer *ui.Renderer) *Handler\\n```\\n\\n**`RegisterRoutes(mux)`** adds all employee endpoints:\\n\\n| Method | Path | Handler | Purpose |\\n|--------|------|---------|---------|\\n| GET | `/employees` | `handleList` | Employee directory with filters |\\n| GET | `/employees/{id}` | `handleDetail` | Full employee profile + related data |\\n| POST | `/api/employees` | `handleCreateProfile` | Create new employee profile |\\n| PUT | `/api/employees/{id}` | `handleUpdateProfile` | Update profile fields |\\n| POST | `/api/employees/{id}/skills` | `handleAddSkill` | Add/upsert skill |\\n| DELETE | `/api/employees/skills/{skillId}` | `handleDeleteSkill` | Remove skill |\\n| POST | `/api/employees/{id}/pay-rates` | `handleAddPayRate` | Add pay rate record |\\n| PUT | `/api/employees/pay-rates/{rateId}` | `handleUpdatePayRate` | Update pay rate |\\n| DELETE | `/api/employees/pay-rates/{rateId}` | `handleDeletePayRate` | Remove pay rate |\\n| POST | `/api/employees/{id}/role-rates` | `handleAddRoleRate` | Add role rate |\\n| PUT | `/api/employees/role-rates/{rateId}` | `handleUpdateRoleRate` | Update role rate |\\n| DELETE | `/api/employees/role-rates/{rateId}` | `handleDeleteRoleRate` | Remove role rate |\\n| POST | `/api/employees/{id}/availability` | `handleSaveAvailability` | Save weekly schedule |\\n| POST | `/api/employees/{id}/time-off` | `handleAddTimeOff` | Request time off |\\n| POST | `/api/employees/time-off/{toId}/approve` | `handleApproveTimeOff` | Approve time-off request |\\n| DELETE | `/api/employees/time-off/{toId}` | `handleDeleteTimeOff` | Cancel time-off request |\\n\\nAll handlers extract `tenantID` from the request context (via `auth.TenantIDFromContext`) to enforce tenant isolation.\\n\\n## Page Handlers\\n\\n### `handleList` \u2014 Employee Directory\\n\\nRenders `/employees` with a filterable list of all employee profiles. Supports query filters:\\n- `?status=active` \u2014 filter by employment status\\n- `?department=IT%20Services` \u2014 filter by department\\n\\n**Query logic:**\\n1. Joins `employee_profiles` with `users` to get full names and email\\n2. Counts skills per employee (subquery)\\n3. Fetches current pay rate (most recent active rate)\\n4. Optionally filters by status and/or department\\n5. Orders by full name\\n\\n**Template data includes:**\\n- `Employees` \u2014 list of profiles with aggregates\\n- `UnlinkedUsers` \u2014 active users without employee profiles (for \\\"Add Employee\\\" dropdown)\\n- `StatusFilter`, `DeptFilter` \u2014 current filter values\\n- `Statuses`, `Departments`, `EmploymentTypes` \u2014 dropdown options\\n- User data injected by `auth.InjectUserData()`\\n\\nHTMX requests (detected via `HX-Request` header) render partial HTML; full page requests render the complete layout.\\n\\n### `handleDetail` \u2014 Employee Profile\\n\\nRenders `/employees/{id}` with comprehensive employee data:\\n\\n**Fetches:**\\n1. **Profile** \u2014 basic employee info (job title, department, hire date, emergency contact, etc.)\\n2. **Skills** \u2014 all skills/certifications with proficiency and expiration\\n3. **Pay Rates** \u2014 historical and current compensation records\\n4. **Role Rates** \u2014 billable/cost rates for role+trade combinations (joined with `billing_roles` for names and default rates)\\n5. **Availability** \u2014 weekly schedule template (7 days, padded with defaults if missing)\\n6. **Time Off** \u2014 PTO requests with approval status and approver name\\n7. **Billing Roles** \u2014 dropdown options for adding new role rates\\n\\n**Template data includes:**\\n- All fetched entities\\n- Lookup tables: `Statuses`, `Departments`, `EmploymentTypes`, `SkillTypes`, `Proficiencies`, `RateTypes`, `Trades`, `ITCategories`, `Roles`\\n- User data injected by `auth.InjectUserData()`\\n\\n## Profile CRUD\\n\\n### `handleCreateProfile` \u2014 POST `/api/employees`\\n\\nCreates a new employee profile linked to an existing user.\\n\\n**Form fields:**\\n- `user_id` (required) \u2014 UUID of the user to link\\n- `employee_number`, `job_title`, `department`, `employment_type`, `hire_date`, `status`, `phone`, `home_city`, `home_state`, `home_zip`, `service_region`, `notes`\\n\\n**Returns:** HTTP 201 with `HX-Redirect` header pointing to the new profile detail page.\\n\\n### `handleUpdateProfile` \u2014 PUT `/api/employees/{id}`\\n\\nUpdates an existing employee profile.\\n\\n**Form fields:** Same as create, plus `termination_date` and emergency contact fields.\\n\\n**Returns:** Toast notification and `HX-Redirect` to the profile detail page.\\n\\n## Skills Management\\n\\n### `handleAddSkill` \u2014 POST `/api/employees/{id}/skills`\\n\\nAdds or updates a skill for an employee.\\n\\n**Form fields:**\\n- `skill_type` \u2014 `\\\"trade\\\"`, `\\\"it_category\\\"`, or `\\\"certification\\\"`\\n- `skill_value` \u2014 specific trade/category/cert name\\n- `proficiency` \u2014 `\\\"junior\\\"`, `\\\"mid\\\"`, `\\\"senior\\\"`, `\\\"expert\\\"`\\n- `certified` \u2014 checkbox (boolean)\\n- `cert_expires` \u2014 optional expiration date\\n\\n**Behavior:** Uses `ON CONFLICT` to upsert on `(user_id, skill_type, skill_value)`. Updates proficiency and expiration if the skill already exists.\\n\\n### `handleDeleteSkill` \u2014 DELETE `/api/employees/skills/{skillId}`\\n\\nRemoves a skill record. Triggers `skill-deleted` HTMX event for UI refresh.\\n\\n## Pay Rates\\n\\n### `handleAddPayRate` \u2014 POST `/api/employees/{id}/pay-rates`\\n\\nAdds a compensation record.\\n\\n**Form fields:**\\n- `rate_type` \u2014 `\\\"hourly\\\"`, `\\\"salary\\\"`, `\\\"per_diem\\\"`, `\\\"flat\\\"`\\n- `base_rate`, `overtime_rate` \u2014 numeric values (parsed as float64)\\n- `effective_date`, `end_date` \u2014 date range\\n- `trade`, `role`, `notes` \u2014 optional context\\n\\n**Behavior:** Stores both base and overtime rates. `end_date` is NULL for current rates.\\n\\n### `handleUpdatePayRate` \u2014 PUT `/api/employees/pay-rates/{rateId}`\\n\\nUpdates an existing pay rate record. Triggers `rate-updated` HTMX event.\\n\\n### `handleDeletePayRate` \u2014 DELETE `/api/employees/pay-rates/{rateId}`\\n\\nRemoves a pay rate. Triggers `rate-deleted` HTMX event.\\n\\n## Role Rates\\n\\nRole rates link employees to billable roles with cost tracking. They bridge employee compensation and job billing.\\n\\n### `handleAddRoleRate` \u2014 POST `/api/employees/{id}/role-rates`\\n\\nAdds a role rate for an employee.\\n\\n**Form fields:**\\n- `billing_role_id` \u2014 UUID of a billing role (optional; if provided, fetches default hourly rate and role name)\\n- `trade` \u2014 specific trade (optional)\\n- `cost_rate` \u2014 employee-specific cost (numeric)\\n- `effective_date`, `end_date` \u2014 date range\\n- `notes` \u2014 optional\\n\\n**Behavior:**\\n1. Looks up the selected `billing_role` to get its `default_hourly_rate` (bill rate) and name\\n2. Inserts record with denormalized `role` name and `bill_rate`\\n3. Stores employee-specific `cost_rate` separately\\n\\n### `handleUpdateRoleRate` \u2014 PUT `/api/employees/role-rates/{rateId}`\\n\\nUpdates role rate. Re-fetches billing role data if `billing_role_id` changes. Triggers `rolerate-updated` HTMX event.\\n\\n### `handleDeleteRoleRate` \u2014 DELETE `/api/employees/role-rates/{rateId}`\\n\\nRemoves role rate. Triggers `rolerate-deleted` HTMX event.\\n\\n## Availability\\n\\n### `handleSaveAvailability` \u2014 POST `/api/employees/{id}/availability`\\n\\nSaves a weekly availability template (7 days).\\n\\n**Form fields:** For each day of week (0\u20136):\\n- `day_{dow}_available` \u2014 checkbox\\n- `day_{dow}_start` \u2014 start time (HH:MM, defaults to \\\"08:00\\\")\\n- `day_{dow}_end` \u2014 end time (HH:MM, defaults to \\\"17:00\\\")\\n- `day_{dow}_max_slots` \u2014 max concurrent assignments (defaults to 4)\\n\\n**Behavior:** Loops through all 7 days and upserts each via `ON CONFLICT (user_id, day_of_week)`.\\n\\n## Time Off\\n\\n### `handleAddTimeOff` \u2014 POST `/api/employees/{id}/time-off`\\n\\nCreates a time-off request.\\n\\n**Form fields:**\\n- `start_date`, `end_date` \u2014 date range\\n- `reason` \u2014 text description\\n\\n**Behavior:** Inserts with `approved = false` and `approved_by = NULL`. Requires manual approval.\\n\\n### `handleApproveTimeOff` \u2014 POST `/api/employees/time-off/{toId}/approve`\\n\\nApproves a pending time-off request.\\n\\n**Behavior:** Sets `approved = true` and `approved_by` to the current user's ID. Triggers `timeoff-updated` HTMX event.\\n\\n### `handleDeleteTimeOff` \u2014 DELETE `/api/employees/time-off/{toId}`\\n\\nCancels a time-off request (approved or pending). Triggers `timeoff-updated` HTMX event.\\n\\n## Data Types\\n\\n### `Employee`\\n\\nExtended user profile with HR metadata.\\n\\n**Key fields:**\\n- `ID`, `UserID`, `TenantID` \u2014 identifiers\\n- `EmployeeNumber`, `JobTitle`, `Department`, `EmploymentType`, `Status` \u2014 role info\\n- `HireDate`, `TerminationDate` \u2014 employment timeline\\n- `Phone`, `EmergencyContactName`, `EmergencyContactPhone` \u2014 contact info\\n- `HomeCity`, `HomeState`, `HomeZip`, `ServiceRegion` \u2014 location\\n- `FullName`, `Email`, `IsActive` \u2014 denormalized from users table\\n- `SkillCount`, `CurrentRate` \u2014 aggregates\\n\\n**Methods:**\\n- `StatusLabel()`, `StatusColor()` \u2014 lookup display values\\n- `EmploymentTypeLabel()` \u2014 lookup display value\\n- `Initials()` \u2014 extract first and last initials for avatars\\n- `CurrentRateFormatted()` \u2014 format current pay rate as \\\"$X.XX/hr\\\"\\n\\n### `Skill`\\n\\nEmployee skill or certification.\\n\\n**Key fields:**\\n- `SkillType` \u2014 `\\\"trade\\\"`, `\\\"it_category\\\"`, or `\\\"certification\\\"`\\n- `SkillValue` \u2014 specific trade/category/cert name\\n- `Proficiency` \u2014 `\\\"junior\\\"`, `\\\"mid\\\"`, `\\\"senior\\\"`, `\\\"expert\\\"`\\n- `Certified` \u2014 boolean\\n- `CertExpires` \u2014 optional expiration date\\n\\n**Methods:**\\n- `SkillTypeLabel()`, `SkillValueLabel()` \u2014 lookup display values\\n- `ProficiencyLabel()`, `ProficiencyColor()` \u2014 lookup and color-code proficiency\\n- `SkillTypeColor()` \u2014 color-code skill type (trade=amber, IT=blue, cert=purple)\\n\\n### `PayRate`\\n\\nCompensation record with effective/end dates.\\n\\n**Key fields:**\\n- `RateType` \u2014 `\\\"hourly\\\"`, `\\\"salary\\\"`, `\\\"per_diem\\\"`, `\\\"flat\\\"`\\n- `BaseRate`, `OvertimeRate` \u2014 numeric values\\n- `EffectiveDate`, `EndDate` \u2014 date range (NULL end = current)\\n- `Trade`, `Role`, `Notes` \u2014 optional context\\n\\n**Methods:**\\n- `RateTypeLabel()`, `RateTypeColor()` \u2014 lookup and color-code\\n- `BaseRateFormatted()`, `OvertimeRateFormatted()` \u2014 format as currency\\n- `TradeLabel()`, `RoleLabel()` \u2014 lookup display values\\n- `IsCurrent()` \u2014 true if `EndDate` is empty\\n\\n### `RoleRate`\\n\\nBillable and cost rate for a role+trade combination.\\n\\n**Key fields:**\\n- `BillingRoleID` \u2014 UUID of linked billing role (optional)\\n- `Role` \u2014 denormalized billing role name\\n- `Trade` \u2014 specific trade (optional; empty = any trade)\\n- `BillRate` \u2014 hourly rate to bill customers (from `billing_roles.default_hourly_rate`)\\n- `CostRate` \u2014 employee-specific cost\\n- `EffectiveDate`, `EndDate` \u2014 date range\\n\\n**Methods:**\\n- `RoleLabel()`, `TradeLabel()` \u2014 lookup display values\\n- `BillRateFormatted()`, `CostRateFormatted()` \u2014 format as currency\\n- `IsCurrent()` \u2014 true if `EndDate` is empty\\n- `Margin()` \u2014 calculate profit margin as `(BillRate - CostRate) / BillRate * 100`\\n- `MarginFormatted()`, `MarginColor()` \u2014 format and color-code margin (green \u226540%, amber \u226520%, red &lt;20%)\\n\\n### `Availability`\\n\\nWeekly schedule template.\\n\\n**Key fields:**\\n- `DayOfWeek` \u2014 0\u20136 (Sunday\u2013Saturday)\\n- `Available` \u2014 boolean\\n- `StartTime`, `EndTime` \u2014 HH:MM format\\n- `MaxSlots` \u2014 max concurrent assignments\\n\\n**Methods:**\\n- `DayLabel()` \u2014 return day name (e.g., \\\"Monday\\\")\\n\\n### `TimeOff`\\n\\nPTO or time-off request.\\n\\n**Key fields:**\\n- `StartDate`, `EndDate` \u2014 date range\\n- `Reason` \u2014 text description\\n- `Approved` \u2014 boolean\\n- `ApprovedBy`, `ApprovedByName` \u2014 approver info\\n\\n## Lookup Tables\\n\\nThe module provides enumeration functions for dropdowns and validation:\\n\\n- **`AllStatuses()`** \u2014 `active`, `on_leave`, `suspended`, `terminated` (with colors)\\n- **`AllEmploymentTypes()`** \u2014 `full_time`, `part_time`, `contractor`, `intern`, `temp`\\n- **`AllDepartments()`** \u2014 Field Operations, IT Services, Project Management, Sales, Administration, Engineering, Support\\n- **`AllSkillTypes()`** \u2014 `trade`, `it_category`, `certification`\\n- **`AllProficiencies()`** \u2014 `junior`, `mid`, `senior`, `expert`\\n- **`AllRateTypes()`** \u2014 `hourly`, `salary`, `per_diem`, `flat`\\n- **`AllTrades()`** \u2014 General, Structured Cabling, Access Control, Fire Alarm, CCTV, Network, AV, Electrical, HVAC, Low Voltage\\n- **`AllITCategories()`** \u2014 Desktop Support, Networking, Server Admin, Cloud/Azure, Security, VoIP, Email/M365, Backup &amp; DR, Printing, Onboarding\\n- **`AllRoles()`** \u2014 Lead Tech, Installer, Apprentice, Helper, Inspector, PM, Engineer, Dispatcher, Admin\\n\\n## Integration Points\\n\\n### Auth Module\\n\\nAll handlers extract tenant and user IDs from the request context:\\n- `auth.TenantIDFromContext(r.Context())` \u2014 tenant isolation\\n- `auth.UserIDFromContext(r.Context())` \u2014 current user (for time-off approvals)\\n- `auth.InjectUserData(r.Context(), data)` \u2014 inject user info into template data\\n\\n### UI Module\\n\\n- `renderer.Render(w, template, data, isHTMX)` \u2014 render HTML pages or HTMX partials\\n- Detects HTMX requests via `HX-Request` header\\n\\n### Other Modules\\n\\n**Dispatch** reads employee availability and skills to assign work orders.\\n\\n**Timesheets** reads employee profiles and pay rates to calculate labor costs.\\n\\n**Job Costing** reads role rates to determine billable and cost rates for projects.\\n\\n**Billing** reads role rates and time entries to generate invoices.\\n\\n## HTMX Integration\\n\\nThe module uses HTMX for dynamic updates without full page reloads:\\n\\n- **`HX-Request` header** \u2014 detected to render partials instead of full pages\\n- **`HX-Redirect`** \u2014 redirect to detail page after create/update\\n- **`HX-Trigger`** \u2014 signal UI to refresh related sections (e.g., `skill-deleted`, `rate-updated`, `timeoff-updated`)\\n- **Toast notifications** \u2014 inline success/error messages\\n\\n## Database Schema (Reference)\\n\\nThe module reads/writes these tables:\\n\\n- `employee_profiles` \u2014 core profile data\\n- `employee_skills` \u2014 skills with proficiency and expiration\\n- `employee_pay_rates` \u2014 compensation records\\n- `employee_role_rates` \u2014 billable/cost rates\\n- `employee_availability` \u2014 weekly schedule template\\n- `employee_time_off` \u2014 PTO requests\\n- `users` \u2014 linked user accounts\\n- `billing_roles` \u2014 referenced for role rate defaults\\n\\nAll tables include `tenant_id` for multi-tenancy and `created_at`/`updated_at` timestamps.\\n\\n## Error Handling\\n\\nHandlers log errors to stdout and return HTTP error responses:\\n- **400 Bad Request** \u2014 missing required fields\\n- **404 Not Found** \u2014 employee/skill/rate not found\\n- **500 Internal Server Error** \u2014 database errors\\n\\nHTMX requests receive error responses; full page requests redirect to error pages.\",\"internal-epa\":\"# internal \u2014 epa\\n\\n# EPA Module Documentation\\n\\n## Overview\\n\\nThe **EPA** (Employee Productivity Analyzer) module ingests 15-minute telemetry windows from the Nexie RMM agent, classifies application usage into productivity categories, computes focus scores, and surfaces aggregate analytics to authorized users through role-gated dashboards.\\n\\nEPA is designed for MSPs managing multiple client accounts. It operates at two scopes:\\n- **Per-client**: A tab on the client detail page showing roster-level stats, per-contact drilldowns, and AI-generated insights\\n- **Per-contact**: Individual productivity detail panels with app breakdowns, activity heatmaps, and AI coaching briefs\\n\\nThe module enforces three sequential gates on telemetry ingest:\\n1. **Tenant consent** \u2014 admin must acknowledge the monitoring policy\\n2. **Agent assignment** \u2014 the device must be bound to a contact\\n3. **Contact opt-in** \u2014 the individual contact must enable EPA monitoring\\n\\n## Architecture\\n\\n```\\nAgent (RMM)\\n    \u2193 (RSA-signed POST)\\nhandleIngest\\n    \u2193\\nverifyAgentSig (cert fingerprint + signature)\\n    \u2193\\nIngestSnapshot (consent \u2192 assignment \u2192 opt-in gates)\\n    \u2193\\nStore (epa_snapshots, epa_employees, epa_alerts)\\n    \u2193\\nevaluateAlerts (background goroutine)\\n    \u2193\\nDashboard / Drilldown / AI handlers\\n```\\n\\n## Key Components\\n\\n### Handler (`handler.go`)\\n\\nThe HTTP handler layer. All EPA endpoints are mounted here and delegate to the store for data access.\\n\\n**Role-gated endpoints** (JWT + role check):\\n- `GET /api/epa/employees/{id}` \u2014 employee detail drill-down\\n- `POST /api/epa/insights` \u2014 org-wide AI insights\\n- `POST /api/epa/employees/{id}/coach` \u2014 AI coaching brief\\n- `GET /api/epa/report` \u2014 JSON export\\n- `GET /api/companies/{id}/epa-summary` \u2014 per-client EPA tab\\n- `POST /api/companies/{id}/epa-insights` \u2014 per-client AI insights\\n- `GET /api/companies/{id}/epa-contact/{cid}/detail` \u2014 contact drilldown\\n- `POST /api/companies/{id}/epa-contact/{cid}/nexie` \u2014 contact AI analysis\\n\\n**Admin-only**:\\n- `POST /api/epa/consent` \u2014 acknowledge monitoring policy\\n\\n**Signature-authenticated** (no JWT):\\n- `POST /api/epa/ingest` \u2014 agent telemetry ingest (must be allow-listed in `main.go`'s `enforceAuth()`)\\n\\n#### Role Gates\\n\\n`requireEPARole` checks if the user has Admin, Manager, or C-Level role. HR access is opt-in per tenant via `epa_settings.allowed_roles`. HTMX requests that fail the gate receive an `HX-Redirect` header instead of a 403 to avoid breaking the page flow.\\n\\n`requireAdmin` enforces admin-only access for consent acknowledgement.\\n\\n#### Ingest Authentication\\n\\n`handleIngest` mirrors the device API signature scheme:\\n1. Extract `X-Nexus-CertFingerprint` and `X-Nexus-Signature` headers\\n2. Call `verifyAgentSig` to look up the agent's stored PEM cert, verify the RSA-SHA256 signature over the request body, and check expiry\\n3. Resolve tenant ID and agent UUID from the agents table (never trust a header)\\n4. Pass to `IngestSnapshot` for gating and persistence\\n\\nThis design ensures agents cannot spoof tenant or company identity.\\n\\n#### AI Integration\\n\\nThree endpoints call Claude for AI-generated insights:\\n- `handleGenerateInsights` \u2014 org-wide productivity summary (3 insights)\\n- `handleGenerateCoaching` \u2014 per-employee coaching brief (4 talking points)\\n- `handleGenerateCompanyInsights` \u2014 per-client summary (3 insights)\\n- `handleContactNexieAnalysis` \u2014 per-contact weekly observation (plain text)\\n\\nAll use `ai.CallClaudeRawTracked` with usage metadata for billing. Responses are parsed from JSON (or plain text for contact analysis), stripped of code fences, and rendered as HTMX partials.\\n\\n#### Visualization Helpers\\n\\n**`buildHeatmap`** \u2014 collapses 7 days of snapshots into a 5\u00d77 grid (5 days, 7 3-hour slots). Each cell's intensity (0\u20134) is driven by the max focus score in that slot.\\n\\n**`buildAppBreakdown`** \u2014 aggregates app usage across snapshots, returns top 5 by seconds, and classifies each app.\\n\\n### Store (`store.go`)\\n\\nAll database access is encapsulated here. The store owns the connection pool and exposes query methods to handlers.\\n\\n#### Ingest Pipeline\\n\\n`IngestSnapshot` is the core ingest method. It:\\n\\n1. **Checks consent** \u2014 queries `epa_settings.consent_acknowledged_at`; returns `ErrConsentRequired` if null\\n2. **Resolves agent context** \u2014 looks up the agent's assigned contact and company; returns `ErrAgentNotAssigned` if no contact is assigned\\n3. **Checks contact opt-in** \u2014 queries `contacts.nexie_epa_enabled`; returns `ErrEPADisabledForContact` if false\\n4. **Classifies apps** \u2014 calls `ClassifyApp` on each app in the request\\n5. **Computes focus score** \u2014 calls `ComputeFocusScore` (productive + 0.5\u00d7meeting) / active\\n6. **Persists snapshot** \u2014 inserts into `epa_snapshots` (idempotent on tenant + agent + window_start)\\n7. **Upserts employee** \u2014 inserts or updates `epa_employees` with display name, role, and contact mapping\\n8. **Spawns alert evaluation** \u2014 calls `evaluateAlerts` in a background goroutine on a fresh context\\n\\nThe per-company kill switch (`companies.nexie_epa_enabled`) was retired in April 2026 because EPA is now billed per opted-in contact. The column remains in the schema to avoid destructive ALTER on production but is no longer read.\\n\\n#### Alert Evaluation\\n\\n`evaluateAlerts` runs fire-and-forget after each snapshot. It:\\n\\n1. Queries the 3-day rolling average focus score\\n2. Compares against tenant thresholds (default: amber=60, red=40)\\n3. Updates `epa_employees.alert_level` and `alert_since`\\n4. Inserts an `epa_alerts` row if a threshold is crossed (idempotent: only one unresolved alert per type per employee)\\n\\n#### Aggregation Queries\\n\\n**`GetOrgStats`** \u2014 org-wide this-week aggregates (focus, active hours, time breakdown) plus last-week trend and top performer.\\n\\n**`GetEmployees`** \u2014 all monitored employees with this-week stats, baseline comparisons, and alert counts. Used by the workspace dashboard (now retired but kept for per-client roster).\\n\\n**`GetEmployeeDetail`** \u2014 one employee's base record, 7-day snapshots, and recent alerts. Used by drill-down and coaching brief.\\n\\n**`GetCompanyRoster`** \u2014 all contacts at a company (opted-in or not), with this-week aggregates and device binding status. Rows are sorted by opt-in status (opted-in first) then focus score.\\n\\n**`GetCompanyEPAStats`** \u2014 per-client aggregate header: opt-in counts, focus + active-hours trends, time breakdown, top performer, and sparkline data. Supports day/week/month periods via `NormalizePeriod`.\\n\\n**`GetContactDetail`** \u2014 one contact's drilldown: header, week aggregates, 7-day snapshots, and alerts. Keyed off contact ID (not employee) so the panel works even before telemetry lands.\\n\\n#### Baseline &amp; Retention\\n\\n`UpdateBaselineNightly` runs nightly from `main.go`:\\n1. Recalculates 30-day rolling averages for all employees (focus + active hours)\\n2. Prunes snapshots older than `epa_settings.retention_days`\\n\\nThis is called via `handler.GetStore()` to avoid importing the unexported store field.\\n\\n### Types (`types.go`)\\n\\n#### Application Classification\\n\\n`ClassifyApp` maps app names to categories via substring matching:\\n- **Productive**: IDEs, terminals, text editors (VS Code, Vim, Sublime, etc.)\\n- **Meeting**: Zoom, Teams, Google Meet, Webex\\n- **Communication**: Slack, Discord, Outlook, Thunderbird\\n- **Distraction**: YouTube, Netflix, Spotify, social media, games\\n- **Neutral**: Unknown apps (fallback, no penalty to focus score)\\n\\nThe map is case-insensitive and uses substring matching so \\\"Visual Studio Code\\\" matches \\\"visual studio code\\\".\\n\\n#### Focus Score\\n\\n`ComputeFocusScore(productive, meeting, distracted, active)` returns 0\u2013100:\\n\\n```\\nscore = (productive + 0.5 \u00d7 meeting) / active \u00d7 100\\n```\\n\\nIdle time divides out (not included in the denominator). Distraction is implicit in `active - productive - meeting`. The fourth parameter is retained for API symmetry but not used in the formula.\\n\\n#### Display Metadata\\n\\n`DashboardEmployee.ComputeDisplayMeta()` populates avatar and status colors:\\n- **Initials** \u2014 first letter of first and last name (or first letter only if single word)\\n- **Avatar palette** \u2014 5 color schemes cycled by a stable hash of agent ID; red if alert level is \\\"red\\\" or focus &lt; 50\\n- **Status dot &amp; score color** \u2014 red if alert=red or focus&lt;50, amber if focus&lt;70 or alert=amber, green otherwise\\n\\n#### Sparkline &amp; Donut\\n\\n`CompanyEPAStats.computeSparkline()` projects daily focus scores into a 700\u00d780 SVG viewBox:\\n- Score 0\u2013100 maps to y \u2208 [10, 70] (inverted; high score = low y)\\n- x-step depends on point count (7 days \u2192 ~117px between points)\\n- Returns line path (M x y L x y \u2026) and fill path (closed area)\\n\\n`CompanyEPAStats.computeDonut()` builds four stroke-dasharray segments (productive \u2192 meetings \u2192 idle \u2192 distracted) with cumulative offsets. Circumference is 188 (2\u03c0\u00b730 truncated to int).\\n\\n#### Period Selection\\n\\n`Period` (day | week | month) controls the analytics window. `NormalizePeriod` maps query-string input to a known period, defaulting to week.\\n\\n`Period.sql()` returns a `periodSQL` struct with:\\n- `trunc` \u2014 date_trunc window ('day' | 'week' | 'month')\\n- `prevOffset` \u2014 offset for trend calculation ('1 day' | '7 days' | '1 month')\\n- `seriesStart` / `seriesEnd` / `seriesStep` \u2014 generate_series bounds for sparkline\\n- `labelFmt` \u2014 to_char format for bucket labels\\n\\nThis allows a single query template to work across all three periods.\\n\\n## Data Flow\\n\\n### Ingest\\n\\n```\\nAgent (RSA-signed POST)\\n  \u2193\\nhandleIngest\\n  \u251c\u2500 verifyAgentSig (cert lookup, signature verify, expiry check)\\n  \u251c\u2500 IngestSnapshot\\n  \u2502  \u251c\u2500 Consent gate (epa_settings.consent_acknowledged_at)\\n  \u2502  \u251c\u2500 Assignment gate (agents.assigned_contact_id)\\n  \u2502  \u251c\u2500 Opt-in gate (contacts.nexie_epa_enabled)\\n  \u2502  \u251c\u2500 ClassifyApp (each app)\\n  \u2502  \u251c\u2500 ComputeFocusScore\\n  \u2502  \u251c\u2500 INSERT epa_snapshots (idempotent)\\n  \u2502  \u251c\u2500 UPSERT epa_employees\\n  \u2502  \u2514\u2500 spawn evaluateAlerts (background)\\n  \u2502     \u251c\u2500 3-day rolling avg focus\\n  \u2502     \u251c\u2500 UPDATE epa_employees.alert_level\\n  \u2502     \u2514\u2500 INSERT epa_alerts (idempotent)\\n  \u2514\u2500 204 No Content\\n```\\n\\n### Dashboard Rendering\\n\\n```\\nhandleCompanyEPASummary\\n  \u251c\u2500 GetCompanyRoster (all contacts, opt-in status, device binding)\\n  \u251c\u2500 GetCompanyEPAStats (aggregates, trends, sparkline, donut)\\n  \u251c\u2500 GetUnassignedAgentsForCompany (device picker)\\n  \u2514\u2500 Render epa/company_tab.html\\n     \u251c\u2500 Roster table (inline toggles for opt-in, device assignment)\\n     \u251c\u2500 Stats header (focus, active hours, time breakdown)\\n     \u2514\u2500 Sparkline + donut charts\\n```\\n\\n### Contact Drilldown\\n\\n```\\nhandleContactDetail\\n  \u251c\u2500 GetContactDetail (header, week aggregates, snapshots, alerts)\\n  \u251c\u2500 buildHeatmap (7-day snapshots \u2192 5\u00d77 grid)\\n  \u251c\u2500 buildAppBreakdown (top 5 apps by seconds)\\n  \u2514\u2500 Render epa/company_tab.html#contact_detail_panel\\n     \u251c\u2500 Contact header (name, title, device status)\\n     \u251c\u2500 Week KPIs (focus, active hours, trend)\\n     \u251c\u2500 App breakdown (top 5)\\n     \u251c\u2500 Activity heatmap\\n     \u2514\u2500 Active alerts\\n```\\n\\n### AI Coaching Brief\\n\\n```\\nhandleGenerateCoaching\\n  \u251c\u2500 GetEmployeeDetail (employee, snapshots, alerts)\\n  \u251c\u2500 buildAppBreakdown (top apps)\\n  \u251c\u2500 Format prompt (employee name, focus, hours, apps, alerts)\\n  \u251c\u2500 ai.CallClaudeRawTracked (system prompt + data)\\n  \u251c\u2500 Parse JSON response (brief + talking_points)\\n  \u251c\u2500 INSERT epa_coaching_sessions (draft status)\\n  \u2514\u2500 Render epa/dashboard.html#coaching_partial\\n```\\n\\n## Integration Points\\n\\n### Authentication\\n\\nEPA uses the standard JWT + role-based access control from `internal/auth`:\\n- `ClaimsFromContext` \u2014 extract JWT claims\\n- `TenantIDFromContext` \u2014 extract tenant ID\\n- `UserIDFromContext` \u2014 extract user ID (for audit trails)\\n- `InjectUserData` \u2014 populate template context with user info\\n\\n### AI\\n\\nEPA calls Claude via `internal/ai`:\\n- `ai.Provider.GetAPIKey` \u2014 fetch tenant's Anthropic API key\\n- `ai.Provider.CallClaudeRawTracked` \u2014 call Claude with usage tracking\\n\\nResponses are tracked with `ai.UsageMeta` (tenant ID, company ID, action name) for billing.\\n\\n### UI Rendering\\n\\nEPA uses `internal/ui.Renderer`:\\n- `Render` \u2014 full page render (dashboard)\\n- `RenderBlock` \u2014 partial render (HTMX responses)\\n\\nTemplates live in `epa/dashboard.html` and `epa/company_tab.html`.\\n\\n### Database\\n\\nEPA owns the `epa_*` tables:\\n- `epa_settings` \u2014 tenant-level consent, thresholds, retention, allowed roles\\n- `epa_snapshots` \u2014 15-minute telemetry windows (indexed on tenant + agent + window_start)\\n- `epa_employees` \u2014 monitored users (agent \u2194 contact mapping, baseline scores, alert state)\\n- `epa_alerts` \u2014 triggered productivity alerts\\n- `epa_coaching_sessions` \u2014 AI-generated coaching briefs (audit trail)\\n\\nEPA also reads from:\\n- `agents` \u2014 device records (cert, fingerprint, assigned contact, status)\\n- `contacts` \u2014 PSA users (name, title, EPA opt-in flag)\\n- `companies` \u2014 client accounts (tenant scope)\\n\\n## Error Handling\\n\\n**Ingest errors** are returned as JSON with appropriate HTTP status codes:\\n- `428 Precondition Required` \u2014 consent not acknowledged\\n- `409 Conflict` \u2014 agent not assigned to a contact\\n- `403 Forbidden` \u2014 EPA disabled for this contact\\n- `401 Unauthorized` \u2014 signature verification failed\\n- `400 Bad Request` \u2014 malformed request body\\n\\n**Handler errors** are logged and returned as HTTP errors (500, 404, etc.). HTMX requests that fail role gates receive `HX-Redirect` instead of 403 to preserve page state.\\n\\n**AI errors** are logged and returned as 500 or 400 (if API key is missing). The UI gracefully degrades if AI is not configured.\\n\\n## Nightly Maintenance\\n\\n`main.go` runs `handler.GetStore().UpdateBaselineNightly(tenantID)` nightly for each tenant:\\n1. Recalculates 30-day rolling averages (focus + active hours) for all employees\\n2. Prunes snapshots older than the tenant's retention window\\n\\nThis keeps baselines fresh for trend calculations and manages storage.\\n\\n## Design Notes\\n\\n### Why Signature Auth for Ingest?\\n\\nAgents are untrusted clients in the field. RSA-signed requests with cert fingerprints prevent spoofing and allow the server to derive tenant identity from the certificate, not from a header. This mirrors the device API pattern.\\n\\n### Why Per-Contact Opt-In?\\n\\nEPA is billed per opted-in contact. A per-company kill switch would gate one billing signal behind another, creating friction. Per-contact opt-in lets admins enable EPA for specific team members without account-wide commitment.\\n\\n### Why Background Alert Evaluation?\\n\\nAlert evaluation is CPU-bound (3-day rolling average, threshold checks). Running it in a background goroutine on a fresh context prevents request cancellation from aborting mid-query and keeps ingest latency low.\\n\\n### Why Idempotent Snapshots?\\n\\nAgents may retry failed requests. Idempotent inserts (ON CONFLICT DO NOTHING) ensure duplicate windows don't skew aggregates.\\n\\n### Why Substring Matching for App Classification?\\n\\nApp names vary by OS and version (e.g., \\\"Visual Studio Code\\\" vs \\\"code\\\" vs \\\"vscode\\\"). Substring matching is forgiving and catches variants without maintaining a massive lookup table.\",\"internal-financial\":\"# internal \u2014 financial\\n\\n# Financial Module Documentation\\n\\n## Overview\\n\\nThe `internal/financial` module provides a provider-agnostic interface for reading and writing financial data (products, vendors, invoices) from upstream accounting systems. It abstracts away provider-specific details so that distributor matching, margin display, and invoice workflows can work with any financial backend\u2014today QuickBooks Online (QBO), tomorrow FreshBooks, NetSuite, or Xero.\\n\\n**Key principle:** QBO is canonical for all financial fields (locked 2026-05-01). PSA never overwrites provider data; it only reads and caches it locally.\\n\\n## Architecture\\n\\n```mermaid\\ngraph TB\\n    Caller[\\\"Distributor / Invoice Code\\\"]\\n    Registry[\\\"Registry(Resolver)\\\"]\\n    Provider[\\\"Provider Interface\\\"]\\n    QBO[\\\"QBO Adapter(qbo.Provider)\\\"]\\n    Cache[\\\"Local Postgres Cache(products, vendors)\\\"]\\n    QBOSync[\\\"internal/quickbooks/sync.go(out-of-band refresh)\\\"]\\n    \\n    Caller --&gt;|Resolve| Registry\\n    Registry --&gt;|returns| Provider\\n    Caller --&gt;|Lookup*| Provider\\n    Provider --&gt;|implements| QBO\\n    QBO --&gt;|reads| Cache\\n    QBOSync --&gt;|populates| Cache\\n    \\n    style Provider fill:#e1f5ff\\n    style Registry fill:#fff3e0\\n    style QBO fill:#f3e5f5\\n    style Cache fill:#e8f5e9\\n```\\n\\n## Core Components\\n\\n### Provider Interface\\n\\n`Provider` is the contract every financial-system adapter must satisfy:\\n\\n```go\\ntype Provider interface {\\n    Name() string\\n    IsConnected(ctx context.Context, tenantID string) bool\\n    LookupProductByMfgPart(ctx context.Context, tenantID, mfgPart string) (*Product, error)\\n    LookupProductBySKU(ctx context.Context, tenantID, sku string) (*Product, error)\\n    LookupVendorByName(ctx context.Context, tenantID, name string) (*Vendor, error)\\n    SyncProducts(ctx context.Context, tenantID string, opts ListOpts) (SyncResult, error)\\n    SyncVendors(ctx context.Context, tenantID string, opts ListOpts) (SyncResult, error)\\n    PushInvoice(ctx context.Context, tenantID string, inv Invoice) (Invoice, error)\\n}\\n```\\n\\n**Lookup methods** read from the local cache (fast, no API calls):\\n- `LookupProductByMfgPart` \u2014 bridge a distributor SKU to a NexusOS product via manufacturer part number\\n- `LookupProductBySKU` \u2014 find a product by internal SKU\\n- `LookupVendorByName` \u2014 find a vendor by display name (case-insensitive)\\n\\n**Sync methods** refresh the local cache from the upstream provider (one-way: provider \u2192 PSA):\\n- `SyncProducts` \u2014 pull product changes since `opts.UpdatedAfter`\\n- `SyncVendors` \u2014 pull vendor changes since `opts.UpdatedAfter`\\n\\n**Push methods** write data outbound:\\n- `PushInvoice` \u2014 write an invoice; idempotency key is caller-minted for safe retries\\n\\n`IsConnected` gates UI and work based on whether the tenant has an active connection to the upstream system.\\n\\n### Registry &amp; Resolver\\n\\n`Registry` is the default `Resolver` implementation. It binds tenant credentials at resolve time (per-call, not per-process) so a single binary safely serves multiple tenants.\\n\\n```go\\ntype Registry struct {\\n    mu     sync.RWMutex\\n    byName map[string]Provider\\n    def    Provider\\n}\\n```\\n\\n**Usage:**\\n1. Adapters register themselves at startup: `registry.Register(qboProvider)`\\n2. The first registered provider becomes the default; override with `SetDefault(name)`\\n3. Callers resolve the active provider per-request: `provider, err := registry.Resolve(ctx, tenantID)`\\n\\nToday every tenant resolves to the default. Tenant-specific binding (e.g., \\\"this tenant uses FreshBooks, that one uses NetSuite\\\") lands when the second provider ships.\\n\\n### QBO Adapter\\n\\n`internal/financial/qbo` implements `Provider` for QuickBooks Online. It reads from a local Postgres cache populated by `internal/quickbooks/sync.go`\u2014never calls the QBO API directly. This keeps reads cheap and avoids leaking QBO rate-limit concerns into hot paths like distributor matching.\\n\\n**Key methods:**\\n\\n- `New(pool *pgxpool.Pool) *Provider` \u2014 constructor\\n- `Name()` \u2014 returns `\\\"quickbooks\\\"`\\n- `IsConnected(ctx, tenantID)` \u2014 checks for an active QBO connection row with a valid refresh token\\n- `LookupProductByMfgPart(ctx, tenantID, mfgPart)` \u2014 queries the `products` table by manufacturer part number\\n- `LookupProductBySKU(ctx, tenantID, sku)` \u2014 queries the `products` table by SKU\\n- `LookupVendorByName(ctx, tenantID, name)` \u2014 queries the `vendors` table by display name (case-insensitive)\\n- `queryOne(ctx, query, tenantID, arg)` \u2014 shared row scanner for product lookups\\n\\n**Stub methods** (real logic lives elsewhere):\\n- `SyncProducts`, `SyncVendors` \u2014 return `ErrNotConnected`; real sync runs via `internal/quickbooks/sync.go` triggered out-of-band by the QBO connect flow\\n- `PushInvoice` \u2014 returns `ErrNotConnected`; real push runs via `internal/quickbooks/push.go`\\n\\nThese stubs exist because sync and push are driven by separate workflows (Phase 3 prerequisite #3 \u2014 nightly sync cron). Wiring them through this adapter lands when distributor sync starts driving them.\\n\\n## Data Types\\n\\n### Product\\n\\nPlain, provider-agnostic shape of a sellable item:\\n\\n```go\\ntype Product struct {\\n    ID                  string\\n    ExternalID          string    // upstream provider's ID\\n    Name                string\\n    SKU                 string\\n    ManufacturerPartNum string\\n    Vendor              string    // free-text vendor name (not an FK)\\n    Description         string\\n    UnitCost            float64\\n    UnitPrice           float64   // monthly recurring or one-time list price\\n    BillingType         string    // mrr, yrr, one_time\\n    RevenueType         string    // mrr, orr, nrr, product_sales, misc\\n    IsActive            bool\\n    UpdatedAt           time.Time\\n}\\n```\\n\\nPSA reads these fields from the provider's cache; it never overwrites them.\\n\\n### Vendor\\n\\nMirrors the \\\"supplier\\\" record on the financial-provider side:\\n\\n```go\\ntype Vendor struct {\\n    ID          string\\n    ExternalID  string\\n    DisplayName string\\n    CompanyName string\\n    Category    string\\n    Email       string\\n    Phone       string\\n    IsActive    bool\\n    QBOVendorID string    // empty for non-QBO providers\\n    UpdatedAt   time.Time\\n}\\n```\\n\\n### Invoice\\n\\nWhat PSA pushes outbound:\\n\\n```go\\ntype Invoice struct {\\n    IdempotencyKey   string        // caller-minted; same key = same invoice\\n    ExternalID       string        // populated on read; empty on push\\n    CustomerExternal string        // FK to a financial-provider customer\\n    DocNumber        string\\n    IssueDate        time.Time\\n    DueDate          time.Time\\n    Lines            []InvoiceLine\\n    Memo             string\\n}\\n\\ntype InvoiceLine struct {\\n    ProductExternal string    // optional; empty = freeform line\\n    Description     string\\n    Qty             float64\\n    UnitPrice       float64\\n    Amount          float64   // Qty * UnitPrice; provider may recompute\\n}\\n```\\n\\n**Idempotency:** The caller mints `IdempotencyKey`. On retry with the same key, the provider returns the same `ExternalID`. This is safer than letting adapters invent keys.\\n\\n### SyncResult\\n\\nUniform shape every `Sync*` method returns:\\n\\n```go\\ntype SyncResult struct {\\n    Synced     int          // records persisted to PSA's local cache\\n    Skipped    int          // records provider returned but PSA didn't write (e.g. archived)\\n    Errors     []SyncError  // non-fatal per-record failures the sync continued past\\n    DurationMS int64\\n}\\n\\ntype SyncError struct {\\n    ExternalID string\\n    Reason     string\\n}\\n```\\n\\n### ListOpts\\n\\nNarrows a sync read:\\n\\n```go\\ntype ListOpts struct {\\n    UpdatedAfter *time.Time  // enables incremental syncs\\n    Limit        int         // advisory; some providers cap differently\\n    Offset       int\\n}\\n```\\n\\n## Error Handling\\n\\nSentinel errors allow callers to distinguish failure modes:\\n\\n```go\\nvar (\\n    ErrNotFound     = errors.New(\\\"financial: not found\\\")\\n    ErrNotConnected = errors.New(\\\"financial: tenant not connected\\\")\\n    ErrTokenExpired = errors.New(\\\"financial: token expired, reconnect required\\\")\\n    ErrConflict     = errors.New(\\\"financial: write rejected (duplicate or stale)\\\")\\n)\\n```\\n\\nAdapters should wrap with `%w` so callers can use `errors.Is()`:\\n\\n```go\\nif errors.Is(err, financial.ErrNotFound) {\\n    // handle missing product\\n}\\n```\\n\\n## Design Principles\\n\\n1. **Uniform interface:** Every provider implements the same `Provider` surface. Distributor code never knows which backend is active.\\n\\n2. **One-way flow:** QBO (and future providers) are canonical. PSA reads and caches; it never overwrites upstream data.\\n\\n3. **Per-call resolution:** The `Registry` binds credentials at resolve time, not at process startup. This lets a single binary serve multiple tenants safely.\\n\\n4. **Caller-minted idempotency:** Invoice pushes use caller-minted idempotency keys, not adapter-generated ones. Safer for retries.\\n\\n5. **Lean defaults:** No provider-specific fields in the core types. Adapters translate to and from their native shapes.\\n\\n6. **Cache-first reads:** Lookup methods read from the local Postgres cache, not the upstream API. Keeps reads fast and avoids rate-limit leakage.\\n\\n## Integration Points\\n\\n### Distributor Matching\\n\\nDistributor code calls `LookupProductByMfgPart` to bridge a distributor SKU to a NexusOS product. The QBO adapter queries the local `products` table.\\n\\n### Margin Display\\n\\nInvoice and quote workflows call `LookupProductBySKU` to fetch unit cost and pricing. Again, reads from the local cache.\\n\\n### Invoice Push\\n\\nBilling code calls `PushInvoice` with a caller-minted idempotency key. Today this returns `ErrNotConnected` (real push lives in `internal/quickbooks/push.go`); wiring it through this adapter lands in Phase 3.\\n\\n### Out-of-Band Sync\\n\\n`internal/quickbooks/sync.go` populates the local `products` and `vendors` tables. This runs out-of-band (triggered by the QBO connect flow, later by a nightly cron). The QBO adapter reads from this cache.\\n\\n## Adding a New Provider\\n\\nTo add a new financial provider (e.g., FreshBooks):\\n\\n1. Create `internal/financial/freshbooks/provider.go` implementing the `Provider` interface.\\n2. Implement all seven methods: `Name()`, `IsConnected()`, three `Lookup*` methods, two `Sync*` methods, `PushInvoice()`.\\n3. Register it in `cmd/psa/main.go`: `registry.Register(freshbooksProvider)`.\\n4. Optionally set it as default: `registry.SetDefault(\\\"freshbooks\\\")`.\\n5. Populate your own local cache tables (or reuse the existing `products` / `vendors` schema if compatible).\\n\\nThe rest of the codebase\u2014distributor matching, invoice workflows, margin display\u2014works unchanged.\",\"internal-health\":\"# internal \u2014 health\\n\\n# Health Module\\n\\nThe health module provides a comprehensive infrastructure monitoring dashboard for NexusOS. It exposes system metrics (CPU, memory, disk, network), manages ecosystem component health checks (HTTP, TCP, PostgreSQL, process), and enables service management (status queries and restarts).\\n\\n## Overview\\n\\nThe module serves two primary functions:\\n\\n1. **System Metrics Collection** \u2014 gathers real-time host-level metrics from `/proc` and system calls\\n2. **Ecosystem Component Management** \u2014 stores, probes, and reports health of external services and dependencies\\n\\nAll component data is tenant-scoped and persisted in the database. The module exposes both HTML dashboard views and JSON APIs for programmatic access.\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    A[\\\"HTTP Handlers\\\"] --&gt;|query| B[\\\"Databasehealth_components\\\"]\\n    A --&gt;|collect| C[\\\"System MetricsCPU, Memory, Disk, Network\\\"]\\n    A --&gt;|probe| D[\\\"Component ProbesHTTP, TCP, PG, Process\\\"]\\n    D --&gt;|concurrent| E[\\\"Probe ResultsStatus + Latency\\\"]\\n    A --&gt;|manage| F[\\\"Service Controlsystemctl\\\"]\\n    B --&gt;|tenant-scoped| G[\\\"Component CRUD\\\"]\\n```\\n\\n## Core Components\\n\\n### Handler\\n\\nThe `Handler` struct is the entry point for all health operations:\\n\\n```go\\ntype Handler struct {\\n    pool     *pgxpool.Pool\\n    renderer *ui.Renderer\\n}\\n```\\n\\nCreate a handler with `NewHandler(pool, renderer)` and register routes via `RegisterRoutes(mux)`.\\n\\n### System Metrics\\n\\nThe module collects five categories of system metrics via `collectMetrics()`:\\n\\n- **CPU** \u2014 usage percentage, core count, model name, load averages (1/5/15 min)\\n- **Memory** \u2014 total/used/free in MB, usage percentage, swap stats\\n- **Disk** \u2014 total/used/free in GB, usage percentage for root mount point\\n- **OS** \u2014 hostname, OS type, architecture, kernel version, distro name\\n- **Network** \u2014 interface list with IPs, MAC addresses, up/down status\\n- **Network I/O** \u2014 cumulative RX/TX bytes across all non-loopback interfaces\\n- **Uptime** \u2014 formatted as \\\"Xd Yh Zm\\\"\\n\\nMetrics are collected on-demand via `/api/health/metrics` and do not require database access.\\n\\n#### Platform-Specific Disk Collection\\n\\nDisk metrics use platform-specific implementations:\\n\\n- **Linux &amp; Darwin** \u2014 `syscall.Statfs()` to query filesystem stats\\n- **Windows** \u2014 stub implementation (returns mount point only)\\n\\n### Ecosystem Components\\n\\nComponents represent external services or dependencies that need monitoring. Each component has:\\n\\n- **Identity** \u2014 unique ID, name, description, category\\n- **Check Configuration** \u2014 type (http, tcp, pg, process), target address\\n- **Display** \u2014 icon, display order for UI sorting\\n- **Remote Access** \u2014 optional SSH host/port/user for off-host probes\\n- **Metrics Collection** \u2014 flag to enable extended metric gathering\\n\\nComponents are stored per-tenant in `health_components` table and managed via CRUD endpoints.\\n\\n#### Component Check Types\\n\\n| Type | Target Format | Probe Method | Extended Metrics |\\n|------|---------------|--------------|------------------|\\n| `http` | Full URL (e.g., `http://api:8080/health`) | GET request, 5s timeout | Status code, content length, server header, avg latency |\\n| `tcp` | Host:port (e.g., `db:5432`) | TCP dial, 5s timeout | None |\\n| `pg` | N/A (uses handler's pool) | `SELECT version()` | Database sizes, active connections, cache hit ratio, slow queries |\\n| `process` | Service name (e.g., `nginx`) | `systemctl is-active` + `pgrep` | Memory, CPU time, PID, uptime, task count |\\n\\n#### Probe Execution\\n\\nThe `handleComponents` endpoint fans out concurrent probes for all enabled components:\\n\\n1. Query database for enabled components\\n2. Launch goroutine per component\\n3. Execute appropriate probe function\\n4. Measure latency\\n5. Collect results and return JSON\\n\\nFailed probes return status \\\"down\\\" or \\\"degraded\\\" with error details.\\n\\n### Component-Specific Metrics\\n\\nThe `/api/health/components/{id}/metrics` endpoint provides focused, extended data for a single component:\\n\\n- **PostgreSQL** \u2014 top 10 databases by size, active/max connections, cache hit ratio, transaction stats, table count, top 5 slow queries (if `pg_stat_statements` available)\\n- **HTTP** \u2014 3 sequential probes for average latency, status code, content length, server header\\n- **Process** \u2014 memory usage (MB), CPU time (seconds), PID, start timestamp, task count\\n\\n### Service Management\\n\\nThe module monitors and controls a hardcoded list of core systemd services:\\n\\n```go\\nvar coreServices = []string{\\n    \\\"nexusos-psa\\\", \\\"postgresql\\\", \\\"docker\\\", \\\"nginx\\\", \\\"ssh\\\",\\n    \\\"systemd-resolved\\\", \\\"fail2ban\\\", \\\"ufw\\\",\\n}\\n```\\n\\n- **Status Queries** \u2014 `/api/health/services` returns status of all core services\\n- **Restart** \u2014 `/POST /api/health/services/{name}/restart` restarts a whitelisted service (requires sudo)\\n\\nRestart operations are audited and logged.\\n\\n## API Endpoints\\n\\n### Dashboard &amp; Metrics\\n\\n| Endpoint | Method | Purpose |\\n|----------|--------|---------|\\n| `/settings/health` | GET | HTML dashboard (supports HTMX partials) |\\n| `/api/health/metrics` | GET | System metrics snapshot (JSON) |\\n\\n### Component Management\\n\\n| Endpoint | Method | Purpose |\\n|----------|--------|---------|\\n| `/api/health/components` | GET | List enabled components with live probes |\\n| `/api/health/components/manage` | GET | List all components (including disabled) for management UI |\\n| `/api/health/components` | POST | Create new component |\\n| `/api/health/components/{id}` | PUT | Update component configuration |\\n| `/api/health/components/{id}` | DELETE | Delete component |\\n| `/api/health/components/{id}/metrics` | GET | Focused metrics for single component |\\n\\n### Service Management\\n\\n| Endpoint | Method | Purpose |\\n|----------|--------|---------|\\n| `/api/health/services` | GET | List status of all core services |\\n| `/api/health/services/{name}/restart` | POST | Restart a service (whitelisted only) |\\n\\n## Data Flow\\n\\n### Metrics Collection\\n\\n```\\nGET /api/health/metrics\\n  \u2192 collectMetrics()\\n    \u2192 collectCPU() [/proc/cpuinfo, /proc/loadavg, /proc/stat]\\n    \u2192 collectMemory() [/proc/meminfo]\\n    \u2192 collectDisk(\\\"/\\\") [syscall.Statfs]\\n    \u2192 collectOS() [/proc/version, /etc/os-release, os.Hostname]\\n    \u2192 collectNetwork() [net.Interfaces]\\n    \u2192 collectNetIO() [/proc/net/dev]\\n    \u2192 collectUptime() [/proc/uptime]\\n  \u2192 JSON response\\n```\\n\\n### Component Probing\\n\\n```\\nGET /api/health/components\\n  \u2192 Query DB for enabled components (tenant-scoped)\\n  \u2192 For each component (concurrent):\\n    \u2192 Probe based on check_type\\n    \u2192 Measure latency\\n    \u2192 Collect status + details\\n  \u2192 Aggregate results\\n  \u2192 JSON response\\n```\\n\\n### Component-Specific Metrics\\n\\n```\\nGET /api/health/components/{id}/metrics\\n  \u2192 Query DB for component config\\n  \u2192 Execute probe (same as above)\\n  \u2192 Collect extended metrics based on check_type\\n  \u2192 Return ComponentMetricsResponse with component + extended data\\n```\\n\\n## Database Schema\\n\\nThe module expects a `health_components` table with columns:\\n\\n- `id` \u2014 unique identifier\\n- `tenant_id` \u2014 tenant scope\\n- `name`, `description`, `category` \u2014 metadata\\n- `check_type` \u2014 probe type (http, tcp, pg, process)\\n- `target` \u2014 probe target (URL, host:port, service name)\\n- `icon`, `display_order` \u2014 UI presentation\\n- `enabled` \u2014 visibility flag\\n- `ssh_host`, `ssh_port`, `ssh_user`, `ssh_key_path` \u2014 remote probe config\\n- `collect_metrics` \u2014 extended metrics flag\\n- `created_at`, `updated_at` \u2014 timestamps\\n\\nComponent CRUD operations also write to `audit_logs` for compliance.\\n\\n## Tenant Isolation\\n\\nAll component queries are scoped to `tenant_id` via context:\\n\\n```go\\ntenantID := auth.TenantIDFromContext(r.Context())\\n```\\n\\nSystem metrics (CPU, memory, disk, network) are host-level and not tenant-scoped; they reflect the entire server state.\\n\\n## Error Handling\\n\\n- **Probe Failures** \u2014 return status \\\"down\\\" or \\\"degraded\\\" with error message\\n- **Database Errors** \u2014 log and return HTTP 500\\n- **Missing Components** \u2014 return HTTP 404\\n- **Invalid Input** \u2014 return HTTP 400 with validation message\\n- **Unauthorized Service Restart** \u2014 return HTTP 403 (service not in whitelist)\\n\\n## Integration Points\\n\\n### Authentication\\n\\nThe module uses `auth.TenantIDFromContext()` and `auth.UserIDFromContext()` to extract tenant and user IDs from request context. All component operations are tenant-scoped.\\n\\n### UI Rendering\\n\\nThe dashboard HTML is rendered via `h.renderer.Render()` with support for HTMX partial updates (detected via `HX-Request` header).\\n\\n### Database\\n\\nThe module uses `pgxpool.Pool` for all database operations. PostgreSQL probes use the same pool connection.\\n\\n### Audit Logging\\n\\nComponent CRUD and service restarts are logged to `audit_logs` table with action, entity type, and details.\\n\\n## Performance Considerations\\n\\n- **Concurrent Probes** \u2014 component probes run in parallel goroutines to minimize latency\\n- **Timeouts** \u2014 all probes have 3\u20135 second timeouts to prevent hanging\\n- **Metrics Caching** \u2014 system metrics are collected on-demand; consider caching if polling frequency is high\\n- **Database Queries** \u2014 component queries are indexed by tenant_id and enabled flag\\n\\n## Common Patterns\\n\\n### Adding a New Check Type\\n\\n1. Define probe function (e.g., `probeCustom(target string) (status, details string)`)\\n2. Add case in `handleComponents` switch statement\\n3. Optionally add extended metrics collector (e.g., `collectCustomMetrics()`)\\n4. Add case in `handleComponentMetrics` switch for extended data\\n5. Update database schema if new fields needed\\n\\n### Querying Component Status\\n\\n```go\\n// Get all enabled components with live probes\\nGET /api/health/components\\n\\n// Get all components (including disabled) for management\\nGET /api/health/components/manage\\n\\n// Get focused metrics for a specific component\\nGET /api/health/components/{id}/metrics\\n```\\n\\n### Restarting a Service\\n\\n```go\\nPOST /api/health/services/{name}/restart\\n\\n// Response includes updated ServiceStatus\\n{\\n  \\\"name\\\": \\\"nginx\\\",\\n  \\\"status\\\": \\\"active\\\",\\n  \\\"is_active\\\": true,\\n  \\\"started_at\\\": \\\"2024-01-15 10:30:45.123456 UTC\\\"\\n}\\n```\",\"internal-helpdesk\":\"# internal \u2014 helpdesk\\n\\n# Internal Helpdesk Module\\n\\nThe helpdesk module provides a complete ticketing system for NexusOS, including ticket lifecycle management, SLA enforcement, workflow automation, email integration, and AI-powered ticket intelligence. It serves as the central hub for IT service delivery, connecting to billing, contracts, RMM agents, and the orchestrator engine.\\n\\n## Core Responsibilities\\n\\n**Ticket Lifecycle**  \\nCreate, update, and close tickets with configurable statuses, priorities, types, and categories. Track ticket history, notes, and time entries. Support multiple ticket sources (email, portal, phone, API, RMM, M365, Entra).\\n\\n**SLA Management**  \\nDefine response and resolution SLAs per ticket type/priority/source combination. Monitor breach and warning thresholds in real time. Escalate tickets automatically when SLAs are at risk.\\n\\n**Workflow Automation**  \\nTrigger actions on ticket events (created, updated, closed, escalated). Execute cross-module actions like creating assessments, generating invoices, or blocking IPs. Support conditional logic and approval gates.\\n\\n**Email Integration**  \\nInbound: Create tickets from emails or add notes to existing tickets via thread matching.  \\nOutbound: Send notifications, auto-replies, and SLA alerts via configurable email templates.\\n\\n**AI Features**  \\nClassify tickets, suggest responses, summarize ticket history, generate solutions, and resolve tickets with AI assistance. Validate scripts before execution. Integrate with session recordings for context-aware resolution.\\n\\n**Configuration Taxonomy**  \\nManage 8 entity types (statuses, priorities, teams, types, categories, sources, impacts, urgencies) with audit logging. Support system rows (shared across tenants) and tenant-scoped customization.\\n\\n---\\n\\n## Architecture Overview\\n\\n```mermaid\\ngraph TB\\n    subgraph \\\"HTTP Layer\\\"\\n        Handler[\\\"Handler(HTTP routes)\\\"]\\n    end\\n    \\n    subgraph \\\"Business Logic\\\"\\n        Store[\\\"Store(DB queries)\\\"]\\n        WF[\\\"WorkflowEngine(automation)\\\"]\\n        SLA[\\\"SLAEngine(breach detection)\\\"]\\n        Email[\\\"EmailProcessor(inbound/outbound)\\\"]\\n    end\\n    \\n    subgraph \\\"External Integrations\\\"\\n        AI[\\\"AIAssistant(optional)\\\"]\\n        RMM[\\\"RMMCommander(optional)\\\"]\\n        Sessions[\\\"SessionProvider(optional)\\\"]\\n    end\\n    \\n    subgraph \\\"Orchestrator\\\"\\n        Orch[\\\"OrchestratorEngine(cross-module events)\\\"]\\n    end\\n    \\n    Handler --&gt;|queries/mutations| Store\\n    Handler --&gt;|triggers| WF\\n    Handler --&gt;|checks| SLA\\n    Handler --&gt;|processes| Email\\n    Handler --&gt;|calls| AI\\n    Handler --&gt;|calls| RMM\\n    Handler --&gt;|queries| Sessions\\n    WF --&gt;|emits| Orch\\n    SLA --&gt;|emits| Orch\\n    Email --&gt;|emits| Orch\\n```\\n\\n---\\n\\n## Key Components\\n\\n### Handler (`handler.go`)\\n\\nHTTP request dispatcher for tickets, notes, time entries, and AI features. Supports both HTMX page rendering and JSON API responses.\\n\\n**Main Routes:**\\n- `GET /helpdesk/tickets` \u2014 ticket list (brief view)\\n- `GET /helpdesk/tickets/{id}` \u2014 ticket detail (V2 layout)\\n- `POST /api/tickets` \u2014 create ticket\\n- `PATCH /api/tickets/{id}` \u2014 update ticket fields\\n- `POST /api/tickets/{id}/notes` \u2014 add note\\n- `POST /api/tickets/{id}/time` \u2014 log time entry\\n- `POST /api/tickets/{id}/ai/*` \u2014 AI-powered features\\n\\n**Key Methods:**\\n- `handleCreateTicket()` \u2014 validates input, creates ticket, fires workflow triggers\\n- `handleUpdateTicket()` \u2014 patches fields, audits changes, re-evaluates SLAs\\n- `handleAISuggestResponse()` \u2014 calls AIAssistant to generate response text\\n- `handleAIResolve()` \u2014 generates resolution with session context, requires approval\\n- `handleApproveResolution()` \u2014 accepts AI resolution, closes ticket, logs time\\n\\n**Dependencies:**\\n- `Store` \u2014 database access\\n- `ui.Renderer` \u2014 template rendering\\n- `AIAssistant` (optional) \u2014 ticket intelligence\\n- `RMMCommander` (optional) \u2014 remote execution\\n- `SessionProvider` (optional) \u2014 session data for AI context\\n- `WorkflowEngine` \u2014 automation triggers\\n\\n---\\n\\n### Store (`store.go`)\\n\\nDatabase abstraction layer for all ticket operations. Manages tickets, notes, time entries, contracts, email accounts, and configuration entities.\\n\\n**Core Tables:**\\n- `tickets` \u2014 ticket records with status, priority, type, source\\n- `ticket_notes` \u2014 comments and internal notes\\n- `ticket_time_entries` \u2014 billable/non-billable time\\n- `ticket_charges` \u2014 expenses and materials\\n- `service_contracts` \u2014 billing contracts with companies\\n- `email_accounts` \u2014 IMAP/SMTP credentials for inbound/outbound\\n- `ticket_statuses`, `ticket_priorities`, `ticket_teams`, `ticket_types`, `ticket_categories`, `ticket_sources`, `ticket_impacts`, `ticket_urgencies` \u2014 configuration taxonomy\\n\\n**Key Methods:**\\n- `CreateTicket(ctx, ticket)` \u2014 inserts ticket, returns generated ID and number\\n- `UpdateTicket(ctx, id, updates)` \u2014 patches fields, returns updated ticket\\n- `GetTicket(ctx, id)` \u2014 fetches full ticket with notes and time entries\\n- `CreateNote(ctx, tenantID, note)` \u2014 adds note, returns note ID\\n- `LogTime(ctx, tenantID, entry)` \u2014 records time entry, updates ticket hours_logged\\n\\n---\\n\\n### WorkflowEngine (`workflow.go`)\\n\\nAutomation engine that evaluates conditions and executes actions on ticket events. Supports cross-module actions (create assessment, generate invoice, block IP, etc.).\\n\\n**Workflow Structure:**\\n```\\nTrigger (event type) \u2192 Conditions (if/else) \u2192 Actions (execute)\\n```\\n\\n**Supported Triggers:**\\n- `ticket_created` \u2014 new ticket from any source\\n- `ticket_updated` \u2014 any field change\\n- `ticket_closed` \u2014 status \u2192 closed/resolved\\n- `ticket_escalated` \u2014 priority increased or SLA warning\\n- `sla_breach` \u2014 response or resolution SLA exceeded\\n- `email_received` \u2014 inbound email processed\\n\\n**Supported Actions:**\\n- **Helpdesk:** `set_status`, `set_priority`, `assign_to`, `assign_team`, `add_note`, `escalate`, `merge_tickets`, `create_child_ticket`, `start_timer`, `log_time`\\n- **Notifications:** `send_notification`, `send_email`, `send_auto_reply`\\n- **Cross-Module:** `create_assessment`, `convert_assessment_to_quote`, `create_contract_from_quote`, `generate_invoice_from_ticket`, `trigger_onboarding`, `advance_deal_stage`, `create_bid_from_survey`, `create_project`, `create_work_order_from_bid`\\n- **Security:** `block_ip`, `enable_ips_rule`, `run_vulnerability_scan`, `run_agent_command`\\n- **Control:** `stop_workflow`, `send_webhook`\\n\\n**Key Methods:**\\n- `CreateWorkflow(ctx, tenantID, workflow)` \u2014 saves workflow definition\\n- `EvaluateTicketEvent(ctx, tenantID, ticketID, eventType)` \u2014 loads ticket, evaluates conditions, executes matching actions\\n- `executeAction(ctx, action, ticket, context)` \u2014 dispatches to action handler\\n- `ModuleActions(eventType)` \u2014 returns cross-module action handlers (wired at startup)\\n\\n**Condition Evaluation:**\\nConditions are evaluated as a tree: `(A AND B) OR (C AND NOT D)`. Each condition references a field (status, priority, source, etc.) and an operator (equals, contains, in_list, etc.).\\n\\n---\\n\\n### SLAEngine (`sla.go`)\\n\\nReal-time SLA monitoring. Tracks response and resolution times, emits breach and warning events, and escalates tickets automatically.\\n\\n**SLA Policy Structure:**\\n```\\n{\\n  \\\"ticket_type\\\": \\\"incident\\\",\\n  \\\"priority\\\": \\\"p1\\\",\\n  \\\"source\\\": \\\"email\\\",\\n  \\\"response_minutes\\\": 60,\\n  \\\"resolution_minutes\\\": 480,\\n  \\\"escalation_policy\\\": \\\"on-call\\\"\\n}\\n```\\n\\n**Key Methods:**\\n- `CreateSLAPolicy(ctx, tenantID, policy)` \u2014 saves policy\\n- `StartBreachChecker(ctx, interval)` \u2014 background goroutine that checks all open tickets every N seconds\\n- `checkTicketSLA(ctx, ticket)` \u2014 compares created_at/updated_at against policy, emits events\\n- `EvaluateSLA(ticket)` \u2014 returns SLA status (ok, warning, breached) and time remaining\\n\\n**Events Emitted:**\\n- `sla_warning` \u2014 ticket at 75% of threshold\\n- `sla_breach` \u2014 ticket exceeded threshold\\n\\n---\\n\\n### EmailProcessor (`email.go`)\\n\\nHandles inbound and outbound email for tickets.\\n\\n**Inbound Flow:**\\n1. Webhook receives email (from SendGrid, Mailgun, or custom IMAP poller)\\n2. Extract sender, subject, body, message ID\\n3. Try to match to existing ticket by ticket number in subject (regex: `#(\\\\d+)`)\\n4. If match: add email as a note (public, visible to client)\\n5. If no match: create new ticket from email\\n6. Fire workflow triggers (`email_received`, `ticket_created`)\\n\\n**Outbound Flow:**\\n- Email templates stored in `email_templates` table\\n- Workflow action `send_email` renders template with ticket context\\n- Supports variable substitution: `{{ticket_number}}`, `{{company_name}}`, etc.\\n\\n**Key Methods:**\\n- `ProcessInboundEmail(ctx, tenantID, email)` \u2014 creates or updates ticket\\n- `SendEmail(ctx, tenantID, templateID, recipient, variables)` \u2014 renders and sends\\n\\n---\\n\\n### Configuration Taxonomy (`config_handler.go`, `config_seed.go`, `config_audit.go`)\\n\\nGeneric CRUD for 8 entity types with audit logging and system row protection.\\n\\n**Entity Types:**\\n| Entity | Table | Display Column | System Aware | Extra Fields |\\n|--------|-------|---|---|---|\\n| statuses | ticket_statuses | label | No | is_default, is_closed |\\n| priorities | ticket_priorities | label | No | is_default |\\n| teams | ticket_teams | name | No | \u2014 |\\n| types | ticket_types | name | No | \u2014 |\\n| categories | ticket_categories | name | No | parent_id, description |\\n| sources | ticket_sources | name | **Yes** | \u2014 |\\n| impacts | ticket_impacts | name | No | level |\\n| urgencies | ticket_urgencies | name | No | level |\\n\\n**System Rows:**\\n- `ticket_sources` has 7 system rows (tenant_id IS NULL): Portal, Email, Phone, API, RMM, M365, Entra\\n- System rows are read-only (slug/name cannot be edited, cannot be deleted)\\n- Color, icon, sort_order, is_active remain editable\\n\\n**Routes:**\\n- `GET /api/helpdesk/config/{entity}` \u2014 list all rows (system + tenant-scoped)\\n- `POST /api/helpdesk/config/{entity}` \u2014 create tenant-scoped row\\n- `PATCH /api/helpdesk/config/{entity}/{id}` \u2014 update row (respects system protection)\\n- `DELETE /api/helpdesk/config/{entity}/{id}` \u2014 delete tenant-scoped row only\\n- `POST /api/helpdesk/config/{entity}/reorder` \u2014 reorder by sort_order\\n\\n**Audit Logging:**\\nEvery mutation writes one row to `settings_audit_log`:\\n- `action`: create, update, delete, reorder\\n- `old_values`, `new_values`: JSONB snapshots of affected fields\\n- Reorder writes ONE row per entity whose sort_order actually changed (not one per entity in the list)\\n\\n**Seeding:**\\n- `SeedSystemSources()` \u2014 called at startup, inserts 7 system sources (idempotent)\\n- `SeedHelpdeskConfigDefaults()` \u2014 called on first config read for a tenant, inserts defaults for all 8 entity types (idempotent)\\n- Uses `ON CONFLICT (tenant_id, slug) DO NOTHING` to preserve existing rows\\n- Respects existing `is_default` rows (doesn't steal the active default)\\n\\n---\\n\\n### Orchestrator Integration (`orchestrator.go`)\\n\\nExposes helpdesk events and actions to the cross-module orchestrator engine.\\n\\n**Events Emitted:**\\n- `ticket_created` \u2014 new ticket from any source\\n- `ticket_updated` \u2014 any field change\\n- `ticket_closed` \u2014 status \u2192 closed/resolved\\n- `ticket_escalated` \u2014 priority increased or SLA warning\\n- `sla_breach` \u2014 response or resolution SLA exceeded\\n- `sla_warning` \u2014 ticket at 75% of threshold\\n- `customer_responded` \u2014 note added by external contact\\n- `first_response_sent` \u2014 first note added by internal user\\n\\n**Actions Exposed:**\\n- `set_status`, `set_priority`, `assign_to`, `assign_team`, `send_email`, `send_auto_reply`, `add_note`, `escalate`, `merge_tickets`, `create_child_ticket`, `start_timer`, `log_time`\\n\\n**Key Methods:**\\n- `EvaluateModuleEvent(ctx, event)` \u2014 receives event from orchestrator, evaluates helpdesk workflows\\n- `ModuleActions(eventType)` \u2014 returns action handlers for cross-module workflows\\n- `OrchestratorEvent(eventType, ticket)` \u2014 formats ticket as orchestrator event, emits to orchestrator\\n\\n---\\n\\n### Capabilities Registry (`capabilities.go`)\\n\\nSingle source of truth for orchestrator capabilities. Describes all events, actions, conditions, and entity types the system supports.\\n\\n**Structure:**\\n```go\\nOrchestratorCapabilities {\\n  Modules []ModuleCapability      // 11 modules (CRM, Assessment, Billing, Legal, Helpdesk, Security, Infrastructure, Onboarding, Nexie, Bidding, Survey, WorkOrder, Project, Procurement)\\n  Events []EventDefinition        // 100+ events across all modules\\n  Actions []ActionDefinition      // 80+ actions with config schemas\\n  Conditions []ConditionField     // 50+ fields for condition matching\\n  Presets []PresetDefinition      // workflow templates\\n}\\n```\\n\\n**Used By:**\\n- UI reads it to populate workflow builder dropdowns\\n- Engine validates actions/events against it before execution\\n- Exposed via `GET /api/orchestrator/capabilities` for frontend consumption\\n\\n---\\n\\n## Data Flow Examples\\n\\n### Creating a Ticket from Email\\n\\n```\\n1. Email gateway sends POST /api/email/inbound\\n2. handleInboundEmail() \u2192 ProcessInboundEmail()\\n3. Try to match ticket number in subject\\n4. If no match:\\n   a. Create new ticket (status=new, source=email)\\n   b. Fire workflow triggers: ticket_created, email_received\\n   c. WorkflowEngine evaluates matching workflows\\n   d. Actions execute (e.g., assign_team, send_auto_reply)\\n5. If match:\\n   a. Add email as public note\\n   b. Fire workflow trigger: customer_responded\\n   c. Update ticket updated_at (may trigger SLA re-check)\\n```\\n\\n### SLA Breach Detection\\n\\n```\\n1. SLAEngine.StartBreachChecker() runs every 30 seconds\\n2. For each open ticket:\\n   a. Load SLA policy (by type, priority, source)\\n   b. Calculate time since created_at (response) and updated_at (resolution)\\n   c. Compare against policy thresholds\\n   d. If breached: emit sla_breach event\\n   e. If at 75%: emit sla_warning event\\n3. OrchestratorEngine receives event\\n4. Evaluates workflows with trigger=sla_breach\\n5. Executes actions (escalate, send_notification, etc.)\\n```\\n\\n### AI-Powered Resolution\\n\\n```\\n1. Agent clicks \\\"AI Resolve\\\" on ticket detail\\n2. handleAIResolve() collects:\\n   a. Ticket title, description, notes\\n   b. Session recordings (if available)\\n   c. Agent OS context\\n3. Calls AIAssistant.GenerateResolution()\\n4. Returns resolution text + estimated cost\\n5. Agent reviews and clicks \\\"Approve\\\"\\n6. handleApproveResolution():\\n   a. Creates internal note with resolution\\n   b. Logs time entry (from AI estimate)\\n   c. Sets status=resolved\\n   d. Fires ticket_closed workflow\\n   e. Emits orchestrator event\\n```\\n\\n### Cross-Module Workflow: Ticket \u2192 Invoice\\n\\n```\\n1. Ticket created with billable time entries\\n2. Workflow trigger: ticket_closed\\n3. Condition: status=resolved AND has_time_entries=true\\n4. Action: generate_invoice_from_ticket\\n5. WorkflowEngine calls ModuleActions[\\\"generate_invoice_from_ticket\\\"]\\n6. Handler:\\n   a. Collects all billable time entries\\n   b. Looks up role rates for each entry\\n   c. Creates invoice in billing module\\n   d. Emits orchestrator event: invoice_created\\n7. Billing module receives event\\n8. Evaluates its own workflows (e.g., send_invoice)\\n```\\n\\n---\\n\\n## Integration Points\\n\\n### With Billing Module\\n- `generate_invoice_from_ticket` \u2014 creates invoice from ticket time entries\\n- `generate_invoice_from_wo` \u2014 creates invoice from work order\\n- Ticket charges (expenses) feed into invoicing\\n\\n### With CRM Module\\n- `advance_deal_stage` \u2014 moves deal forward when ticket resolves\\n- `create_assessment` \u2014 initiates discovery from ticket context\\n- Ticket linked to company/contact for context\\n\\n### With RMM Module\\n- `run_agent_command` \u2014 executes script on agent (requires approval)\\n- `block_ip` \u2014 blocks IP via FortiGate firewall\\n- Session recordings attached to tickets for AI context\\n\\n### With Orchestrator Engine\\n- Helpdesk emits events (ticket_created, sla_breach, etc.)\\n- Helpdesk receives cross-module actions (create_assessment, generate_invoice, etc.)\\n- Capabilities registry describes all helpdesk events/actions\\n\\n### With AI Module\\n- `AIAssistant` interface for ticket intelligence\\n- Classify, suggest responses, summarize, generate solutions\\n- Validate scripts before execution\\n- Session context for AI Resolve\\n\\n---\\n\\n## Configuration &amp; Customization\\n\\n### Ticket Prefix &amp; Padding\\n```\\ntenant_settings:\\n  ticket_prefix = \\\"TKT-\\\"\\n  ticket_zero_padding = 5\\n  \\nResult: TKT-00042\\n```\\n\\n### SLA Policies\\nDefine response/resolution times per ticket type, priority, and source. Escalation policies trigger automatic actions when SLAs are at risk.\\n\\n### Email Templates\\nStore reusable email templates with variable substitution. Used by `send_email` workflow action and auto-reply on ticket creation.\\n\\n### Workflow Automation\\nDefine triggers, conditions, and actions. Conditions support AND/OR/NOT logic. Actions can be helpdesk-specific or cross-module.\\n\\n### Configuration Taxonomy\\nCustomize statuses, priorities, teams, types, categories, impacts, and urgencies. System rows (sources) are read-only but color/icon/sort_order are editable.\\n\\n---\\n\\n## Error Handling &amp; Resilience\\n\\n**Audit Log Failures**  \\nAudit writes are best-effort; failures are logged but don't fail the parent mutation. The audit log is a tamper-evident trail, not a transaction guarantee.\\n\\n**Workflow Action Failures**  \\nIf an action fails, the workflow stops and logs the error. Subsequent actions don't execute. The ticket state is left as-is (partial update).\\n\\n**SLA Checker Failures**  \\nIf a single ticket's SLA check fails, the checker logs the error and continues with the next ticket. A wedged ticket doesn't block the entire checker.\\n\\n**Email Processing Failures**  \\nIf inbound email processing fails, the webhook returns 500. The email gateway should retry. Failures are logged for manual investigation.\\n\\n---\\n\\n## Performance Considerations\\n\\n**Ticket List Queries**  \\nUse pagination (limit/offset) and indexed filters (status, priority, source, assigned_to). Avoid N+1 queries by pre-fetching notes and time entries.\\n\\n**SLA Checker**  \\nRuns every 30 seconds on all open tickets. For large tenants (10k+ tickets), consider increasing the interval or sharding by company.\\n\\n**Workflow Evaluation**  \\nWorkflows are evaluated synchronously on ticket creation/update. For high-volume tenants, consider async evaluation via a job queue.\\n\\n**Email Processing**  \\nInbound email processing is synchronous. For high-volume email, consider async processing via a message queue.\\n\\n**Configuration Seeding**  \\nUses advisory locks to prevent concurrent seed races. Idempotent (ON CONFLICT DO NOTHING), so safe to call repeatedly.\\n\\n---\\n\\n## Testing &amp; Debugging\\n\\n**Workflow Execution Trace**  \\nEnable debug logging to see condition evaluation and action execution. Logs include field values, operator results, and action outcomes.\\n\\n**SLA Calculation**  \\nUse `GET /api/tickets/{id}` to inspect `sla_status`, `sla_response_remaining`, `sla_resolution_remaining`.\\n\\n**Email Thread Matching**  \\nTicket number regex: `#(\\\\d+)`. Test with subjects like `[#1042] Re: Network outage` or `Ticket #1042: Follow-up`.\\n\\n**Audit Log Queries**  \\n```sql\\nSELECT * FROM settings_audit_log \\nWHERE entity_type = 'ticket_statuses' \\n  AND action = 'update' \\nORDER BY created_at DESC;\\n```\\n\\n---\\n\\n## Future Enhancements\\n\\n- **Async Workflow Evaluation** \u2014 move to job queue for high-volume tenants\\n- **Ticket Merging** \u2014 combine duplicate tickets, consolidate notes and time\\n- **Bulk Actions** \u2014 update multiple tickets at once (status, priority, assignment)\\n- **Advanced Reporting** \u2014 SLA compliance, MTTR, resolution rate by team\\n- **Knowledge Base Integration** \u2014 link resolved tickets to KB articles\\n- **Sentiment Analysis** \u2014 detect customer frustration in notes/emails\\n- **Predictive Escalation** \u2014 ML-based escalation before SLA breach\",\"internal-hudu\":\"# internal \u2014 hudu\\n\\n# Hudu Integration Module\\n\\nThe `internal/hudu` module provides bidirectional synchronization between NexusOS and Hudu, an IT documentation platform. It handles syncing companies, assets, articles, and passwords while maintaining sync status tracking.\\n\\n## Overview\\n\\nHudu serves as a centralized knowledge base and asset registry. This module:\\n\\n- **Pushes** NexusOS companies and RMM agents (as assets) to Hudu\\n- **Pulls** KB articles from Hudu for display in ticket views\\n- **Manages** passwords and custom fields in Hudu\\n- **Tracks** sync status per entity to avoid duplicates and handle errors\\n\\nThe module is organized into three files:\\n- `client.go` \u2014 Low-level HTTP API client\\n- `handler.go` \u2014 HTTP request handlers and sync orchestration\\n- `types.go` \u2014 Data structures and configuration\\n\\n## Client API\\n\\nThe `Client` type wraps HTTP communication with Hudu's REST API. All requests are authenticated via the `x-api-key` header and use a 30-second timeout.\\n\\n### Core Method: `do()`\\n\\n```go\\nfunc (c *Client) do(method, path string, body interface{}) ([]byte, int, error)\\n```\\n\\nThe `do()` method is the foundation for all API calls. It:\\n1. JSON-marshals the body (if provided)\\n2. Constructs an HTTP request with auth headers\\n3. Executes the request\\n4. Returns raw response bytes, HTTP status, and any error\\n\\nAll higher-level methods use `do()` and check for success status codes (typically 200, 201).\\n\\n### Companies\\n\\n**`ListCompanies()`** \u2014 Fetches all companies from Hudu. Used during sync to match NexusOS companies by name.\\n\\n**`CreateCompany(company HuduCompany)`** \u2014 Creates a new company. Returns the created company with its Hudu ID assigned.\\n\\n**`UpdateCompany(id int, company HuduCompany)`** \u2014 Updates an existing company by ID. Used when a NexusOS company already exists in Hudu.\\n\\n### Articles (Knowledge Base)\\n\\n**`SearchArticles(query string, limit int)`** \u2014 Performs a fuzzy-match search against article titles and content. The Hudu API paginates at 25 results by default; this method caps results client-side (default limit 5). Used by the ticket brief view to surface relevant KB articles.\\n\\n**`CreateArticle(article HuduArticle)`** \u2014 Creates a KB article.\\n\\n**`UpdateArticle(id int, article HuduArticle)`** \u2014 Updates an existing article.\\n\\n### Assets\\n\\n**`CreateAsset(companyID int, asset HuduAsset)`** \u2014 Creates an asset under a company. RMM agents are synced as assets with OS type, version, IP, and MAC address stored in custom fields.\\n\\n**`ListAssets(companyID int, layoutIDs ...int)`** \u2014 Lists assets for a company with optional filtering by asset layout ID. Handles pagination automatically (100 per page) and walks all pages until empty. If multiple layout IDs are provided, filters client-side.\\n\\n### Passwords\\n\\n**`ListPasswords(companyID int)`** \u2014 Lists all password records for a company. Note: passwords are masked in list responses.\\n\\n**`GetPassword(passwordID int)`** \u2014 Fetches a single password with the unmasked value. Call this after `ListPasswords()` to retrieve actual secrets.\\n\\n**`CreatePassword(companyID int, pw HuduPassword)`** \u2014 Creates a password record.\\n\\n### Connection Testing\\n\\n**`TestConnection()`** \u2014 Verifies the API key by attempting to list companies with a page size of 1. Returns authentication errors (401/403) or unexpected status codes.\\n\\n## Handler &amp; Sync Orchestration\\n\\nThe `Handler` type manages HTTP endpoints and coordinates sync operations. It holds a database connection pool and UI renderer.\\n\\n### Endpoints\\n\\n```\\nPOST /api/hudu/test-connection    \u2014 Verify API credentials\\nPOST /api/hudu/sync/companies     \u2014 Sync NexusOS companies to Hudu\\nPOST /api/hudu/sync/assets        \u2014 Sync RMM agents as assets to Hudu\\nGET  /api/hudu/sync/status        \u2014 Retrieve sync status summary\\n```\\n\\n### Configuration Loading\\n\\n**`getConfig(ctx, tenantID)`** \u2014 Loads Hudu settings from the `tenants` table:\\n- Provider type (must be \\\"hudu\\\")\\n- API URL and key\\n- Enabled flag\\n\\nThe method normalizes the API URL by stripping trailing `/api/v1` or `/` to avoid double-pathing.\\n\\n### Company Sync\\n\\n**`syncCompanies(ctx, tenantID, client)`** \u2014 Pushes NexusOS companies to Hudu:\\n\\n1. Fetches all Hudu companies and builds a name-based lookup map (case-insensitive)\\n2. Queries active NexusOS companies from the database\\n3. For each company:\\n   - If a Hudu company with the same name exists, updates it\\n   - Otherwise, creates a new company in Hudu\\n4. Records sync status in `hudu_sync_status` table\\n5. Returns a `SyncResult` with counts of created, updated, and error records\\n\\n### Asset Sync\\n\\n**`syncAssets(ctx, tenantID, client)`** \u2014 Pushes RMM agents as assets to Hudu:\\n\\n1. Loads the company-to-Hudu ID mapping from `hudu_sync_status`\\n2. Queries active agents from the database\\n3. For each agent:\\n   - Looks up its company's Hudu ID\\n   - Skips if the company hasn't been synced to Hudu\\n   - Creates an asset in Hudu with OS type, version, IP, and MAC address in custom fields\\n4. Records sync status\\n5. Returns a `SyncResult`\\n\\n### Sync Status Tracking\\n\\n**`upsertSyncRecord(ctx, tenantID, entityType, entityID, huduID, status, errMsg)`** \u2014 Inserts or updates a record in `hudu_sync_status`. This table tracks:\\n- Which NexusOS entity (company, asset, etc.) maps to which Hudu ID\\n- Current sync status (pending, synced, error, skipped)\\n- Last sync timestamp\\n- Error messages for failed syncs\\n\\n**`handleSyncStatus()`** \u2014 Returns aggregated sync statistics grouped by entity type:\\n- Total count\\n- Synced count\\n- Error count\\n- Last sync timestamp\\n\\n## Data Types\\n\\n### Config\\n\\nHolds tenant-level Hudu settings loaded from the database.\\n\\n### SyncResult\\n\\nSummarizes the outcome of a sync operation:\\n- `Created`, `Updated`, `Skipped`, `Errors` \u2014 counts\\n- `Messages` \u2014 detailed error or status messages\\n\\n### HuduCompany\\n\\nMaps to Hudu's company object. Fields include name, phone, website, address components, and notes.\\n\\n### HuduArticle\\n\\nRepresents a KB article. The `URL` field is populated by Hudu on reads and points to the article's web view\u2014used as a click-through link in the ticket brief.\\n\\n### HuduAsset\\n\\nRepresents an asset in Hudu. Custom fields are stored in a `Fields` map. RMM agents are synced as assets with OS and network information.\\n\\n### HuduPassword\\n\\nRepresents a password record. The `Password` field is masked in list responses; use `GetPassword()` to retrieve the actual value.\\n\\n## Integration Points\\n\\n### Incoming Dependencies\\n\\n- **`internal/auth`** \u2014 `TenantIDFromContext()` extracts the tenant ID from request context\\n- **`internal/ui`** \u2014 `Renderer` is passed to the handler (currently unused in this module)\\n- **`pgx`** \u2014 Database queries for companies, agents, and sync status\\n\\n### Outgoing Dependencies\\n\\n- **`internal/helpdesk`** \u2014 `newHuduClient()` in `peek_hudu.go` creates a client to search KB articles for ticket views\\n- **`cmd/psa`** \u2014 Main entry point registers the handler's routes\\n\\n## Sync Flow Diagram\\n\\n```mermaid\\ngraph LR\\n    A[\\\"NexusOS DBcompanies, agents\\\"] --&gt;|Query| B[\\\"HandlersyncCompaniessyncAssets\\\"]\\n    B --&gt;|Create/Update| C[\\\"Hudu APIClient\\\"]\\n    C --&gt;|HTTP| D[\\\"Hudu Instance\\\"]\\n    D --&gt;|Response| C\\n    C --&gt;|Status| E[\\\"hudu_sync_statusTable\\\"]\\n    B --&gt;|Upsert| E\\n```\\n\\n## Error Handling\\n\\nAll sync operations are resilient:\\n- Individual entity failures are logged and counted but don't stop the sync\\n- Errors are recorded in `hudu_sync_status` with error messages\\n- HTTP errors (non-2xx status) are wrapped with context\\n- JSON parsing errors are caught and reported\\n\\n## Usage Example\\n\\n```go\\n// In a request handler:\\ntenantID := auth.TenantIDFromContext(r.Context())\\ncfg, err := h.getConfig(r.Context(), tenantID)\\nif err != nil {\\n    // Handle config error\\n}\\n\\nclient := NewClient(cfg.APIURL, cfg.APIKey)\\nresult, err := h.syncCompanies(r.Context(), tenantID, client)\\nif err != nil {\\n    // Handle sync error\\n}\\n\\n// result.Created, result.Updated, result.Errors, result.Messages\\n```\\n\\n## Key Design Decisions\\n\\n1. **Name-based company matching** \u2014 Companies are matched by name (case-insensitive) rather than by external ID, since NexusOS companies may not have pre-existing Hudu IDs.\\n\\n2. **Client-side pagination** \u2014 `ListAssets()` and `ListPasswords()` walk all pages automatically, simplifying caller code.\\n\\n3. **Sync status table** \u2014 Maintains a persistent record of which NexusOS entities map to Hudu IDs, enabling idempotent updates and error recovery.\\n\\n4. **Skipped assets** \u2014 Agents whose companies haven't been synced to Hudu are skipped rather than created as orphans.\\n\\n5. **Custom fields for agent data** \u2014 RMM agent metadata (OS, IP, MAC) is stored in Hudu's custom fields map rather than as top-level asset properties.\",\"internal-industry\":\"# internal \u2014 industry\\n\\n# Industry Intelligence Module\\n\\nThe `internal/industry` module provides read-only reference data about US industries, mapping each to regulatory requirements, compliance frameworks, technology standards, and risk profiles. All data is seeded from authoritative sources and is not user-generated.\\n\\n## Overview\\n\\nThis module serves two primary functions:\\n\\n1. **Web UI** \u2014 Display industry intelligence through HTML pages and HTMX-driven interactions\\n2. **API** \u2014 Expose industry data as JSON for programmatic access and AI system integration\\n3. **Company Assignment** \u2014 Allow companies to be tagged with an industry classification\\n\\nThe module is tenant-aware (all queries filter by tenant context) and read-only for end users. Industry data itself is managed through database seeding, not through application APIs.\\n\\n## Core Components\\n\\n### Handler\\n\\n`Handler` is the main entry point. It manages HTTP routing, database queries, and response rendering.\\n\\n```go\\ntype Handler struct {\\n\\tpool     *pgxpool.Pool\\n\\trenderer *ui.Renderer\\n}\\n```\\n\\nCreate a handler with `NewHandler(pool, renderer)` and register its routes with `RegisterRoutes(mux)`.\\n\\n### Data Types\\n\\n**Industry** \u2014 The base industry record:\\n- `ID`, `Slug` \u2014 Unique identifiers\\n- `Name` \u2014 Display name (e.g., \\\"Healthcare\\\")\\n- `NAICSPrefix` \u2014 NAICS code prefix for classification\\n- `Description` \u2014 Overview of the industry\\n- `RegulatoryBody`, `RegulatoryURL` \u2014 Primary regulator and link\\n- `RiskTier` \u2014 Categorical risk level (e.g., \\\"high\\\", \\\"medium\\\", \\\"low\\\")\\n- `SortOrder` \u2014 Display ordering\\n\\n**ComplianceRequirement** \u2014 Maps a compliance framework to an industry:\\n- `FrameworkSlug`, `FrameworkName` \u2014 Framework identifier and name (e.g., \\\"hipaa\\\", \\\"HIPAA\\\")\\n- `RequirementLevel` \u2014 \\\"mandatory\\\" or \\\"recommended\\\"\\n- `ConditionNote` \u2014 Contextual notes (e.g., \\\"if handling PHI\\\")\\n- `RegulatoryCitation` \u2014 Regulatory reference (e.g., \\\"45 CFR \u00a7164.308\\\")\\n- `PenaltyInfo` \u2014 Enforcement consequences\\n- `AuthorityName`, `AuthorityURL` \u2014 Issuing authority\\n\\n**TechnologyStandard** \u2014 Baseline technology requirements:\\n- `Category` \u2014 Domain (e.g., \\\"encryption\\\", \\\"access-control\\\")\\n- `Requirement` \u2014 Specific requirement text\\n- `ProductCategory` \u2014 Type of product needed (e.g., \\\"firewall\\\", \\\"MFA\\\")\\n- `Priority` \u2014 \\\"critical\\\", \\\"high\\\", \\\"medium\\\", \\\"low\\\"\\n- `Rationale` \u2014 Why this standard applies\\n\\n**RiskFactor** \u2014 Industry-specific risk dimensions:\\n- `RiskCategory` \u2014 Type of risk (e.g., \\\"data-breach\\\", \\\"ransomware\\\")\\n- `RiskLevel` \u2014 Severity (\\\"critical\\\", \\\"high\\\", \\\"medium\\\", \\\"low\\\")\\n- `Description` \u2014 Risk explanation\\n- `MitigationNote` \u2014 Suggested controls\\n\\n**BusinessType** \u2014 Common company types within an industry:\\n- `Name` \u2014 Business type name (e.g., \\\"Hospital\\\", \\\"Clinic\\\")\\n- `Description` \u2014 What this type does\\n- `NAICSCode` \u2014 Specific NAICS code\\n\\n**IndustryDetail** \u2014 Composite type combining an industry with all related data:\\n```go\\ntype IndustryDetail struct {\\n\\tIndustry\\n\\tComplianceRequirements []ComplianceRequirement\\n\\tTechnologyStandards    []TechnologyStandard\\n\\tRiskFactors            []RiskFactor\\n\\tBusinessTypes          []BusinessType\\n}\\n```\\n\\n## Routes\\n\\n### Web UI Routes\\n\\n| Method | Path | Handler | Purpose |\\n|--------|------|---------|---------|\\n| GET | `/industries` | `handleList` | List all active industries with compliance/tech/risk counts |\\n| GET | `/industries/{slug}` | `handleDetail` | Display full industry detail page |\\n\\nBoth routes support HTMX requests (check `HX-Request` header) and render either full HTML or partial fragments.\\n\\n### API Routes\\n\\n| Method | Path | Handler | Purpose |\\n|--------|------|---------|---------|\\n| GET | `/api/industries` | `handleAPIList` | JSON list of all active industries |\\n| GET | `/api/industries/{slug}` | `handleAPIDetail` | JSON detail for a single industry |\\n| PUT | `/api/companies/{id}/industry` | `handleAssignIndustry` | Assign an industry to a company |\\n\\n### Company Assignment\\n\\n`handleAssignIndustry` accepts a JSON body:\\n```json\\n{\\n  \\\"industry_id\\\": \\\"uuid-string\\\"\\n}\\n```\\n\\nIt updates the `companies.industry_id` column for the authenticated tenant's company. Passing an empty string clears the assignment (via `NULLIF`).\\n\\n## Key Functions\\n\\n### loadIndustryDetail(r, slug) \u2192 *IndustryDetail, error\\n\\nLoads a complete industry record and all related data in a single logical operation. Executes five queries:\\n\\n1. Base industry record (by slug)\\n2. Compliance requirements (ordered by `sort_order`)\\n3. Technology standards (ordered by category, then priority)\\n4. Risk factors (ordered by risk category)\\n5. Business types (ordered by `sort_order`)\\n\\nReturns `nil, error` if the industry is not found or inactive.\\n\\n### GetIndustryContextForAI(r, slug) \u2192 string\\n\\nFormats industry detail as a text block suitable for AI system prompts. Used by the assessment analyzer to provide authoritative context when evaluating compliance posture.\\n\\nOutput format:\\n```\\nINDUSTRY CONTEXT:\\nIndustry: Healthcare (NAICS 62)\\nRisk Tier: high\\nRegulatory Body: HHS\\n\\nCOMPLIANCE REQUIREMENTS:\\n- [mandatory] HIPAA (45 CFR \u00a7164.308) \u2014 Penalty: ... \u2014 Note: ...\\n\\nTECHNOLOGY STANDARDS:\\n- [critical] Encrypt all data at rest (encryption) \u2014 ...\\n\\nRISK FACTORS:\\n- data-breach: high \u2014 ...\\n\\nCOMMON BUSINESS TYPES IN THIS INDUSTRY:\\n- Hospital \u2014 ...\\n```\\n\\n### GetAllBusinessTypesForAI(r) \u2192 string\\n\\nReturns a formatted reference of all business types across all active industries, grouped by industry. Used by Nexie (the AI assistant) to auto-detect a client's industry from document content.\\n\\nOutput format:\\n```\\nINDUSTRY CLASSIFICATION REFERENCE (use to auto-detect client industry):\\n\\nHealthcare [healthcare]:\\n  - Hospital\\n  - Clinic\\n  - ...\\n\\nFinance [finance]:\\n  - Bank\\n  - ...\\n```\\n\\n## Data Flow\\n\\n```mermaid\\ngraph TD\\n    A[\\\"HTTP Request/industries or /api/industries\\\"] --&gt; B{Route Handler}\\n    B --&gt;|GET /industries| C[\\\"handleList\\\"]\\n    B --&gt;|\\\"GET /industries/{slug}\\\"| D[\\\"handleDetail\\\"]\\n    B --&gt;|GET /api/industries| E[\\\"handleAPIList\\\"]\\n    B --&gt;|\\\"GET /api/industries/{slug}\\\"| F[\\\"handleAPIDetail\\\"]\\n    B --&gt;|\\\"PUT /api/companies/{id}/industry\\\"| G[\\\"handleAssignIndustry\\\"]\\n    \\n    C --&gt; H[\\\"Query industries+ count aggregates\\\"]\\n    D --&gt; I[\\\"loadIndustryDetail\\\"]\\n    E --&gt; J[\\\"Query industries\\\"]\\n    F --&gt; I\\n    G --&gt; K[\\\"UPDATE companiesSET industry_id\\\"]\\n    \\n    I --&gt; L[\\\"Query compliancerequirements\\\"]\\n    I --&gt; M[\\\"Query techstandards\\\"]\\n    I --&gt; N[\\\"Query riskfactors\\\"]\\n    I --&gt; O[\\\"Query businesstypes\\\"]\\n    \\n    H --&gt; P[\\\"Render HTMLor JSON\\\"]\\n    I --&gt; P\\n    J --&gt; P\\n    K --&gt; P\\n```\\n\\n## Tenant Isolation\\n\\nAll handlers extract the tenant ID from the request context via `auth.TenantIDFromContext()`. The `handleAssignIndustry` endpoint uses this to ensure a company can only be updated if it belongs to the authenticated tenant:\\n\\n```go\\nWHERE id = $2 AND tenant_id = $3\\n```\\n\\nIndustry reference data itself is global (not tenant-specific), but company assignments are tenant-scoped.\\n\\n## Integration Points\\n\\n### With Assessment Module\\n\\nThe assessment analyzer calls `GetIndustryContextForAI()` to inject industry-specific compliance and risk context into AI prompts when evaluating a company's security posture.\\n\\n### With Company Module\\n\\nCompanies have an optional `industry_id` foreign key. The industry assignment endpoint (`PUT /api/companies/{id}/industry`) populates this field, enabling downstream filtering and context-aware recommendations.\\n\\n### With UI Renderer\\n\\nBoth web handlers use `renderer.Render()` to produce HTML. The renderer checks the `HX-Request` header to determine whether to return a full page or a partial fragment for HTMX swaps.\\n\\n## Database Schema (Implied)\\n\\nThe module assumes these tables:\\n\\n- `industries` \u2014 Base industry records (id, name, slug, naics_prefix, description, regulatory_body, regulatory_url, risk_tier, sort_order, is_active)\\n- `industry_compliance_requirements` \u2014 Maps frameworks to industries (id, industry_id, framework_slug, framework_name, requirement_level, condition_note, regulatory_citation, penalty_info, authority_name, authority_url, sort_order)\\n- `industry_technology_standards` \u2014 Tech baselines (id, industry_id, category, requirement, product_category, priority, rationale)\\n- `industry_risk_factors` \u2014 Risk dimensions (id, industry_id, risk_category, risk_level, description, mitigation_note)\\n- `industry_business_types` \u2014 Common company types (id, industry_id, name, description, naics_code, sort_order)\\n- `companies` \u2014 Has `industry_id` column (uuid, nullable)\\n\\n## Error Handling\\n\\nHandlers return HTTP error responses for:\\n- **400 Bad Request** \u2014 Invalid JSON body in `handleAssignIndustry`\\n- **404 Not Found** \u2014 Industry slug not found or inactive\\n- **500 Internal Server Error** \u2014 Database query or update failures\\n\\nErrors are logged to stdout (via `log.Printf`) for debugging.\\n\\n## Performance Considerations\\n\\n- `handleList` executes N+4 queries (one per industry for counts). Consider caching or denormalizing counts if the industry list grows large.\\n- `loadIndustryDetail` executes 5 queries sequentially. These could be parallelized if latency becomes an issue.\\n- All queries filter by `is_active = true`, ensuring inactive industries are never exposed.\",\"internal-infra\":\"# internal \u2014 infra\\n\\n# Internal \u2014 Infra Module\\n\\n## Overview\\n\\nThe `infra` module is a shared infrastructure verification engine that maintains a device registry, performs connectivity testing, and records an immutable audit trail. It serves as the authoritative source for infrastructure state across NexusOS, consumed by CMMC, Onboarding, SIEM, RMM, Metasploit, Helpdesk, and NexusPulse modules.\\n\\nThe module's core responsibilities are:\\n\\n- **Device registry**: CRUD operations on infrastructure devices (servers, firewalls, workstations, etc.)\\n- **Connectivity verification**: Protocol-specific reachability probes (HTTP/HTTPS, SSH, LDAP, SNMP, TCP)\\n- **Data ingestion**: Sync from Hudu (documentation platform) and CSV runbook imports\\n- **Audit trail**: Immutable append-only logging of all infrastructure changes and verification events\\n- **Multi-tenant isolation**: All operations scoped to tenant and company IDs\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    Handler[\\\"Handler(HTTP routes)\\\"]\\n    Device[\\\"Device Registry(infra_devices table)\\\"]\\n    Verify[\\\"Verification Engine(probe functions)\\\"]\\n    Audit[\\\"Audit Log(infra_audit_log table)\\\"]\\n    Hudu[\\\"Hudu Sync(external API)\\\"]\\n    Runbook[\\\"Runbook Import(CSV parser)\\\"]\\n    \\n    Handler --&gt;|CRUD| Device\\n    Handler --&gt;|verify-all/verify-one| Verify\\n    Verify --&gt;|record result| Device\\n    Verify --&gt;|log action| Audit\\n    Handler --&gt;|sync| Hudu\\n    Hudu --&gt;|upsert devices| Device\\n    Handler --&gt;|import| Runbook\\n    Runbook --&gt;|insert devices| Device\\n    Handler --&gt;|all actions| Audit\\n```\\n\\n## Core Components\\n\\n### Handler\\n\\n`Handler` is the HTTP entry point. It wires all routes and coordinates between the database, verification engine, and external integrations.\\n\\n**Key methods:**\\n\\n- **Page routes**: `handleSelectCompany`, `handleDashboard`, `handleReport` \u2014 render HTML UI\\n- **Device CRUD**: `handleCreateDevice`, `handleUpdateDevice`, `handleDeleteDevice`\\n- **Verification**: `handleSweep` (verify all devices), `handleVerifyOne` (single device)\\n- **Integrations**: `handleSyncHudu`, `handleImportRunbook`\\n- **Public API**: `ListDevices`, `Pool` \u2014 exported for sibling modules\\n\\n**Initialization:**\\n\\n```go\\nh := NewHandler(pool, renderer)\\nh.RegisterRoutes(mux)\\n```\\n\\nThe handler extracts tenant and user context from the request using `auth.TenantIDFromContext` and `auth.UserIDFromContext`, ensuring all operations are scoped to the authenticated tenant.\\n\\n### Device Registry\\n\\nThe `Device` struct represents a single piece of infrastructure:\\n\\n```go\\ntype Device struct {\\n    ID, TenantID, CompanyID string\\n    Name, DeviceType, Hostname, IPAddress string\\n    Manufacturer, Model, Firmware string\\n    ManagementURL, Location string\\n    \\n    // Verification state\\n    Verified bool\\n    VerifyStatus, VerifyProtocol string\\n    VerifyLatency int\\n    VerifyError string\\n    \\n    // External references\\n    HuduAssetID, HuduLayoutID, HuduPasswordID int\\n    Source string  // \\\"manual\\\", \\\"hudu\\\", or \\\"runbook\\\"\\n}\\n```\\n\\nDevices are stored in `infra_devices` table. The `Source` field tracks origin (manual entry, Hudu sync, or CSV import) for audit purposes. Deletion is a soft-delete: the `status` column is set to `'decommissioned'` rather than removing the row.\\n\\n**Device types** (from `DefaultProtocol`):\\n- `firewall`, `vpn`, `email_gateway`, `backup`, `cloud_service` \u2192 HTTPS\\n- `server` \u2192 SSH\\n- `switch`, `ap` \u2192 HTTPS\\n- `domain_controller` \u2192 LDAP\\n- `workstation` \u2192 RMM (agent-driven)\\n- `printer` \u2192 SNMP\\n\\n### Verification Engine\\n\\n`VerifyDevice` tests connectivity to a single device and records the result:\\n\\n```go\\nv, err := VerifyDevice(ctx, pool, dev, triggeredBy, userID)\\n```\\n\\n**Flow:**\\n\\n1. Determine protocol (from device config or default)\\n2. Run protocol-specific probe (TCP, HTTP, UDP, etc.)\\n3. Record result to `infra_verifications` table\\n4. Update device's latest-state cache in `infra_devices`\\n5. Log audit entry\\n\\n**Probe functions** (`probe`, `probeHTTP`, `probeTCP`, `probeUDP`):\\n\\n- **HTTP/HTTPS**: Full HTTP GET request with TLS verification disabled (for self-signed certs). Returns HTTP status and Server header.\\n- **TCP**: Simple connection attempt to a port (SSH=22, LDAP=389).\\n- **UDP**: Returns \\\"pending\\\" \u2014 UDP has no real reachability signal.\\n- **RMM**: Returns \\\"pending\\\" \u2014 verification is agent-driven, not LAN-based.\\n\\n**Status values:**\\n- `verified` \u2014 device is reachable\\n- `auth_failed` \u2014 credentials rejected (future: when credential probes are added)\\n- `unreachable` \u2014 connection refused or timeout\\n- `protocol_error` \u2014 invalid URL, missing IP, etc.\\n- `timeout` \u2014 connection timed out\\n- `pending` \u2014 not yet checked (RMM, UDP)\\n\\n### Hudu Sync\\n\\n`SyncFromHudu` pulls assets and passwords from a Hudu company and upserts them into the device registry:\\n\\n```go\\nresult, err := SyncFromHudu(ctx, pool, client, tenantID, companyID, huduCompanyID, userID)\\n```\\n\\n**Process:**\\n\\n1. Fetch all assets from Hudu company\\n2. Map Hudu `asset_layout_id` to NexusOS `device_type` using `HuduLayoutToDeviceType`\\n3. Extract device metadata from Hudu custom fields (hostname, IP, management URL, etc.)\\n4. Match Hudu passwords to devices by URL/hostname/IP overlap\\n5. Upsert into `infra_devices` using `(tenant_id, hudu_asset_id)` as the unique key\\n6. On conflict, update metadata while preserving manually-entered fields (IP, hostname, management URL)\\n\\n**Field extraction** (`extractAssetFields`):\\n\\nHudu custom fields are keyed by lowercased label, but keys vary per layout. The function accepts multiple spellings:\\n- `hostname` / `host_name` / `name`\\n- `ip_address` / `lan_ip_address` / `wan_ip_address` / `ip`\\n- `management_url` / `admin_url` / `url`\\n- `manufacturer` / `vendor`\\n- `firmware` / `firmware_version` / `version` / `operating_system`\\n\\n**Password matching** (`matchPassword`):\\n\\nSearches Hudu passwords for a match against the device's IP, hostname, management URL, or name. Returns the password ID if found, 0 otherwise. Used to link credentials for later authentication probes.\\n\\n### Runbook Import\\n\\n`ImportRunbookCSV` parses a CSV file and inserts device rows:\\n\\n```go\\nresult, err := ImportRunbookCSV(ctx, pool, tenantID, companyID, userID, file)\\n```\\n\\n**Expected CSV columns** (case-insensitive):\\n- `device_name`, `device_type` (required)\\n- `ip_address`, `hostname`, `management_url`, `manufacturer`, `model`, `location`, `notes` (optional)\\n\\n**Behavior:**\\n\\n- Skips rows with missing name or type\\n- Inserts with `source='runbook'`\\n- Credentials are NOT stored here; they belong in Hudu or `device_credentials`\\n- Returns counts of created, errors, and error messages\\n\\n### Audit Trail\\n\\n`LogAudit` appends an immutable entry to `infra_audit_log`:\\n\\n```go\\nLogAudit(ctx, pool, AuditEntry{\\n    TenantID: tenantID,\\n    CompanyID: companyID,\\n    DeviceID: deviceID,\\n    UserID: userID,\\n    Action: \\\"device_added\\\",\\n    Details: `{\\\"name\\\":\\\"...\\\",\\\"type\\\":\\\"...\\\"}`,\\n    Module: \\\"infra\\\",\\n})\\n```\\n\\n**Key properties:**\\n\\n- **Append-only**: No UPDATE or DELETE \u2014 chain of custody for compliance\\n- **Non-blocking**: Errors are logged but never returned; audit failure must not break the caller's operation\\n- **Structured details**: JSON-encoded metadata for each action\\n- **Tenant-scoped**: All entries include `tenant_id` for multi-tenant isolation\\n\\n**Common actions:**\\n- `device_added`, `device_updated`, `device_removed`\\n- `device_verified`, `device_verify_failed`\\n- `sweep_started`, `sweep_completed`\\n- `hudu_synced`, `runbook_imported`\\n\\n## SSRF Protection\\n\\nThe module includes defense-in-depth SSRF guards for HTTP probes (`probeHTTP`):\\n\\n1. **Scheme allowlist**: Only `http` and `https` allowed\\n2. **Cloud-metadata hostname rejection**: Blocks well-known metadata endpoints (`169.254.169.254`, `metadata.google.internal`, etc.)\\n3. **Custom dialer** (`safeDialContext`): Resolves the target hostname and blocks the dial if the IP is:\\n   - Loopback (127.0.0.0/8, ::1)\\n   - Link-local (169.254.0.0/16, fe80::/10)\\n   - Multicast\\n   - Unspecified (0.0.0.0, ::)\\n   - Private RFC1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)\\n   - CGNAT (100.64.0.0/10)\\n   - IPv6 ULA (fc00::/7)\\n\\nThis defends against DNS rebinding and attackers who configure DNS to point a public hostname at 169.254.169.254 or 127.0.0.1.\\n\\n**Note:** This guard is intentionally strict. Device verification is meant to reach public management endpoints over the internet. If a future feature needs to probe server-internal addresses, add a separate dialer with an explicit allowlist \u2014 do not loosen this one.\\n\\n## HTTP Routes\\n\\n### Pages\\n\\n- `GET /infra` \u2014 Company selector with device counts\\n- `GET /infra/{company_id}` \u2014 Dashboard with device list grouped by type\\n- `GET /infra/{company_id}/report` \u2014 Printable verification report\\n\\n### Device CRUD\\n\\n- `POST /api/infra/{company_id}/devices` \u2014 Create device\\n- `PUT /api/infra/devices/{id}` \u2014 Update device\\n- `DELETE /api/infra/devices/{id}` \u2014 Soft-delete device (mark decommissioned)\\n\\n### Verification\\n\\n- `POST /api/infra/{company_id}/verify-all` \u2014 Verify all devices in company (sweep)\\n- `POST /api/infra/devices/{id}/verify` \u2014 Verify single device\\n- `GET /api/infra/{company_id}/status` \u2014 Get summary counts (JSON)\\n\\n### Integrations\\n\\n- `POST /api/infra/{company_id}/sync-hudu` \u2014 Sync devices from Hudu\\n- `POST /api/infra/{company_id}/import-runbook` \u2014 Import CSV runbook\\n\\n### Public API (for sibling modules)\\n\\n- `GET /api/infra/{company_id}/verified` \u2014 List verified devices\\n- `GET /api/infra/{company_id}/by-type/{type}` \u2014 List devices by type\\n\\n## Integration with Other Modules\\n\\n### Exported API\\n\\nSibling modules access the infra module via the `Handler`:\\n\\n```go\\n// From CMMC, Onboarding, etc.\\ndevices, err := infraHandler.ListDevices(ctx, tenantID, companyID)\\npool := infraHandler.Pool()  // For shared audit writes\\n```\\n\\n### Audit Logging\\n\\nOther modules can log to the infra audit trail:\\n\\n```go\\ninfra.LogAudit(ctx, pool, infra.AuditEntry{\\n    TenantID: tenantID,\\n    CompanyID: companyID,\\n    UserID: userID,\\n    Action: \\\"cmmc_control_activated\\\",\\n    Module: \\\"cmmc\\\",\\n})\\n```\\n\\n### Device Domain Mapping\\n\\nThe `DeviceDomainMap` maps device types to CMMC domains they provide evidence for:\\n\\n```go\\ndomains := DeviceDomainMap[\\\"firewall\\\"]  // [\\\"AC\\\", \\\"AU\\\", \\\"SC\\\", \\\"SI\\\"]\\n```\\n\\nUsed by the CMMC module to auto-tag devices on the Connect page.\\n\\n## Data Model\\n\\n### infra_devices\\n\\nCore device registry. Soft-deletes via `status='decommissioned'`.\\n\\n**Key columns:**\\n- `id, tenant_id, company_id` \u2014 identity\\n- `name, device_type, hostname, ip_address` \u2014 device metadata\\n- `manufacturer, model, firmware, management_url, location` \u2014 hardware details\\n- `source` \u2014 origin (manual, hudu, runbook)\\n- `hudu_asset_id, hudu_layout_id, hudu_password_id` \u2014 external references\\n- `verified, verified_at` \u2014 latest verification state\\n- `verify_status, verify_protocol, verify_latency, verify_details, verify_error` \u2014 probe results\\n- `status` \u2014 active or decommissioned\\n- `created_at, updated_at` \u2014 timestamps\\n\\n### infra_verifications\\n\\nImmutable history of all verification attempts.\\n\\n**Columns:**\\n- `id, tenant_id, device_id` \u2014 identity\\n- `status, protocol, latency_ms, details, error` \u2014 probe result\\n- `triggered_by, triggered_by_id` \u2014 who initiated the verification\\n- `created_at` \u2014 timestamp\\n\\n### infra_audit_log\\n\\nImmutable append-only audit trail.\\n\\n**Columns:**\\n- `tenant_id, company_id, device_id, user_id` \u2014 context\\n- `action` \u2014 what happened\\n- `details` \u2014 JSON metadata\\n- `module` \u2014 which module logged it\\n- `created_at` \u2014 timestamp\\n\\n## Error Handling\\n\\n- **Audit write failures**: Logged but never returned. Audit errors must not break the caller's main operation.\\n- **Verification failures**: Recorded as a status (unreachable, timeout, etc.) and returned to the caller. The device is updated with the failure state.\\n- **Database errors**: Returned to the HTTP handler, which responds with 500 or 400 as appropriate.\\n- **Hudu sync errors**: Returned as JSON error response (502 if Hudu API fails, 400 if config missing).\\n- **Runbook import errors**: Collected and returned as a list of messages; import continues on per-row errors.\\n\\n## Multi-Tenancy\\n\\nAll operations are scoped to `tenant_id`:\\n\\n- Device queries filter by `tenant_id` and `company_id`\\n- Audit entries include `tenant_id` for compliance\\n- Hudu sync and runbook import are tenant-aware\\n- HTTP handlers extract tenant from request context via `auth.TenantIDFromContext`\\n\\n## Testing Considerations\\n\\n- **Verification probes** are real network calls; mock or use test fixtures in unit tests\\n- **SSRF guard** should be tested with blocked IPs (loopback, link-local, private ranges)\\n- **Hudu sync** requires a mock Hudu client or test API\\n- **Audit trail** should be verified as append-only (no updates/deletes)\\n- **Multi-tenancy** should be tested with multiple tenant IDs to ensure isolation\",\"internal-itil\":\"# internal \u2014 itil\\n\\n# ITIL Module Documentation\\n\\n## Overview\\n\\nThe `internal/itil` module implements ITIL v4-aligned process management for NexusOS PSA. It provides four independent disciplines\u2014Problem Management, Change Management, CMDB, and Service Catalog\u2014as a standalone package that does not depend on the helpdesk module, though it can cross-reference helpdesk tickets via direct SQL joins.\\n\\nThis module is designed to support enterprise IT service management workflows with proper lifecycle tracking, approval workflows, and configuration tracking.\\n\\n## Architecture\\n\\nThe module is organized into three files:\\n\\n- **itil_types.go** \u2014 Data structures for all four disciplines\\n- **itil_store.go** \u2014 Database access layer (queries, inserts, updates)\\n- **itil_handler.go** \u2014 HTTP handlers for pages and API endpoints\\n\\nAll handlers extract tenant and user context from the request using `auth.TenantIDFromContext()` and `auth.UserIDFromContext()`, ensuring multi-tenant isolation. The module uses PostgreSQL with parameterized queries to prevent SQL injection.\\n\\n## Core Disciplines\\n\\n### Problem Management\\n\\nProblems represent root-cause investigations that may affect multiple incidents (helpdesk tickets).\\n\\n**Lifecycle:** `new` \u2192 `under_investigation` \u2192 `known_error` \u2192 `resolved` \u2192 `closed`\\n\\n**Key fields:**\\n- `ProblemNumber` \u2014 auto-incremented identifier\\n- `Status`, `Priority`, `Impact`, `Urgency` \u2014 severity classification\\n- `IsKnownError` \u2014 flag indicating a documented known issue\\n- `Workaround`, `RootCause`, `Resolution` \u2014 investigation findings\\n- `IncidentCount` \u2014 denormalized count of linked tickets\\n\\n**Operations:**\\n- `ListProblems()` \u2014 filtered list with status/priority facets\\n- `GetProblem()` \u2014 full detail with linked incidents\\n- `CreateProblem()` \u2014 new investigation record\\n- `UpdateProblem()` \u2014 patch status, findings, assignments\\n- `LinkIncident()` \u2014 associate a helpdesk ticket to the problem\\n- `ListProblemIncidents()` \u2014 retrieve all linked tickets\\n\\nThe problem-to-incident relationship is stored in the `problem_incidents` table, which joins to the helpdesk `tickets` table by ID without importing the helpdesk package.\\n\\n### Change Management\\n\\nChanges represent formal change requests (RFCs) with risk assessment and CAB approval workflow.\\n\\n**Lifecycle:** `draft` \u2192 `submitted` \u2192 `cab_review` \u2192 `approved` \u2192 `scheduled` \u2192 `implementing` \u2192 `implemented` \u2192 `pir` \u2192 `closed` (or `rejected`/`cancelled`)\\n\\n**Key fields:**\\n- `ChangeNumber` \u2014 auto-incremented identifier\\n- `ChangeType` \u2014 `standard`, `normal`, `emergency`, `major`\\n- `RiskLevel`, `RiskAssessment`, `ImpactAssessment` \u2014 risk evaluation\\n- `BackoutPlan`, `TestPlan` \u2014 implementation safeguards\\n- `ScheduledStart`, `ScheduledEnd` \u2014 planned window\\n- `ActualStart`, `ActualEnd` \u2014 execution timestamps\\n- `RequestedBy` \u2014 originating user\\n- `PIRNotes` \u2014 post-implementation review findings\\n\\n**Operations:**\\n- `ListChanges()` \u2014 filtered list with status/type/priority facets\\n- `GetChange()` \u2014 full detail with approval history\\n- `CreateChange()` \u2014 new RFC (defaults to `draft` status)\\n- `UpdateChange()` \u2014 patch fields (status, dates, notes)\\n- `CreateChangeApproval()` \u2014 record a CAB member's decision\\n- `ListChangeApprovals()` \u2014 retrieve all approval records\\n\\nThe approval workflow is tracked in the `change_approvals` table, storing each approver's decision (`approved`, `rejected`, `abstain`) with comments and timestamp.\\n\\n### CMDB (Configuration Management Database)\\n\\nThe CMDB maintains a registry of configuration items (CIs) and their relationships.\\n\\n**CI Types:** `server`, `workstation`, `network_device`, `software`, `service`, `vm`, `cloud`, `printer`, `mobile`, `other`\\n\\n**CI Status:** `active`, `inactive`, `retired`, `maintenance`, `disposed`\\n\\n**Key fields:**\\n- `CINumber` \u2014 formatted identifier (e.g., `CI-00042`)\\n- `CIType`, `Category`, `Subcategory` \u2014 classification\\n- `OwnerID`, `CompanyID` \u2014 ownership and organizational context\\n- `Attributes` \u2014 flexible JSON key-value store for custom properties\\n- `PurchaseDate`, `WarrantyExpiry`, `PurchaseCost` \u2014 asset tracking\\n- `SerialNumber`, `AssetTag` \u2014 physical identification\\n- `AgentID` \u2014 link to RMM (remote monitoring) agent\\n\\n**Relationships:** Directed edges between CIs with types like `depends_on`, `hosted_on`, `connected_to`, `uses`, `manages`, `backs_up`.\\n\\n**Operations:**\\n- `ListCMDBItems()` \u2014 filtered list by type, status, company\\n- `GetCMDBItem()` \u2014 full detail with all attributes\\n- `CreateCMDBItem()` \u2014 new CI record\\n- `UpdateCMDBItem()` \u2014 patch CI properties\\n- `ListCMDBRelationships()` \u2014 retrieve all relationships for a CI (both directions)\\n\\n### Service Catalog\\n\\nThe Service Catalog publishes available services with SLA targets and billing information.\\n\\n**Service Status:** `draft`, `active`, `retired`, `suspended`\\n\\n**Key fields:**\\n- `Name`, `Description`, `Category` \u2014 service identity\\n- `OwnerID` \u2014 service owner\\n- `SLAPolicyID` \u2014 linked SLA policy\\n- `AvailabilityTarget` \u2014 e.g., 99.9% uptime\\n- `SupportHours` \u2014 e.g., \\\"24x7\\\" or \\\"business hours\\\"\\n- `CostPerMonth`, `BillingType` \u2014 pricing (`fixed`, `per_user`, `per_device`, `usage`)\\n- `DocumentationURL`, `RunbookURL` \u2014 support resources\\n- `IsInternal` \u2014 visibility flag (internal services hidden from clients)\\n- `SortOrder` \u2014 display ordering\\n\\n**Operations:**\\n- `ListServices()` \u2014 optionally filtered by category\\n- `CreateService()` \u2014 new service entry (defaults to `draft`)\\n\\n## HTTP Routes\\n\\nAll routes are registered under `/itil/` and `/api/itil/` prefixes.\\n\\n### Problem Management\\n\\n| Method | Path | Handler | Purpose |\\n|--------|------|---------|---------|\\n| GET | `/itil/problems` | `handleProblemList` | List problems with filters |\\n| GET | `/itil/problems/new` | `handleProblemForm` | New problem form |\\n| GET | `/itil/problems/{id}` | `handleProblemDetail` | Problem detail + incidents |\\n| POST | `/api/itil/problems` | `handleCreateProblem` | Create problem (form or JSON) |\\n| PATCH | `/api/itil/problems/{id}` | `handleUpdateProblem` | Update problem fields |\\n| POST | `/api/itil/problems/{id}/incidents` | `handleLinkIncident` | Link helpdesk ticket |\\n\\n### Change Management\\n\\n| Method | Path | Handler | Purpose |\\n|--------|------|---------|---------|\\n| GET | `/itil/changes` | `handleChangeList` | List changes with filters |\\n| GET | `/itil/changes/new` | `handleChangeForm` | New change form |\\n| GET | `/itil/changes/{id}` | `handleChangeDetail` | Change detail + approvals |\\n| POST | `/api/itil/changes` | `handleCreateChange` | Create change (form or JSON) |\\n| PATCH | `/api/itil/changes/{id}` | `handleUpdateChange` | Update change fields |\\n| POST | `/api/itil/changes/{id}/approve` | `handleApproveChange` | CAB approval decision |\\n\\n### CMDB\\n\\n| Method | Path | Handler | Purpose |\\n|--------|------|---------|---------|\\n| GET | `/itil/cmdb` | `handleCMDBList` | List CIs with filters |\\n| GET | `/itil/cmdb/new` | `handleCMDBForm` | New CI form |\\n| GET | `/itil/cmdb/{id}` | `handleCMDBDetail` | CI detail + relationships |\\n| POST | `/api/itil/cmdb` | `handleCreateCMDBItem` | Create CI (form or JSON) |\\n| PATCH | `/api/itil/cmdb/{id}` | `handleUpdateCMDBItem` | Update CI fields |\\n\\n### Service Catalog\\n\\n| Method | Path | Handler | Purpose |\\n|--------|------|---------|---------|\\n| GET | `/itil/services` | `handleServiceCatalog` | List services (optionally by category) |\\n| GET | `/itil/services/new` | `handleServiceForm` | New service form |\\n| POST | `/api/itil/services` | `handleCreateService` | Create service (form or JSON) |\\n\\n## Request Handling Patterns\\n\\n### Form vs. JSON\\n\\nCreate and update handlers detect the request content type and parse accordingly:\\n\\n```go\\nisForm := r.Header.Get(\\\"X-Form-Post\\\") == \\\"true\\\" ||\\n    r.Header.Get(\\\"Content-Type\\\") == \\\"application/x-www-form-urlencoded\\\"\\n\\nif isForm {\\n    r.ParseForm()\\n    // Extract fields from form values\\n} else {\\n    json.NewDecoder(r.Body).Decode(&amp;obj)\\n    // Decode JSON body\\n}\\n```\\n\\nForm submissions redirect via `HX-Redirect` header (HTMX convention); JSON responses return the created/updated object.\\n\\n### HTMX Support\\n\\nAll page handlers check for the `HX-Request` header to determine whether to render a full page or a partial fragment. The `renderer.Render()` call handles this distinction.\\n\\n### Tenant Isolation\\n\\nEvery database query includes a `tenant_id` filter extracted from the request context:\\n\\n```go\\ntenantID := auth.TenantIDFromContext(r.Context())\\n```\\n\\nThis ensures strict multi-tenant data isolation.\\n\\n## Database Schema Patterns\\n\\n### Auto-Numbered Identifiers\\n\\nProblems, changes, and CIs use auto-incrementing numeric identifiers (e.g., `problem_number`, `change_number`, `ci_number`) for user-facing display, while UUIDs serve as primary keys.\\n\\n### Denormalized Fields\\n\\nList queries include denormalized user names and company names to avoid N+1 joins:\\n\\n```sql\\nLEFT JOIN users u ON u.id = p.assigned_to\\n-- Scan into AssignedName field\\n```\\n\\n### Flexible Attributes\\n\\nThe CMDB `attributes` column stores JSON for extensibility without schema changes:\\n\\n```go\\nAttributes json.RawMessage `json:\\\"attributes,omitempty\\\"`\\n```\\n\\n### Relationship Tracking\\n\\nThe `cmdb_relationships` table stores directed edges with a `relationship` type field. Queries retrieve both directions (source or target) for a given CI.\\n\\n## Update Semantics\\n\\nThe `UpdateProblem()`, `UpdateChange()`, and `UpdateCMDBItem()` methods use a whitelist pattern to prevent SQL injection via field names:\\n\\n```go\\nallowed := map[string]bool{\\n    \\\"title\\\": true, \\\"status\\\": true, \\\"priority\\\": true,\\n    // ... other safe fields\\n}\\nfor field, val := range updates {\\n    if !allowed[field] {\\n        continue  // Skip unknown fields\\n    }\\n    query += fmt.Sprintf(\\\", %s = $%d\\\", field, n)\\n    args = append(args, val)\\n    n++\\n}\\n```\\n\\nOnly explicitly whitelisted fields are updated. All updates set `updated_at = now()`.\\n\\n## Integration Points\\n\\n### With Auth Module\\n\\n- `auth.TenantIDFromContext()` \u2014 extract tenant ID from request\\n- `auth.UserIDFromContext()` \u2014 extract user ID for requestor/approver tracking\\n\\n### With UI Module\\n\\n- `ui.Renderer.Render()` \u2014 render HTML templates with data and HTMX support\\n\\n### With Helpdesk Module (Decoupled)\\n\\nThe ITIL module does **not** import the helpdesk package. Instead, it reads ticket data directly via SQL:\\n\\n```sql\\nLEFT JOIN tickets t ON t.id = pi.ticket_id\\n```\\n\\nThis allows the modules to evolve independently while maintaining referential integrity at the database level.\\n\\n## Convenience Queries\\n\\nTwo helper methods support form option loaders:\\n\\n- `ListUsers()` \u2014 active users for assignment dropdowns\\n- `ListCompanies()` \u2014 active companies for CMDB ownership\\n\\nBoth return simple `{ID, Name}` structs for template rendering.\\n\\n## Error Handling\\n\\nHandlers return HTTP error responses with descriptive messages:\\n\\n- `400 Bad Request` \u2014 invalid form/JSON, missing required fields\\n- `404 Not Found` \u2014 record not found for tenant\\n- `500 Internal Server Error` \u2014 database errors with error message appended\\n\\n## Initialization\\n\\nThe module is initialized in `main()`:\\n\\n```go\\nitilHandler := itil.NewHandler(pool, renderer)\\nitilHandler.RegisterRoutes(mux)\\n```\\n\\n`NewHandler()` creates a `Handler` with a `Store` (database connection pool) and a `Renderer` (template engine).\",\"internal-kb\":\"# internal \u2014 kb\\n\\n# Knowledge Base Module (`internal/kb`)\\n\\nThe knowledge base module provides article management, full-text search, and AI-powered article suggestions for the NexusOS PSA system. It handles article CRUD operations, category organization, and integrates with the helpdesk to suggest relevant articles when creating or viewing tickets.\\n\\n## Overview\\n\\nThe module is built around two core concepts:\\n\\n1. **Article Management** \u2014 Create, read, update, and delete knowledge base articles with metadata (status, tags, category, author)\\n2. **Search &amp; Discovery** \u2014 Full-text search using PostgreSQL `tsvector` with relevance ranking, plus AI-powered suggestions for helpdesk tickets\\n\\nAll operations are tenant-isolated; articles and categories belong to a specific tenant and cannot be accessed across tenant boundaries.\\n\\n## Core Types\\n\\n### Article\\n\\nAn `Article` represents a single knowledge base entry with the following fields:\\n\\n- **ID, TenantID** \u2014 Unique identifier and tenant ownership\\n- **Title, Slug, Content** \u2014 Article text; slug is auto-generated from title if not provided\\n- **CategoryID, CategoryName** \u2014 Optional category association\\n- **Tags** \u2014 Comma-separated metadata tags stored as JSON\\n- **Status** \u2014 One of `draft`, `published`, or `archived`; only published articles appear in search results\\n- **IsInternal** \u2014 Boolean flag for internal-only articles\\n- **AuthorID, AuthorName** \u2014 Creator information\\n- **ViewCount, HelpfulCount** \u2014 Engagement metrics\\n- **PublishedAt, CreatedAt, UpdatedAt** \u2014 Timestamps\\n\\n### Category\\n\\nA `Category` organizes articles hierarchically:\\n\\n- **ID, TenantID, Name, Slug** \u2014 Identifier and display name\\n- **ParentID** \u2014 Optional parent category for nesting\\n- **SortOrder** \u2014 Display order within the category list\\n- **ArticleCount** \u2014 Denormalized count of published articles (populated by `getCategories`)\\n\\n### SearchResult\\n\\nExtends `Article` with search-specific fields:\\n\\n- **Rank** \u2014 PostgreSQL `ts_rank` relevance score\\n- **Headline** \u2014 HTML snippet with matched terms highlighted in `` tags\\n\\n## Handler &amp; Routes\\n\\nThe `Handler` struct manages all HTTP endpoints and database operations. It requires:\\n\\n- A `*pgxpool.Pool` for database access\\n- A `*ui.Renderer` for HTML template rendering\\n- An optional `AIProvider` for autonomous article generation\\n\\n### Registered Routes\\n\\n**Page Routes (HTML)**\\n\\n| Method | Path | Handler | Purpose |\\n|--------|------|---------|---------|\\n| GET | `/kb` | `handleArticleList` | List all articles, optionally filtered by category |\\n| GET | `/kb/new` | `handleNewArticleForm` | Render article creation form |\\n| POST | `/kb/new` | `handleNewArticleSubmit` | Process form submission and create article |\\n| GET | `/kb/search` | `handleSearch` | Render search results page |\\n| GET | `/kb/articles/{slug}` | `handleArticleView` | Display single article and increment view count |\\n\\n**API Routes (JSON)**\\n\\n| Method | Path | Handler | Purpose |\\n|--------|------|---------|---------|\\n| POST | `/api/kb/articles` | `handleCreateArticle` | Create article from JSON |\\n| PUT | `/api/kb/articles/{id}` | `handleUpdateArticle` | Update article metadata and content |\\n| DELETE | `/api/kb/articles/{id}` | `handleDeleteArticle` | Remove article |\\n| GET | `/api/kb/search` | `handleSearchAPI` | Search articles, return JSON results |\\n| POST | `/api/kb/categories` | `handleCreateCategory` | Create category |\\n| POST | `/api/kb/articles/{id}/helpful` | `handleMarkHelpful` | Increment helpful count |\\n| GET | `/api/kb/suggest` | `handleSuggest` | Suggest top 5 articles for a query (used by helpdesk) |\\n| POST | `/api/kb/ai/generate` | `handleAIGenerate` | Generate article from natural language prompt |\\n\\n## Key Operations\\n\\n### Article Creation\\n\\nBoth form-based (`handleNewArticleSubmit`) and API-based (`handleCreateArticle`) creation follow the same flow:\\n\\n1. Parse input (title, content, category, tags, status)\\n2. Generate slug from title using `slugify()` if not provided\\n3. Convert tags from comma-separated string to JSON array\\n4. Set `published_at` timestamp if status is `published`\\n5. Insert into `kb_articles` table with tenant isolation\\n6. Return created article with generated ID and timestamp\\n\\nThe `save_action` form field determines final status: `\\\"draft\\\"` or `\\\"publish\\\"` override the submitted status value.\\n\\n### Search\\n\\nThe `search()` function performs full-text search using PostgreSQL's `tsvector` and `plainto_tsquery`:\\n\\n```\\nSELECT ... WHERE a.search_vector @@ plainto_tsquery('english', $2)\\nORDER BY ts_rank(...) DESC\\n```\\n\\nKey behaviors:\\n\\n- Only searches published articles (`a.status = 'published'`)\\n- Uses `ts_headline()` to extract a 25\u201350 word snippet with matched terms wrapped in `` tags\\n- Returns up to 20 results ranked by relevance\\n- Empty query returns no results\\n\\nThe `handleSuggest` endpoint limits results to the top 5 for use in the helpdesk ticket interface.\\n\\n### Article View\\n\\n`handleArticleView` retrieves a single article by slug and increments its view count asynchronously:\\n\\n```go\\ngo h.pool.Exec(r.Context(), \\\"UPDATE kb_articles SET view_count = view_count + 1 WHERE id = $1\\\", a.ID)\\n```\\n\\nThis non-blocking approach prevents view count updates from delaying the response.\\n\\n### AI Article Generation\\n\\nIf an `AIProvider` is registered via `SetAIProvider()`, the `handleAIGenerate` endpoint accepts a JSON request with a `prompt` field and returns generated article content:\\n\\n```json\\n{\\n  \\\"title\\\": \\\"Generated Title\\\",\\n  \\\"content\\\": \\\"Generated markdown content...\\\",\\n  \\\"tags\\\": \\\"tag1, tag2, tag3\\\"\\n}\\n```\\n\\nThe provider implementation is responsible for calling the configured LLM API with the tenant's API key. If no provider is set, the endpoint returns `501 Not Implemented`.\\n\\n## Utility Functions\\n\\n### slugify(s string) \u2192 string\\n\\nConverts a title to a URL-safe slug:\\n\\n- Lowercases and trims whitespace\\n- Keeps alphanumeric characters, spaces, hyphens, and underscores\\n- Replaces spaces and other punctuation with hyphens\\n- Collapses consecutive hyphens\\n- Trims leading/trailing hyphens\\n\\nExample: `\\\"Hello, World!\\\"` \u2192 `\\\"hello-world\\\"`\\n\\n### getCategories(r *http.Request, tenantID string) \u2192 []Category\\n\\nLoads all categories for a tenant with article counts:\\n\\n```sql\\nSELECT c.id, c.name, c.slug, c.parent_id, c.sort_order, COUNT(a.id)\\nFROM kb_categories c\\nLEFT JOIN kb_articles a ON a.category_id = c.id AND a.status = 'published'\\nWHERE c.tenant_id = $1\\nGROUP BY c.id\\nORDER BY c.sort_order, c.name\\n```\\n\\nReturns categories sorted by `sort_order` and name, with `article_count` populated from the join.\\n\\n## Integration Points\\n\\n### Authentication\\n\\nAll handlers extract tenant and user IDs from the request context:\\n\\n```go\\ntenantID := auth.TenantIDFromContext(r.Context())\\nuserID := auth.UserIDFromContext(r.Context())\\n```\\n\\nThis ensures all database queries are scoped to the authenticated tenant.\\n\\n### UI Rendering\\n\\nHTML page handlers use `h.renderer.Render()` to populate templates with data:\\n\\n```go\\nh.renderer.Render(w, \\\"kb/articles.html\\\", data, isHTMX)\\n```\\n\\nThe `isHTMX` flag indicates whether the request came from an HTMX partial update, allowing the renderer to return a fragment or full page as appropriate.\\n\\n### Helpdesk Integration\\n\\nThe helpdesk module calls `GET /api/kb/suggest?q=` to retrieve relevant articles when creating or viewing a ticket. The endpoint returns the top 5 search results as JSON, enabling inline suggestions in the ticket interface.\\n\\n### AI Provider Interface\\n\\nThe `AIProvider` interface allows pluggable AI backends:\\n\\n```go\\ntype AIProvider interface {\\n    GenerateArticleForTenant(ctx context.Context, tenantID, prompt string) (title, content, tags string, err error)\\n}\\n```\\n\\nImplementations receive the tenant ID and can use it to look up the tenant's configured API key (e.g., OpenAI, Anthropic) and generate article content autonomously.\\n\\n## Database Schema Assumptions\\n\\nThe module expects the following tables:\\n\\n- **kb_articles** \u2014 Columns: `id`, `tenant_id`, `category_id`, `title`, `slug`, `content`, `tags` (JSON), `is_internal`, `status`, `author_id`, `view_count`, `helpful_count`, `published_at`, `created_at`, `updated_at`, `search_vector` (tsvector)\\n- **kb_categories** \u2014 Columns: `id`, `tenant_id`, `name`, `slug`, `parent_id`, `sort_order`\\n- **users** \u2014 Columns: `id`, `full_name` (for author lookup)\\n\\nThe `search_vector` column must be maintained via a PostgreSQL trigger or application logic to stay in sync with article content and title.\\n\\n## Error Handling\\n\\nHandlers return appropriate HTTP status codes:\\n\\n- **400 Bad Request** \u2014 Invalid JSON body, missing required fields, or malformed form data\\n- **404 Not Found** \u2014 Article slug not found\\n- **501 Not Implemented** \u2014 AI generation requested but no provider configured\\n- **500 Internal Server Error** \u2014 Database errors\\n\\nError messages are returned as plain text in the response body.\\n\\n## Tenant Isolation\\n\\nAll queries include a `WHERE ... tenant_id = $1` clause to prevent cross-tenant data leakage. The tenant ID is extracted from the request context by the authentication middleware and is never user-controlled.\",\"internal-legal\":\"# internal \u2014 legal\\n\\n# Legal Module Documentation\\n\\n## Overview\\n\\nThe Legal module manages contract lifecycle, document management, and e-signature workflows for the PSA platform. It handles contract creation, approval, signing, billing configuration, and document generation with support for multiple contract types (recurring service, block hours, retainer, time &amp; materials, fixed price).\\n\\nKey responsibilities:\\n- **Contract management**: CRUD operations, status tracking, versioning\\n- **Document library**: Upload, organize, preview, and attach legal documents\\n- **E-signature workflows**: Public signing pages, ACH authorization, signature collection\\n- **Billing configuration**: Work types, billing roles, rate overrides, block hours, retainers, milestones\\n- **Document generation**: Auto-generate Order documents, MSAs, Contract Summaries, and final Executed Packages\\n- **Integration**: Syncs with CRM (companies), applies policies, generates invoices\\n\\n---\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    A[\\\"HTTP Handlers(handler.go)\\\"] --&gt;|routes| B[\\\"Contract Pages(list, detail, edit)\\\"]\\n    A --&gt;|routes| C[\\\"Document Library(upload, preview, delete)\\\"]\\n    A --&gt;|routes| D[\\\"Public E-Signature(signing_page_helpers.go)\\\"]\\n    A --&gt;|routes| E[\\\"Billing Config(billing_handler.go)\\\"]\\n    \\n    B --&gt;|loads| F[\\\"Contract Data(types.go)\\\"]\\n    B --&gt;|generates| G[\\\"Order Documents(order_generator.go)\\\"]\\n    B --&gt;|generates| H[\\\"MSA Documents(msa_generator.go)\\\"]\\n    \\n    D --&gt;|validates| I[\\\"ACH Submission(ach_submit_helpers.go)\\\"]\\n    D --&gt;|encrypts| J[\\\"AES-256-GCM(crypto.go)\\\"]\\n    I --&gt;|records| K[\\\"Signatures(legal_contract_signatures)\\\"]\\n    \\n    G --&gt;|stamps| L[\\\"Signature Stamper(signature_stamper.go)\\\"]\\n    H --&gt;|stamps| L\\n    L --&gt;|modifies DOCX| M[\\\"DOCX Renderer(docx_renderer.go)\\\"]\\n    \\n    K --&gt;|triggers| N[\\\"Executed Package(executed_package.go)\\\"]\\n    N --&gt;|composes| O[\\\"Final HTMLwith embedded sigs\\\"]\\n    \\n    E --&gt;|manages| P[\\\"Work TypesBilling RolesRate Overrides\\\"]\\n    P --&gt;|used by| G\\n    P --&gt;|used by| H\\n```\\n\\n---\\n\\n## Core Components\\n\\n### Handler (`handler.go`)\\n\\nCentral HTTP request dispatcher. Manages:\\n- **Contract CRUD**: `handleNewContractForm`, `handleEditContractForm`, `handleContractDetail`\\n- **Contract actions**: `handleSendContract`, `handleActivateContract`, `handleCancelContract`, `handleModifyContract`\\n- **Document library**: `handleUploadDocument`, `handleBulkUpload`, `handlePreviewDocument`, `handleDownloadDocument`\\n- **Public signing**: Routes without auth middleware for e-signature workflows\\n- **CRM integration**: `handleContractsByCompany` for company-level contract queries\\n\\n**Key pattern**: Handlers extract `tenantID` and `userID` from context, validate ownership, execute database operations, and render responses via the UI renderer.\\n\\n### Contract Types (`types.go`)\\n\\nData structures for contracts and billing:\\n\\n```go\\ntype LegalContract struct {\\n    ID, ContractNumber, Name, Status\\n    BillingType, BillingAmount\\n    StartDate, EndDate, SignedDate\\n    AutoRenew, HasPendingChanges\\n    // ... 20+ fields\\n}\\n\\ntype BillingWorkType struct {\\n    ID, Name, Description\\n    DefaultRateMultiplier, SortOrder\\n}\\n\\ntype BillingRole struct {\\n    ID, Name, Description\\n    DefaultHourlyRate, SortOrder\\n}\\n\\ntype BlockHoursConfig struct {\\n    HoursPurchased, HoursConsumed, HoursRemaining\\n    OverageRate, RolloverPolicy, RolloverPercentage\\n}\\n\\ntype RetainerConfig struct {\\n    MonthlyHourCap, CurrentMonthHoursUsed\\n    OverageRate, UnusedHoursPolicy, CreditRate\\n}\\n\\ntype FixedPriceMilestone struct {\\n    ID, ContractID, Name, Amount, DueDate, Status\\n}\\n```\\n\\nHelper functions map enum values to human labels:\\n- `ContractCategoryLabel()`, `BillingTypeLabel()`, `ServiceTypeLabel()`\\n- `RolloverPolicies()`, `UnusedHoursPolicies()`, `CoverageTypes()`\\n\\n### Document Generation\\n\\n#### Order Documents (`order_generator.go`)\\n\\nGenerates DOCX Service Order documents with:\\n- Contract header (number, parties, dates)\\n- Service line items (description, type, billing, amount)\\n- Work type coverage matrix\\n- Billing role rate schedule\\n- Block hours / retainer / milestone details\\n- Signature blocks\\n\\n**Flow**:\\n1. `generateOrderDocument()` \u2192 loads contract data\\n2. `generateOrderHTML()` \u2192 renders HTML preview\\n3. `generateOrderDOCX()` \u2192 creates DOCX with embedded HTML\\n4. `removeOldOrders()` \u2192 cleans up previous versions\\n5. Attaches to contract with `signature_type='order'`\\n\\n**Key functions**:\\n- `loadOrderData()` \u2192 fetches all contract details, work types, roles, billing configs\\n- `orderData` \u2192 struct holding loaded data\\n- `buildServiceTable()` \u2192 renders service line items\\n- `buildWorkTypeTable()` \u2192 renders coverage matrix\\n- `buildRoleRateTable()` \u2192 renders hourly rates\\n\\n#### MSA Documents (`msa_generator.go`)\\n\\nGenerates Master Service Agreement DOCX with:\\n- Standard legal terms (scope, payment, termination, liability)\\n- Service descriptions from contract\\n- Pricing summary\\n- Signature blocks\\n\\n**Flow**:\\n1. `generateMSADocument()` \u2192 loads contract data\\n2. `buildMSAServiceContent()` \u2192 renders service section\\n3. `generateMSADOCX()` \u2192 creates DOCX\\n4. Attaches with `signature_type='msa'`\\n\\n#### Contract Summary (`contract_summary_generator.go`)\\n\\nAuto-generates professional HTML document capturing full contract terms:\\n- Parties and contact info\\n- Contract terms (type, dates, billing cadence, payment terms, auto-renewal)\\n- Services &amp; pricing table\\n- Work type coverage\\n- Service rate schedule\\n- Block hours / retainer details\\n- Acceptance statement and signature blocks\\n\\n**Flow**:\\n1. `generateContractSummaryDocument()` \u2192 loads MSA data\\n2. `renderContractSummaryHTML()` \u2192 builds styled HTML\\n3. Saves to disk and attaches with `signature_type='signature'`\\n4. This is the document clients \\\"sign\\\" when providing contract signature\\n\\n#### Executed Package (`executed_package.go`)\\n\\nComposed final document generated after all signatures collected:\\n- Cover page with contract metadata\\n- All attached documents (Order, MSA, Contract Summary) with content embedded\\n- Signature stamps with signer name, title, date, and signature image\\n- ACH authorization details (masked bank info)\\n- Professional styling for print-to-PDF\\n\\n**Flow**:\\n1. `generateExecutedPackage()` \u2192 triggered after contract activation\\n2. Loads all attached documents and signatures\\n3. `renderContractSummaryHTML()` \u2192 builds composed HTML\\n4. Embeds signature images (base64 PNG data URIs)\\n5. Saves to disk and attaches with `signature_type='none'`\\n\\n### DOCX Rendering (`docx_renderer.go`)\\n\\nServer-side DOCX-to-HTML converter (zero external dependencies):\\n- Parses `word/document.xml` from DOCX ZIP\\n- Extracts text runs with formatting (bold, italic, underline, color, size)\\n- Handles paragraphs, tables, headings, lists, page breaks, hyperlinks\\n- Generates styled HTML with CSS classes for formatting\\n\\n**Key functions**:\\n- `renderDOCXToHTML()` \u2192 entry point, returns complete HTML page\\n- `convertDocXMLToHTML()` \u2192 XML parser, builds HTML incrementally\\n- `renderParagraph()` \u2192 wraps content in appropriate tag (p, h1-h4, ul, ol)\\n- `parseHalfPoints()` \u2192 converts Word half-point font sizes to CSS points\\n- `wordHighlightColor()` \u2192 maps Word highlight names to CSS colors\\n\\nUsed by:\\n- Document preview in library (`handlePreviewDocument`)\\n- Executed package generation (embeds document content)\\n- Contract summary generation (for reference)\\n\\n### Signature Stamping (`signature_stamper.go`)\\n\\nApplies digital signatures to DOCX documents:\\n\\n**For HTML documents**:\\n- `stampSignatureHTML()` \u2192 injects signature image and metadata into HTML\\n\\n**For DOCX documents**:\\n- `stampSignatureDOCX()` \u2192 modifies DOCX XML to add signature drawing\\n- `injectSignatureDrawing()` \u2192 creates Word drawing element with signature image\\n- `injectSignatureRelationship()` \u2192 adds relationship entry for image\\n- `ensureNamespaces()` \u2192 ensures required XML namespaces present\\n\\nSignature images are base64-encoded PNG data URIs embedded directly in documents.\\n\\n### E-Signature Workflows\\n\\n#### Signing Page (`signing_page_helpers.go`)\\n\\nPublic page (no auth) for clients to sign contracts:\\n- `handleSigningPage()` \u2192 displays contract details and documents\\n- `buildSignableDocsList()` \u2192 lists documents requiring signature\\n- `buildACHForm()` \u2192 renders ACH authorization form if required\\n\\n#### ACH Authorization (`ach_submit_helpers.go`)\\n\\nHandles bank account authorization:\\n\\n**Validation**:\\n- `validateACHRequest()` \u2192 enforces required fields, routing/account number format\\n- Routing: exactly 9 digits\\n- Account: 4-17 digits\\n- Account type: \\\"checking\\\" or \\\"savings\\\" (defaults to \\\"checking\\\")\\n\\n**Encryption**:\\n- `encryptACHFields()` \u2192 AES-256-GCM encryption of bank name, routing, account\\n- Stores encrypted values in `ach_authorizations` table\\n- Stores masked last-4 digits for display\\n\\n**Database operations**:\\n- `insertACHAuthorization()` \u2192 persists encrypted bank details, signer metadata\\n- `insertACHSignature()` \u2192 records ACH as signature row (counts toward `signatures_required`)\\n- `advanceContractSignatures()` \u2192 increments `signatures_collected`, activates contract when threshold reached\\n- `runContractActivationEffects()` \u2192 triggers company lifecycle bump, policy application, executed package generation\\n\\n#### Sign Contract (`sign_contract_helpers.go`)\\n\\nHandles contract signature submission:\\n- `handleSignContract()` \u2192 validates token, loads contract, applies signature\\n- `applySignatureToDocument()` \u2192 stamps signature on each signable document\\n- `stampSignatureOnDocument()` \u2192 routes to HTML or DOCX stamper\\n- Records signature in `legal_contract_signatures` table\\n- Advances signature count and activates contract if threshold met\\n\\n### Billing Configuration (`billing_handler.go`, `billing_types.go`)\\n\\nTenant-level billing settings and per-contract overrides:\\n\\n**Settings page** (`/settings/billing`):\\n- Work type CRUD with sort order\\n- Billing role CRUD with sort order\\n- Quote sequence configuration (prefix, next number, padding)\\n\\n**Contract-level configuration**:\\n- Block hours: hours purchased, overage rate, rollover policy, alert thresholds\\n- Retainer: monthly cap, overage rate, unused hours policy, credit rate\\n- Fixed-price milestones: name, amount, due date, status\\n- Work type coverage: per-contract coverage rules and rate multiplier overrides\\n- Role rate overrides: per-contract hourly rate overrides for billing roles\\n\\n**Data loaders** (used by contract forms):\\n- `loadWorkTypes()` \u2192 active work types ordered by sort order\\n- `loadBillingRoles()` \u2192 active billing roles ordered by sort order\\n- `loadBlockHoursConfig()` \u2192 block hours config with derived `HoursRemaining`\\n- `loadRetainerConfig()` \u2192 retainer config\\n- `loadFixedPriceMilestones()` \u2192 milestones ordered by sort order\\n- `loadContractWorkTypeConfigs()` \u2192 per-contract work type coverage\\n- `loadContractRoleRateOverrides()` \u2192 per-contract role rate overrides\\n\\n### Document Analysis (`analyze.go`)\\n\\nAuto-detects document category and generates description:\\n- `analyzeDocument()` \u2192 extracts text from file, detects metadata\\n- `detectCategory()` \u2192 keyword matching on filename + content\\n  - Categories: msa, nda, sla, sow, dpa, baa, toc, other\\n  - Scores keywords by length (longer matches = higher score)\\n- `generateDescription()` \u2192 creates brief description from category and content\\n- `extractDOCXText()` \u2192 reads DOCX and extracts plain text\\n- `extractTextFromWordXML()` \u2192 parses Word XML, preserves paragraph structure\\n\\nUsed by bulk upload to auto-categorize documents.\\n\\n### Encryption (`crypto.go`)\\n\\nAES-256-GCM encryption for sensitive ACH data:\\n- `initACHKey()` \u2192 loads key from `ACH_ENCRYPTION_KEY` env var (64 hex chars = 32 bytes)\\n- `encryptACH()` \u2192 encrypts plaintext, returns hex-encoded nonce+ciphertext\\n- `decryptACH()` \u2192 decrypts hex-encoded ciphertext back to plaintext\\n\\nNonce is randomly generated per encryption and prepended to ciphertext.\\n\\n---\\n\\n## Database Schema (Key Tables)\\n\\n### legal_contracts\\n```\\nid, tenant_id, company_id, contract_number, name, status\\nbilling_type, billing_amount, start_date, end_date, signed_date\\nauto_renew, approval_token, has_ach_form, has_pending_changes\\npending_change_count, signatures_required, signatures_collected\\nescalation_policy_id, sla_policy_id, created_at, updated_at\\n```\\n\\n### legal_documents\\n```\\nid, tenant_id, name, description, category, version\\nfile_name, file_path, file_size, mime_type, uploaded_by\\nis_active, created_at, updated_at\\n```\\n\\n### legal_contract_documents\\n```\\ncontract_id, document_id, signature_type (order, msa, signature, ach_authorization, none)\\n```\\n\\n### legal_contract_signatures\\n```\\ncontract_id, signature_type, signer_name, signer_email, signer_title\\nsigner_ip, signature_data (base64 PNG), consent_text, user_agent\\nsigned_at, document_id\\n```\\n\\n### ach_authorizations\\n```\\ntenant_id, contract_id, company_id, company_name\\nrecurring_amount, billing_frequency\\nbank_name_enc, routing_number_enc, account_number_enc (AES-256-GCM)\\naccount_type, authorized_signer\\nrouting_last4, account_last4 (masked)\\nsigner_name, signer_email, signer_ip, user_agent\\nconsent_text, signature_data, submitted_at\\n```\\n\\n### billing_work_types\\n```\\nid, tenant_id, name, description, default_rate_multiplier\\nis_active, sort_order, created_at, updated_at\\n```\\n\\n### billing_roles\\n```\\nid, tenant_id, name, description, default_hourly_rate\\nis_active, sort_order, created_at, updated_at\\n```\\n\\n### contract_block_hours\\n```\\ncontract_id, hours_purchased, hours_consumed, overage_rate\\nrollover_policy, rollover_percentage, rollover_cap_hours\\nalert_threshold_1, alert_threshold_2, period_start, period_end\\n```\\n\\n### contract_retainer_config\\n```\\ncontract_id, monthly_hour_cap, current_month_hours_used, overage_rate\\nunused_hours_policy, rollover_cap_hours, credit_rate\\n```\\n\\n### contract_fixed_price_milestones\\n```\\nid, contract_id, name, description, amount, due_date, status, sort_order\\ninvoiced_at, created_at\\n```\\n\\n### contract_work_type_config\\n```\\nid, contract_id, work_type_id, coverage, rate_multiplier_override\\n```\\n\\n### contract_role_rate_overrides\\n```\\nid, contract_id, billing_role_id, hourly_rate_override\\n```\\n\\n---\\n\\n## Workflows\\n\\n### Contract Creation\\n\\n1. User navigates to `/contracts/new`\\n2. `handleNewContractForm()` loads:\\n   - Companies (for client selection)\\n   - Work types and billing roles\\n   - Available escalation and SLA policies\\n3. User fills form with:\\n   - Client, contract type, dates, billing amount\\n   - Service line items (description, type, billing, amount)\\n   - Work type coverage and role rate overrides\\n   - Block hours / retainer / milestone configs\\n4. `handleNewContractSubmit()`:\\n   - Validates input\\n   - Creates `legal_contracts` row\\n   - Creates service line items in `contract_service_lines`\\n   - Creates work type configs, role overrides, billing configs\\n   - Generates Order document\\n   - Generates MSA document\\n   - Generates Contract Summary document\\n   - Logs audit entry\\n   - Redirects to contract detail page\\n\\n### Contract Modification\\n\\n1. User clicks \\\"Modify\\\" on contract detail\\n2. `handleModifyContract()` creates a new version:\\n   - Copies contract to new row with `version` incremented\\n   - Sets `has_pending_changes = true` on original\\n   - Regenerates Order document with new terms\\n   - Marks original as \\\"pending_approval\\\"\\n3. User reviews changes and clicks \\\"Approve\\\" or \\\"Dismiss\\\"\\n4. `handleApproveChanges()`:\\n   - Deletes old version\\n   - Promotes new version to current\\n   - Regenerates documents\\n   - Resets `has_pending_changes = false`\\n\\n### Contract Signing\\n\\n1. User sends contract via `handleSendContract()`:\\n   - Generates unique `approval_token`\\n   - Sends email with signing link: `/sign/contract/{token}`\\n2. Client visits signing page (no auth required):\\n   - `handleSigningPage()` loads contract and documents\\n   - Displays list of signable documents\\n   - Shows ACH form if `has_ach_form = true`\\n3. Client signs contract:\\n   - Draws signature on canvas (HTML5)\\n   - Submits via `handleSignContract()` or `handleACHSubmit()`\\n   - Signature stamped on each document\\n   - Recorded in `legal_contract_signatures`\\n   - `signatures_collected` incremented\\n4. When `signatures_collected &gt;= signatures_required`:\\n   - Contract status \u2192 \\\"active\\\"\\n   - `signed_date` and `start_date` stamped\\n   - `runContractActivationEffects()` triggers:\\n     - Company lifecycle bump (prospect \u2192 customer)\\n     - Policy application (escalation, SLA)\\n     - Async executed package generation\\n\\n### Executed Package Generation\\n\\n1. Triggered after contract activation (async via goroutine)\\n2. `generateExecutedPackage()`:\\n   - Loads contract metadata\\n   - Loads all attached documents (Order, MSA, Contract Summary)\\n   - Loads all signatures\\n   - Builds composed HTML with:\\n     - Cover page\\n     - Section dividers\\n     - Document content (embedded)\\n     - Signature stamps with images\\n     - ACH authorization details (if present)\\n   - Saves to disk\\n   - Attaches to contract with `signature_type='none'`\\n3. Final deliverable ready for download/print-to-PDF\\n\\n---\\n\\n## Integration Points\\n\\n### CRM (Companies)\\n\\n- Contract creation requires company selection\\n- `handleContractsByCompany()` returns contracts for a company\\n- Contract activation bumps company lifecycle stage (prospect \u2192 customer)\\n- Company name and contact info embedded in documents\\n\\n### Policies (Escalation, SLA)\\n\\n- Contract can be assigned escalation and SLA policies\\n- `loadActiveContractPolicies()` loads available policies\\n- `applyContractPolicies()` applies policies when contract activates\\n\\n### Invoicing\\n\\n- `handleGenerateInvoice()` creates invoice from contract\\n- Uses billing amount and frequency from contract\\n- Linked to contract for tracking\\n\\n### Document Sync\\n\\n- `handleSaveSyncSettings()` configures sync interval\\n- Sync scheduler (in `sync.go`) periodically syncs contract status with external systems\\n\\n---\\n\\n## Key Design Patterns\\n\\n### Tenant Isolation\\n\\nAll queries filter by `tenant_id` from context. No cross-tenant data leakage.\\n\\n### Soft Deletes\\n\\nDocuments and work types use `is_active` flag instead of hard delete.\\n\\n### Versioning\\n\\nContracts support modification via version copies. Original preserved for audit trail.\\n\\n### Async Generation\\n\\nExecuted package generation runs in goroutine to avoid blocking HTTP response.\\n\\n### Encryption at Rest\\n\\nACH bank details encrypted with AES-256-GCM. Only masked last-4 digits stored unencrypted.\\n\\n### Document Composition\\n\\nFinal executed package composes multiple documents into single HTML with embedded content and signatures.\\n\\n### Server-Side Rendering\\n\\nDOCX-to-HTML conversion done server-side (zero JS dependencies). Signature stamping modifies DOCX XML directly.\\n\\n---\\n\\n## Error Handling\\n\\n- **Validation errors**: Return 400 with user-facing message\\n- **Not found**: Return 404 (contract, document, etc.)\\n- **Encryption errors**: Log specific field, return generic \\\"encryption error\\\" 500\\n- **Database errors**: Log full error, return generic message to user\\n- **File I/O errors**: Log and return 500\\n\\n---\\n\\n## Security Considerations\\n\\n1. **ACH Encryption**: Bank details encrypted with AES-256-GCM, key from environment\\n2. **Signature Tokens**: Unique per contract, single-use for signing\\n3. **Tenant Isolation**: All queries filter by tenant_id\\n4. **File Upload**: Validate file type, generate random disk names, store outside web root\\n5. **DOCX Parsing**: XML parsing only, no code execution\\n6. **HTML Escaping**: All user input escaped in HTML/XML output\\n\\n---\\n\\n## Testing Considerations\\n\\n- Mock database pool for unit tests\\n- Test contract creation with various billing types\\n- Test signature workflows (single and multi-signature)\\n- Test ACH encryption/decryption\\n- Test DOCX-to-HTML conversion with various formatting\\n- Test document generation with edge cases (long names, special characters)\\n- Test tenant isolation (verify cross-tenant queries fail)\",\"internal-lifecycle\":\"# internal \u2014 lifecycle\\n\\n# Lifecycle Module\\n\\n## Overview\\n\\nThe `lifecycle` package is the logic-layer engine for sales and contract lifecycle events. It provides a transactional, append-only audit log of state changes and synchronously updates financial projections (MRR, churn tracking) within the caller's database transaction.\\n\\n**Core principle:** This module implements invariant logic that every tenant applies identically. Configurable workflows (the Orchestrator layer) subscribe to these events *after* the transaction commits; that integration is handled separately.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    A[\\\"Handler(domain mutation)\\\"] --&gt;|Emit| B[\\\"lifecycle.Emit\\\"]\\n    B --&gt;|INSERT| C[\\\"lifecycle_events(audit log)\\\"]\\n    B --&gt;|applyProjections| D[\\\"ProjectionFan-out\\\"]\\n    D --&gt;|MRR events| E[\\\"mrr_projectionUPSERT\\\"]\\n    D --&gt;|Churn events| F[\\\"churn_ledgerINSERT\\\"]\\n    A --&gt;|tx.Commit| G[\\\"Orchestrator(async, later PR)\\\"]\\n```\\n\\nThe module operates entirely within a single database transaction provided by the caller. It never commits or rolls back\u2014the caller owns transaction lifecycle.\\n\\n## Key Components\\n\\n### Event\\n\\n```go\\ntype Event struct {\\n    ID         string         // optional \u2014 server fills if empty\\n    TenantID   string\\n    EventType  string         // 'contract.signed', 'quote.accepted', etc.\\n    EntityType string         // 'contract', 'deal', 'quote', 'mrr', ...\\n    EntityID   string\\n    FromState  string\\n    ToState    string\\n    ActorID    string         // \\\"\\\" = system\\n    OccurredAt time.Time      // optional \u2014 server fills if zero\\n    Metadata   map[string]any // free-form payload\\n}\\n```\\n\\nAn `Event` represents a single state change. All ID fields are strings (validated as UUIDs by the database). The `Metadata` map is free-form JSON and carries both projection-specific data (e.g., `mrr`, `nrr`) and workflow condition data.\\n\\n### Emit\\n\\n```go\\nfunc Emit(ctx context.Context, tx pgx.Tx, e Event) (Event, error)\\n```\\n\\nThe primary entry point. Validates required fields (`TenantID`, `EventType`, `EntityType`), fills in server-side defaults (`ID`, `OccurredAt`), inserts the event into `lifecycle_events`, and applies synchronous projections.\\n\\n**Validation:**\\n- `TenantID` is required\\n- `EventType` and `EntityType` are required\\n- `OccurredAt` defaults to `time.Now().UTC()` if zero\\n- `Metadata` defaults to an empty map if nil\\n\\n**ID generation:** If the caller does not supply an `ID`, the database generates one via `RETURNING id::text`. This is important for downstream operations (e.g., `churn_ledger` foreign key insertion).\\n\\n**Return value:** The populated `Event` with assigned `ID` and `OccurredAt`. On error, the original event is returned with an error message.\\n\\n### Projection System\\n\\nProjections are synchronous, in-transaction updates to derived tables. The `applyProjections` dispatcher routes events to the appropriate projection handlers based on `EventType`.\\n\\n#### MRR Projection (`applyMRRDelta`)\\n\\nMaintains a per-month, per-tenant row in `mrr_projection` (MRR, ARR, ORR, NRR).\\n\\n**Event types:**\\n- `mrr.added`, `contract.signed` \u2192 add metadata values (sign = +1)\\n- `mrr.removed` \u2192 subtract metadata values (sign = -1)\\n- `mrr.changed` \u2192 replace with delta values (sign = 0)\\n- `contract.churned`, `contract.expired`, `contract.non_renewed` \u2192 subtract and also trigger churn ledger\\n\\n**Metadata keys (all optional; missing = 0):**\\n- `mrr`, `orr`, `nrr`, `arr` \u2014 used by +1/-1 events\\n- `delta_mrr`, `delta_orr`, `delta_nrr`, `delta_arr` \u2014 used by sign=0 (mrr.changed)\\n\\n**Behavior:**\\n- Rows are bucketed by month (first day of the month, UTC)\\n- Uses PostgreSQL `ON CONFLICT ... DO UPDATE` to accumulate deltas\\n- No-op short-circuit: if all four metrics are zero, the row is not touched\\n- Tracks `last_event_id` and `updated_at` for audit purposes\\n\\n#### Churn Ledger (`appendChurnLedger`)\\n\\nAppends one row per churn-shaped event (`contract.churned`, `contract.expired`, `contract.non_renewed`). Idempotent on `event_id` (unique constraint).\\n\\n**Extracted from event:**\\n- `reason` \u2014 defaults to `ToState`, or inferred from `EventType` if empty\\n- `mrr_lost`, `nrr_lost` \u2014 extracted from metadata\\n- `company_id` \u2014 optional, extracted from metadata if present\\n\\n**Behavior:**\\n- Only processes events with `EntityType == \\\"contract\\\"`\\n- Uses `ON CONFLICT (event_id) DO NOTHING` for idempotency\\n- Captures the churn timestamp (`OccurredAt`)\\n\\n## Usage Pattern\\n\\n```go\\ntx, _ := pool.Begin(ctx)\\ndefer tx.Rollback(ctx)\\n\\n// ... perform domain mutations ...\\n\\nif _, err := lifecycle.Emit(ctx, tx, lifecycle.Event{\\n    TenantID:   tenantID,\\n    EventType:  \\\"contract.signed\\\",\\n    EntityType: \\\"contract\\\",\\n    EntityID:   contractID,\\n    FromState:  \\\"draft\\\",\\n    ToState:    \\\"signed\\\",\\n    ActorID:    userID,\\n    Metadata: map[string]any{\\n        \\\"mrr\\\":          1250.00,\\n        \\\"term_months\\\":  36,\\n        \\\"company_id\\\":   companyID,\\n    },\\n}); err != nil {\\n    return err\\n}\\n\\n// ... more mutations ...\\n\\nif err := tx.Commit(ctx); err != nil {\\n    return err\\n}\\n```\\n\\n**Key points:**\\n1. The caller creates and manages the transaction\\n2. `Emit` never commits or rolls back\\n3. All projections are applied within the same transaction\\n4. If any step fails, the entire transaction can be rolled back atomically\\n5. After `tx.Commit()`, a separate bridge (future PR) publishes the event to the Orchestrator\\n\\n## Metadata Extraction\\n\\nThe `numFromMeta` helper safely extracts numeric values from the `Metadata` map. It handles multiple input types:\\n- `float64` (JSON unmarshal default)\\n- `int`, `int64` (Go native types)\\n- `json.Number` (when using `json.Decoder` with `UseNumber()`)\\n\\nMissing keys or non-numeric values return `0`.\\n\\n## Design Decisions\\n\\n**Why synchronous projections?**\\n- Projections must be consistent with the audit log within a single transaction\\n- Queries for current MRR or churn status must never see a stale state\\n- Failures in projection logic fail the entire operation, preventing silent data loss\\n\\n**Why append-only events?**\\n- Immutable audit trail for compliance and debugging\\n- Enables event sourcing patterns for future features\\n- Supports temporal queries (\\\"what was MRR on date X?\\\")\\n\\n**Why separate Logic and Orchestrator?**\\n- Logic (this module) is invariant: every tenant gets identical behavior\\n- Orchestrator (future) is configurable: tenants can define custom workflows\\n- Decoupling allows independent evolution and testing\\n\\n## Integration Points\\n\\n**Incoming:**\\n- Domain handlers (contracts, quotes, deals) call `Emit` within their transaction\\n\\n**Outgoing (future):**\\n- After `tx.Commit()`, a bridge will publish events to `WorkflowEngine.EvaluateModuleEvent`\\n- Orchestrator workflows subscribe to events and trigger tenant-specific actions\\n\\n**Database tables:**\\n- `lifecycle_events` \u2014 immutable audit log\\n- `mrr_projection` \u2014 monthly financial snapshot\\n- `churn_ledger` \u2014 churn tracking and analysis\\n\\n## Error Handling\\n\\nAll errors are wrapped with context. Common failure modes:\\n- Missing required fields \u2192 validation error\\n- JSON marshaling failure \u2192 metadata corruption\\n- Database constraint violation \u2192 duplicate ID or FK error\\n- Transaction already rolled back \u2192 context error\\n\\nThe caller should log the full error chain and roll back the transaction.\",\"internal-mcpclient\":\"# internal \u2014 mcpclient\\n\\n# mcpclient Module\\n\\nThe `mcpclient` module is NexusOS's server-to-server client for communicating with Model Context Protocol (MCP) servers. It handles JSON-RPC 2.0 calls over HTTP, manages OAuth 2.0 token refresh flows, and transparently encrypts/decrypts sensitive credentials.\\n\\n## Overview\\n\\nEach `Client` instance is bound to a single MCP server (identified by `mcp_servers.id`) and a tenant's encryption key. When you call a method like `ListTools()` or `CallTool()`, the client:\\n\\n1. Loads the server's configuration from the database\\n2. Checks and refreshes the OAuth access token if needed (within 60 seconds of expiry)\\n3. Constructs a JSON-RPC 2.0 request with the appropriate authorization header\\n4. Sends it to the server and decodes the response\\n5. Handles both plain JSON and Server-Sent Events (SSE) response formats\\n\\nThe module abstracts away token lifecycle management\u2014callers never think about expiry or refresh.\\n\\n## Core Components\\n\\n### Client\\n\\n```go\\ntype Client struct {\\n\\tpool        *pgxpool.Pool\\n\\tplatformKey []byte\\n\\tserverID    string\\n\\thttpClient  *http.Client\\n}\\n```\\n\\nThe main entry point. Construct one per (tenant, server) pair using `New()`.\\n\\n**Key methods:**\\n- `Call(ctx, method, params)` \u2014 Execute any JSON-RPC method; returns raw JSON result\\n- `ListTools(ctx)` \u2014 Wrapper for the standard `tools/list` MCP method\\n- `CallTool(ctx, name, args)` \u2014 Wrapper for `tools/call` with name and arguments\\n\\n### Configuration &amp; Token Management\\n\\nThe client reads server configuration on each call via `loadConfig()`, which queries:\\n- Server URL and auth type\\n- Static tokens (for `api_key` / `bearer_token` auth)\\n- OAuth credentials (client ID, client secret, token endpoint)\\n- Current access and refresh tokens (encrypted)\\n\\n**Token refresh flow:**\\n\\n```\\nauthorizationHeader()\\n  \u2193\\n  (if oauth_authorization_code)\\n  \u2193\\nensureAccessToken()\\n  \u251c\u2500 Check if current token is valid (not within 60s of expiry)\\n  \u251c\u2500 If expired, POST to token_endpoint with refresh_token grant\\n  \u251c\u2500 Decrypt refresh_token and client_secret from database\\n  \u251c\u2500 Parse TokenResponse\\n  \u2514\u2500 persistTokens() \u2192 encrypt and write new tokens back to mcp_servers\\n```\\n\\nThe `refreshSkew` constant (60 seconds) ensures proactive refresh before expiry, outrunning typical request round-trip times.\\n\\n### Authorization Handling\\n\\n`authorizationHeader()` returns the Bearer token string based on auth type:\\n\\n| Auth Type | Behavior |\\n|-----------|----------|\\n| `none` / empty | Returns empty string (no auth) |\\n| `api_key` / `bearer_token` | Decrypts and returns static token |\\n| `oauth_authorization_code` | Calls `ensureAccessToken()` to get/refresh token |\\n\\nAll encrypted tokens are decrypted using the platform key (typically `SIEM_ENCRYPTION_KEY`).\\n\\n### JSON-RPC 2.0 Protocol\\n\\nThe `Call()` method constructs and sends JSON-RPC 2.0 requests:\\n\\n```go\\ntype rpcRequest struct {\\n\\tJSONRPC string      `json:\\\"jsonrpc\\\"`\\n\\tID      int         `json:\\\"id\\\"`\\n\\tMethod  string      `json:\\\"method\\\"`\\n\\tParams  interface{} `json:\\\"params,omitempty\\\"`\\n}\\n```\\n\\nResponses are parsed into:\\n\\n```go\\ntype rpcResponse struct {\\n\\tJSONRPC string          `json:\\\"jsonrpc\\\"`\\n\\tID      int             `json:\\\"id\\\"`\\n\\tResult  json.RawMessage `json:\\\"result\\\"`\\n\\tError   *RPCError       `json:\\\"error,omitempty\\\"`\\n}\\n```\\n\\nThe `Result` is returned as raw JSON so callers can unmarshal into method-specific types. If `Error` is present, it's returned as an `RPCError`.\\n\\n### MCP Streamable HTTP Support\\n\\nThe MCP spec allows servers to respond with either:\\n1. Plain JSON (Content-Type: `application/json`)\\n2. Server-Sent Events (Content-Type: `text/event-stream`)\\n\\nThe client advertises support for both via the `Accept` header and detects the response type. If SSE is used, `extractSSEData()` unwraps the JSON-RPC payload from `data:` lines.\\n\\n## Usage Patterns\\n\\n### Basic Setup\\n\\n```go\\nclient := mcpclient.New(pool, platformKey, serverID)\\n```\\n\\n### Calling a Tool\\n\\n```go\\nresult, err := client.CallTool(ctx, \\\"some_tool\\\", map[string]any{\\n\\t\\\"param1\\\": \\\"value1\\\",\\n})\\nif err != nil {\\n\\t// Handle error (includes auth failures, network errors, RPC errors)\\n}\\n// result is json.RawMessage; unmarshal into your type\\n```\\n\\n### Listing Available Tools\\n\\n```go\\ntools, err := client.ListTools(ctx)\\nif err != nil {\\n\\t// Handle error\\n}\\n// tools is json.RawMessage; unmarshal into your type\\n```\\n\\n### Custom JSON-RPC Methods\\n\\n```go\\nresult, err := client.Call(ctx, \\\"custom/method\\\", map[string]any{\\n\\t\\\"key\\\": \\\"value\\\",\\n})\\n```\\n\\n## Integration Points\\n\\nThe module is used by:\\n\\n- **`internal/mcpvendors`** \u2014 Factory and resolver for vendor-specific MCP implementations (PAX8, etc.)\\n- **`internal/tenant`** \u2014 Handler for testing MCP servers and syncing with distributors\\n- **`internal/sentinel`** \u2014 Encryption/decryption of credentials\\n\\nCallers typically don't instantiate `Client` directly; they go through vendor-specific wrappers or tenant handlers that manage the client lifecycle.\\n\\n## Error Handling\\n\\nErrors fall into several categories:\\n\\n| Error | Cause |\\n|-------|-------|\\n| `\\\"server X not found\\\"` | `mcp_servers` row doesn't exist |\\n| `\\\"no refresh_token \u2014 server has not been authorized\\\"` | OAuth server not connected; user must click Connect in UI |\\n| `\\\"decrypt refresh token: ...\\\"` | Decryption failed (wrong key, corrupted ciphertext) |\\n| `\\\"refresh failed: HTTP X\\\"` | Token endpoint returned non-200 status |\\n| `\\\"HTTP X \u2014 ...\\\"` | MCP server returned non-2xx status |\\n| `rpc error X: ...` | JSON-RPC error in response |\\n\\nAll errors are wrapped with context (method name, operation) for debugging.\\n\\n## Security Considerations\\n\\n- **Encryption:** All sensitive tokens (access, refresh, client secret, static API keys) are encrypted at rest using AES-256 via the platform key. Decryption happens in-memory only when needed.\\n- **Token refresh:** New tokens are immediately re-encrypted and persisted to the database, minimizing the window where plaintext tokens exist in memory.\\n- **HTTP client:** Uses a 30-second timeout to prevent hanging requests.\\n- **No token caching:** Configuration (including tokens) is loaded fresh on each `Call()`, ensuring stale tokens are never used.\\n\\n## Testing Considerations\\n\\nWhen testing code that uses this module:\\n\\n- Mock the `pgxpool.Pool` to control what `loadConfig()` returns\\n- For OAuth flows, mock the HTTP client's token endpoint responses\\n- Provide a test encryption key (or use `nil` for non-encrypted auth types)\\n- Remember that `Call()` always loads config from the database\u2014tests must set up the mcp_servers row\",\"internal-mcpvendors\":\"# internal \u2014 mcpvendors\\n\\n# mcpvendors Module\\n\\nThe `mcpvendors` module bridges NexusOS to hosted distributor APIs via MCP (Model Context Protocol) servers. It translates distributor-specific catalog, pricing, subscription, and provisioning operations into a unified interface that the rest of the platform consumes.\\n\\n## Overview\\n\\nDistributors like PAX8 expose their APIs through MCP servers that tenants authorize via OAuth. The `mcpvendors` module:\\n\\n1. **Discovers** authorized vendor MCP servers for a tenant (by scanning `mcp_servers` rows)\\n2. **Wraps** distributor-specific tool calls into the `distributor.Wrapper` interface\\n3. **Normalizes** responses (products, pricing, subscriptions, errors) into platform types\\n4. **Provisions** new subscriptions through the distributor's MCP surface\\n\\nPhase 2 ships PAX8 only; additional distributors (TD Synnex, Ingram, D&amp;H, MS NCE) will register adapters the same way as they land.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    A[\\\"mcp_servers(OAuth-authorized)\\\"] --&gt;|NewDefaultFactory| B[\\\"WrapperFactory\\\"]\\n    B --&gt;|per tenant| C[\\\"PAX8 Wrapper\\\"]\\n    C --&gt;|CallTool| D[\\\"MCP Servermcp-vendors\\\"]\\n    D --&gt;|\\\"pax8.list_productspax8.get_pricingpax8.list_subscriptionspax8.provision_subscription\\\"| E[\\\"PAX8 API\\\"]\\n    C --&gt;|normalize| F[\\\"distributor.Productdistributor.Pricingdistributor.Subscription\\\"]\\n    G[\\\"Procurement Service\\\"] --&gt;|NewWrapperResolver| H[\\\"WrapperResolver\\\"]\\n    H --&gt;|specific distributor| C\\n```\\n\\n## Key Components\\n\\n### Factory (`factory.go`)\\n\\n**`NewDefaultFactory(pool, platformKey) \u2192 distributor.WrapperFactory`**\\n\\nReturns a factory function that, given a tenant ID, queries the database for all active OAuth-authorized vendor MCP servers and returns one `distributor.TenantWrapper` per server.\\n\\n**Discovery logic:**\\n- Scans `mcp_servers` where:\\n  - `tenant_id` matches the tenant\\n  - `is_active = true`\\n  - `auth_type = 'oauth_authorization_code'`\\n  - `refresh_token_enc` is populated (OAuth token available)\\n  - `lower(name) LIKE '%vendor%'` (identifies vendor MCP servers by name pattern)\\n\\n**Why in `mcpvendors` not `distributor`?** Avoids an import cycle: the factory needs both the `distributor` package (for the `WrapperFactory` signature) and the concrete `pax8` package (which itself imports `distributor` for types).\\n\\n### PAX8 Wrapper (`pax8/pax8.go`)\\n\\nThe `Wrapper` type implements `distributor.Wrapper` and `distributor.Provisioner` for PAX8.\\n\\n#### Catalog Operations\\n\\n**`ListProducts(ctx) \u2192 []distributor.Product`**\\n\\nCalls `pax8.list_products` tool on the MCP server. Maps each PAX8 product to a `distributor.Product`:\\n- `DistributorSKU` \u2190 PAX8 product UUID (`id`)\\n- `MfgPartNumber` \u2190 PAX8 SKU field\\n- `VendorName`, `Name`, `Description`, `Category` \u2190 direct mapping\\n- `IsSubscription` \u2190 boolean flag\\n- `Raw` \u2190 full PAX8 JSON (for forward-compat and debug)\\n- `LastSyncedAt` \u2190 current time\\n\\n**`GetPricing(ctx, distributorSKU) \u2192 []distributor.Pricing`**\\n\\nCalls `pax8.get_pricing` with a product UUID. Returns tier-break rows:\\n- `Tier`, `QtyMin`, `QtyMax`, `UnitCost`, `Currency`, `BillingTerm`\\n- `EffectiveDate`, `ExpiresDate` \u2190 parsed from \\\"2006-01-02\\\" format\\n- Defaults: `Currency = \\\"USD\\\"`, `Tier = \\\"standard\\\"`, `QtyMin = 1` if missing\\n\\n**`ListSubscriptions(ctx) \u2192 []distributor.Subscription`**\\n\\nCalls `pax8.list_subscriptions`. Maps each subscription:\\n- `DistributorSubID` \u2190 PAX8 subscription ID\\n- `Qty`, `UnitCost`, `Status` \u2190 direct mapping (status normalized via `normalizeStatus`)\\n- `StartedDate`, `RenewalDate` \u2190 parsed from \\\"2006-01-02\\\" format\\n- `Raw` \u2190 full PAX8 JSON\\n\\n#### Provisioning\\n\\n**`Provision(ctx, req) \u2192 *distributor.ProvisionResult | *distributor.ProvisionError`**\\n\\nDispatches a SaaS subscription order to PAX8 via `pax8.provision_subscription` tool.\\n\\n**Input validation:**\\n- `IdempotencyKey` required (enforces idempotency upstream via Idempotency-Key header)\\n- `ClientCompanyID` required (PAX8 partner account / tenant ID)\\n- At least one line item required\\n\\n**Request shape:**\\n```json\\n{\\n  \\\"idempotencyKey\\\": \\\"...\\\",\\n  \\\"companyId\\\": \\\"...\\\",\\n  \\\"startDate\\\": \\\"2006-01-02\\\",\\n  \\\"notes\\\": \\\"...\\\",\\n  \\\"lines\\\": [\\n    {\\n      \\\"productId\\\": \\\"uuid\\\",\\n      \\\"quantity\\\": 10,\\n      \\\"unitCost\\\": 99.99,\\n      \\\"billingTerm\\\": \\\"monthly\\\"\\n    }\\n  ]\\n}\\n```\\n\\n**Response handling:**\\n1. Unwraps the MCP tool envelope (`toolResult`)\\n2. Checks `isError` flag for hard failures \u2192 `ErrClassTransport`\\n3. Parses the JSON payload for `pax8ProvisionResp`\\n4. If `errorCode` is set, classifies via `classifyPAX8Code`\\n5. Returns `ProvisionResult` with `SubscriptionID`, `OrderID`, `Status`, and raw response\\n\\n**Error classification:**\\n\\n- `classifyPAX8Error(msg)` \u2014 inspects free-text error messages:\\n  - \\\"sku\\\", \\\"product not found\\\", \\\"invalid productId\\\" \u2192 `ErrClassSKUInvalid`\\n  - \\\"quantity\\\", \\\"not available\\\", \\\"out of stock\\\" \u2192 `ErrClassQtyUnavailable`\\n  - \\\"unauthorized\\\", \\\"forbidden\\\", \\\"401\\\", \\\"403\\\" \u2192 `ErrClassAuth`\\n  - \\\"timeout\\\", \\\"connection\\\", \\\"network\\\", \\\"5xx\\\" \u2192 `ErrClassTransport`\\n  - Otherwise \u2192 `ErrClassUnknown`\\n\\n- `classifyPAX8Code(code)` \u2014 maps structured error codes:\\n  - `SKU_NOT_FOUND`, `INVALID_PRODUCT` \u2192 `ErrClassSKUInvalid`\\n  - `QTY_UNAVAILABLE`, `OUT_OF_STOCK` \u2192 `ErrClassQtyUnavailable`\\n  - `UNAUTHORIZED`, `FORBIDDEN` \u2192 `ErrClassAuth`\\n  - Otherwise \u2192 `ErrClassUnknown`\\n\\n#### Tool Response Decoding\\n\\n**`decodeToolPayload(raw, v)`**\\n\\nUnwraps the standard MCP `tools/call` response envelope:\\n```json\\n{\\n  \\\"content\\\": [\\n    {\\n      \\\"type\\\": \\\"text\\\",\\n      \\\"text\\\": \\\"{...json payload...}\\\"\\n    }\\n  ],\\n  \\\"isError\\\": false\\n}\\n```\\n\\nExtracts the first text block, decodes its JSON into the target struct. Returns error if `isError=true` or content is empty.\\n\\n#### Status Normalization\\n\\n**`normalizeStatus(s) \u2192 distributor.SubStatus`**\\n\\nMaps PAX8 status strings to platform enum:\\n- \\\"Active\\\", \\\"active\\\" \u2192 `SubStatusActive`\\n- \\\"Cancelled\\\", \\\"canceled\\\" \u2192 `SubStatusCancelled`\\n- \\\"Paused\\\", \\\"suspended\\\" \u2192 `SubStatusPaused`\\n- \\\"Pending\\\" \u2192 `SubStatusPending`\\n- Unknown \u2192 `SubStatusPending` (safe default)\\n\\n### Resolver (`resolver.go`)\\n\\nProvides narrower lookups for the procurement dispatch path, which needs a single wrapper for a specific (mcp_server_id, distributor_key) pair.\\n\\n**`NewWrapperResolver(pool, platformKey) \u2192 procurement.WrapperResolver`**\\n\\nReturns a resolver function that maps `(tenantID, mcpServerID, key) \u2192 distributor.Wrapper`.\\n\\n**Lookup:**\\n1. Queries `mcp_servers` to verify the server exists and is active\\n2. Creates an `mcpclient.Client` for that server\\n3. Switches on `distributor.Key`:\\n   - `KeyPAX8` \u2192 returns `pax8.New(client, tenantID, mcpServerID)`\\n   - Other keys \u2192 returns error (no adapter yet)\\n\\n**`NewClientCompanyResolver(pool) \u2192 procurement.ClientCompanyResolver`**\\n\\nDerives the distributor-side customer ID from existing subscription data.\\n\\n**Lookup chain:**\\n1. Query the PO to find its company:\\n   - If the PO has a `contract_line_item_id`, follow it to the contract's company\\n   - Otherwise use `purchase_orders.company_id`\\n2. Query `distributor_subscriptions` for an active/pending subscription at that company and distributor\\n3. Extract the distributor's customer ID from the subscription's `raw` JSONB:\\n   - PAX8 stores it as `\\\"companyId\\\"`\\n   - Other distributors may use different keys (extend here as adapters land)\\n\\n**Failure mode:** Returns empty string if no prior subscription exists. The procurement service surfaces this as an `ErrClassUnknown` remediation entry with an explicit \\\"companyId not mapped\\\" message. First-time provisioning under a new agreement for a customer who has never bought from that distributor is a Phase 3.1 follow-up (requires a \\\"find or create company\\\" call on PAX8's API).\\n\\n## Integration Points\\n\\n### Nightly Sync\\n\\nThe `distributor` package's catalog sync uses `NewDefaultFactory` to discover all vendor wrappers for a tenant, then calls `ListProducts`, `GetPricing`, and `ListSubscriptions` to populate the local database.\\n\\n### Procurement Dispatch\\n\\nThe `procurement` service uses `NewWrapperResolver` to fetch a specific wrapper and `NewClientCompanyResolver` to map the PO's company to a distributor customer ID, then calls `Provision` to place the order.\\n\\n### MCP Client\\n\\nBoth factory and resolver create `mcpclient.Client` instances, which handle OAuth token refresh and tool invocation against the MCP server.\\n\\n## Design Notes\\n\\n- **Name-based discovery:** Vendor MCP servers are identified by `lower(name) LIKE '%vendor%'` rather than a `kind` column. This avoids a schema migration in Phase 2 and is stable enough for the current set of vendors.\\n\\n- **Idempotency:** Provision requests require an `IdempotencyKey` that the MCP server forwards as the PAX8 Idempotency-Key header. Same key reuses the prior response, preventing duplicate orders.\\n\\n- **Error classification:** Errors are classified into remediation-queue buckets (`ErrClassSKUInvalid`, `ErrClassQtyUnavailable`, `ErrClassAuth`, `ErrClassTransport`, `ErrClassUnknown`). The platform uses these hints to suggest fixes (e.g., \\\"check the SKU\\\" vs. \\\"re-authorize\\\").\\n\\n- **Raw payloads:** All responses store the full distributor JSON in a `Raw` field for forward-compat and debugging. New fields from the distributor don't require schema changes.\\n\\n- **Extensibility:** New distributors extend the `switch` statements in `NewWrapperResolver` and `NewClientCompanyResolver` as their adapters ship. The interface is the same; only the tool names and response shapes differ.\",\"internal-metasploit\":\"# internal \u2014 metasploit\\n\\n# Metasploit Integration Module\\n\\nThe `internal/metasploit` package provides vulnerability scanning and penetration testing capabilities by integrating NexusOS with the Metasploit Framework. It manages scan lifecycle (creation, execution, result harvesting), stores findings in the database, and optionally routes scans through Sentinel agents for network isolation.\\n\\n## Overview\\n\\nThis module bridges three concerns:\\n\\n1. **RPC Client** (`client.go`) \u2014 Low-level MessagePack communication with `msfrpcd`\\n2. **HTTP Handlers** (`handler.go`) \u2014 Web UI endpoints for scan management, settings, and reporting\\n3. **Data Models** (`types.go`) \u2014 Scan templates, profiles, and database structures\\n\\nThe module is tenant-scoped: each tenant has its own Metasploit RPC credentials, and all scans are isolated by `tenant_id`.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    UI[\\\"Web UIScan Forms\\\"]\\n    Handler[\\\"HandlerHTTP Endpoints\\\"]\\n    Client[\\\"ClientRPC Calls\\\"]\\n    MSF[\\\"msfrpcdMetasploit\\\"]\\n    DB[\\\"PostgreSQLScans &amp; Findings\\\"]\\n    Sentinel[\\\"Sentinel HubSOCKS Proxy\\\"]\\n    \\n    UI --&gt;|POST /security/scans/new| Handler\\n    Handler --&gt;|NewClient| Client\\n    Client --&gt;|MessagePack| MSF\\n    Handler --&gt;|INSERT/UPDATE| DB\\n    Handler --&gt;|OpenSession| Sentinel\\n    Sentinel --&gt;|Proxies=socks5:...| Client\\n```\\n\\n## Client (`client.go`)\\n\\nThe `Client` struct communicates with `msfrpcd` via HTTPS + MessagePack RPC. It handles:\\n\\n- **Authentication** \u2014 `Authenticate()` logs in and caches a session token\\n- **Module execution** \u2014 `RunModule()` launches a Metasploit module with options\\n- **Job polling** \u2014 `ListJobs()` checks if a job is still running\\n- **Database queries** \u2014 `GetHosts()`, `GetServices()`, `GetVulns()` pull results from Metasploit's internal database\\n- **Module search** \u2014 `SearchModules()` finds modules by keyword\\n\\n### Key Methods\\n\\n```go\\n// Create a client and authenticate\\nclient := NewClient(rpcURL, user, pass)\\nif err := client.Authenticate(); err != nil {\\n    // Handle auth failure\\n}\\n\\n// Run a module\\njobID, err := client.RunModule(\\\"auxiliary\\\", \\\"scanner/portscan/tcp\\\", map[string]interface{}{\\n    \\\"RHOSTS\\\": \\\"10.0.0.0/24\\\",\\n    \\\"THREADS\\\": 10,\\n})\\n\\n// Poll for completion\\nfor {\\n    jobs, _ := client.ListJobs()\\n    if _, running := jobs[jobID]; !running {\\n        break // Job finished\\n    }\\n    time.Sleep(10 * time.Second)\\n}\\n\\n// Harvest results\\nhosts, _ := client.GetHosts()\\nservices, _ := client.GetServices()\\nvulns, _ := client.GetVulns()\\n```\\n\\n### MessagePack Quirks\\n\\nThe RPC API returns values in MessagePack's smallest-fitting integer type. Helper functions handle this:\\n\\n- `extractString()` \u2014 Converts `string` or `[]byte` to `string`\\n- `msfString()` \u2014 Extracts string fields from response maps\\n- `msfInt()` \u2014 Extracts integers (handles `int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `int64`, `uint64`)\\n\\nToken values can arrive as `string`, `[]byte`, or `[]interface{}` (array of byte values) \u2014 `Authenticate()` normalizes all three.\\n\\n## Handler (`handler.go`)\\n\\nThe `Handler` struct provides HTTP endpoints for the web UI. It depends on:\\n\\n- `*pgxpool.Pool` \u2014 Database connection pool\\n- `*ui.Renderer` \u2014 Template rendering\\n- `AISecurityAdvisor` \u2014 Optional AI scan profile generator (Nexie wizard)\\n- `SocksHubOpener` \u2014 Optional Sentinel SOCKS hub for agent-routed scans\\n- `DeviceHarvester` \u2014 Optional Sentinel device mirror for discovered hosts\\n\\n### Route Groups\\n\\n#### Settings &amp; Connection (`/settings/metasploit`)\\n\\n- `handleSettings` \u2014 Display RPC URL, user, password, and scan stats\\n- `handleSaveSettings` \u2014 Update tenant's Metasploit config\\n- `handleTestConnection` \u2014 Verify RPC connectivity and fetch version\\n\\n#### Scan Management (`/security/scans`)\\n\\n- `handleScanList` \u2014 List all scans for the tenant\\n- `handleNewScanForm` \u2014 Render the scan creation form (with Sentinel agent dropdown)\\n- `handleLaunchScan` \u2014 Create scan record and launch `executeMultiModuleScan` in a goroutine\\n- `handleScanDetail` \u2014 Show scan status and findings\\n- `handleScanReport` \u2014 Print-friendly report with risk scoring\\n- `handleCancelScan` \u2014 Stop a running Metasploit job\\n\\n#### Module Library (`/api/metasploit/modules`)\\n\\n- `handleModuleLibrary` \u2014 Fetch and categorize all `auxiliary/scanner/*` modules from Metasploit\\n- `handleModuleSearch` \u2014 Search modules by keyword (live from Metasploit)\\n\\n#### Per-Client Security Tab (`/api/company-security/{companyID}`)\\n\\n- `handleCompanySecurityTab` \u2014 Render the CRM client detail's Security tab (scan config + recent scans + SIEM analyses)\\n- `handleCompanyConfigAPI` \u2014 Return company's IP ranges and scan profile as JS snippet\\n- `handleSaveCompanySecurityConfig` \u2014 Update company's scan settings (IP ranges, profile, schedule, pen-test auth)\\n- `handleScanNow` \u2014 Launch an immediate scan using the company's configured profile\\n\\n#### Nexie Security Wizard (`/api/security/nexie-wizard/{companyID}`)\\n\\n- `handleNexieWizard` \u2014 Gather company context (industry, agents, prior findings) and ask AI to recommend a custom scan profile\\n\\n### Scan Execution Flow\\n\\n```\\nhandleLaunchScan\\n  \u251c\u2500 Parse form (scan_name, targets, module_name, via_agent_id)\\n  \u251c\u2500 Validate targets (if via_agent_id: must be IP/CIDR)\\n  \u251c\u2500 INSERT vulnerability_scans (status='running')\\n  \u2514\u2500 go executeMultiModuleScan(...)\\n       \u251c\u2500 [If via_agent_id] OpenSession(SOCKS hub) \u2192 get port\\n       \u251c\u2500 NewClient + Authenticate\\n       \u251c\u2500 For each module:\\n       \u2502   \u251c\u2500 RunModule(options + Proxies if SOCKS)\\n       \u2502   \u251c\u2500 Poll ListJobs until job completes\\n       \u2502   \u2514\u2500 UPDATE scan notes with progress\\n       \u2514\u2500 harvestResults(...)\\n            \u251c\u2500 harvestHosts \u2192 INSERT 'Host Discovered' findings\\n            \u251c\u2500 harvestServices \u2192 INSERT 'Open port' findings\\n            \u251c\u2500 harvestVulns \u2192 INSERT vulnerability findings (classified by severity)\\n            \u251c\u2500 updateScanTotals \u2192 UPDATE scan with per-severity counts\\n            \u2514\u2500 pushSentinelDiscovery \u2192 UpsertDiscoveredDevices (if agent-routed)\\n```\\n\\n### Result Harvesting\\n\\nAfter all modules complete, `harvestResults()` pulls data from Metasploit's database and writes findings:\\n\\n1. **Hosts** \u2014 Each host becomes an `info`-severity \\\"Host Discovered\\\" finding\\n2. **Services** \u2014 Each open port becomes an `info` or `low`-severity finding (elevated if version info present)\\n3. **Vulnerabilities** \u2014 Classified by `classifyVulnSeverity()` heuristic:\\n   - **Critical**: RCE keywords (remote code execution, EternalBlue, BlueKeep)\\n   - **High**: Auth bypass, anonymous access, defaults, backdoors\\n   - **Medium**: Disclosure, version exposure, deprecated services (default fallback)\\n\\nThe `scanFindingCounts` struct accumulates severity buckets across all three passes, then `updateScanTotals()` writes the final counts back to the scan record.\\n\\n### Sentinel Integration (Agent-Routed Scans)\\n\\nWhen a scan is launched with `via_agent_id` set:\\n\\n1. **Validation** \u2014 `targetsToCIDRs()` ensures targets are IP/CIDR (not hostnames or ranges)\\n2. **SOCKS Session** \u2014 `h.socks.OpenSession()` opens an ephemeral listener scoped to the CIDRs\\n3. **Proxy Injection** \u2014 `Proxies=socks5::` is added to every module's options\\n4. **Device Harvest** \u2014 After scan completes, `pushSentinelDiscovery()` mirrors discovered hosts to `sentinel.devices`\\n\\nThe bind IP comes from the `SENTINEL_SOCKS_BIND_IP` environment variable (set by systemd unit); msfrpc on `.250` dials it over the trusted internal subnet.\\n\\n### Company Security Tab\\n\\nThe per-client Security tab (`handleCompanySecurityTab`) renders three collapsible cards:\\n\\n1. **Vulnerability Scan Configuration** \u2014 IP ranges, scan profile selector, cron schedule, pen-test authorization\\n2. **Recent Scans** \u2014 Last 5 scans with status, finding counts, and report links\\n3. **SIEM Analyses** \u2014 Last 10 syslog capture sessions, joined to Nexie AI traffic analyses\\n\\nCard order and collapse state are persisted to localStorage per company (via SortableJS).\\n\\n### Nexie Security Wizard\\n\\n`handleNexieWizard` gathers company context in one round trip:\\n\\n- Company basics (name, industry, employee count, IP ranges, current scan profile)\\n- Assigned compliance frameworks\\n- RMM agent inventory and OS distribution\\n- Prior scan totals (findings, critical/high counts, last scan date)\\n- Observed open ports and services\\n\\nThis is marshalled into `buildNexieWizardInput()` and sent to the AI provider, which returns module recommendations + rationale.\\n\\n## Data Models (`types.go`)\\n\\n### VulnerabilityScan\\n\\nRepresents a scan job in the database. Key fields:\\n\\n- `Status` \u2014 `pending`, `running`, `completed`, `failed`, `cancelled`\\n- `Targets` \u2014 Comma-separated IPs/CIDRs\\n- `ModuleName` \u2014 Comma-separated module names (e.g., `auxiliary/scanner/portscan/tcp,auxiliary/scanner/ssh/ssh_version`)\\n- `FindingCount`, `CriticalCount`, `HighCount`, `MediumCount`, `LowCount`, `InfoCount` \u2014 Per-severity totals\\n- `DurationSeconds` \u2014 Elapsed time from `started_at` to `completed_at`\\n\\n### VulnerabilityFinding\\n\\nOne row per discovered vulnerability. Fields:\\n\\n- `Host` \u2014 IP address\\n- `Port`, `Protocol`, `Service` \u2014 Network details\\n- `Vulnerability` \u2014 Name (e.g., \\\"Open port 22/tcp (SSH)\\\", \\\"EternalBlue (MS17-010)\\\")\\n- `Severity` \u2014 `critical`, `high`, `medium`, `low`, `info`\\n- `CVE` \u2014 CVE ID if extracted from Metasploit refs\\n- `Description` \u2014 Version info, banner, or vulnerability details\\n\\n### ScanTemplate\\n\\nPredefined scan configurations grouped by category:\\n\\n- **discovery** \u2014 Port scans, ARP sweep, UDP sweep\\n- **network** \u2014 SMB, SSH, RDP, SSL/TLS vulnerabilities\\n- **windows** \u2014 MS17-010, SMB signing, RPC enumeration\\n- **web** \u2014 HTTP version, directory scan, Shellshock, Tomcat\\n- **credential** \u2014 FTP, SSH, SNMP, VNC, database login tests\\n- **compliance** \u2014 SSL cipher audit, SMTP relay, NFS exports\\n\\n### ScanProfile\\n\\nNamed bundles of modules with frequency and description:\\n\\n- **basic** \u2014 Monthly discovery (TCP scan, UDP sweep, service detection)\\n- **standard** \u2014 Weekly assessment (discovery + critical CVEs + defaults)\\n- **comprehensive** \u2014 Quarterly deep dive (all checks + brute-force + web + compliance)\\n\\n`ProfileModules(profileKey)` returns the module list for a profile; `DefaultScanProfiles()` returns all three.\\n\\n## Integration Points\\n\\n### Database Schema\\n\\nThe module assumes these tables:\\n\\n```sql\\n-- Scan records\\nvulnerability_scans (\\n  id, tenant_id, company_id, scan_name, scan_type, targets, module_name,\\n  status, msf_job_id, finding_count, critical_count, high_count, medium_count,\\n  low_count, info_count, started_at, completed_at, duration_seconds,\\n  created_by, notes, created_at\\n)\\n\\n-- Findings\\nvulnerability_findings (\\n  id, tenant_id, scan_id, host, port, protocol, service,\\n  vulnerability, severity, cve_id, description, solution, exploitable, created_at\\n)\\n\\n-- Tenant config\\ntenants (\\n  id, metasploit_rpc_url, metasploit_rpc_user, metasploit_rpc_password,\\n  metasploit_enabled, ...\\n)\\n\\n-- Client config\\ncompanies (\\n  id, tenant_id, name, industry, employee_count, managed_ip_ranges,\\n  scan_profile, scan_schedule, scan_enabled,\\n  last_scan_score, last_scan_date,\\n  pen_test_authorized, pen_test_authorized_by, pen_test_authorized_date,\\n  pen_test_scope, ...\\n)\\n\\n-- RMM agents (for Sentinel routing)\\nagents (\\n  id, tenant_id, company_id, rmm_agent_id, hostname, status,\\n  sentinel_hmac_key_enc, os_type, os_version, ...\\n)\\n```\\n\\n### External Dependencies\\n\\n- **`internal/auth`** \u2014 `TenantIDFromContext()`, `UserIDFromContext()` for request scoping\\n- **`internal/ui`** \u2014 `Renderer.Render()` for template rendering\\n- **`internal/sentinel`** \u2014 `DiscoveredDevice`, `DiscoveredPort` types for device harvesting\\n- **`github.com/vmihailenco/msgpack`** \u2014 MessagePack marshalling/unmarshalling\\n- **`github.com/jackc/pgx`** \u2014 PostgreSQL driver\\n\\n### Optional Integrations\\n\\n- **AI Security Advisor** \u2014 Wired via `SetAI()` for Nexie wizard\\n- **Sentinel SOCKS Hub** \u2014 Wired via `SetSocksHub()` for agent-routed scans\\n- **Device Harvester** \u2014 Wired via `SetDeviceHarvester()` to mirror discovered hosts to Sentinel dashboard\\n\\n## Common Workflows\\n\\n### Launch a Direct Scan (from .250)\\n\\n```go\\n// User submits form at POST /security/scans/new\\nhandler.handleLaunchScan(w, r)\\n  // Parses targets, modules, company_id\\n  // Inserts scan record with status='running'\\n  // Launches executeMultiModuleScan in background\\n  // Returns HX-Redirect to scan detail page\\n```\\n\\n### Launch an Agent-Routed Scan\\n\\n```go\\n// User selects via_agent_id in form\\nhandler.handleLaunchScan(w, r)\\n  // Validates targets are IP/CIDR\\n  // Inserts scan record\\n  // Launches executeMultiModuleScan with viaAgentID + scopeCIDRs\\n    // Opens SOCKS session via Sentinel hub\\n    // Injects Proxies into module options\\n    // Runs modules (msfrpc reaches targets through agent tunnel)\\n    // Harvests results and mirrors devices to Sentinel\\n```\\n\\n### View Scan Results\\n\\n```go\\n// User navigates to GET /security/scans/{id}\\nhandler.handleScanDetail(w, r)\\n  // Loads scan record + all findings\\n  // Renders scan_detail.html with findings table\\n  // Findings sorted by severity (critical \u2192 info)\\n```\\n\\n### Generate a Scan Report\\n\\n```go\\n// User clicks \\\"Report\\\" link\\nhandler.handleScanReport(w, r)\\n  // Loads scan + findings\\n  // Calculates risk score (0\u2013100)\\n  // Renders scan_report.html (print-friendly)\\n```\\n\\n### Configure Company Scanning\\n\\n```go\\n// User edits company in CRM\\nhandler.handleCompanySecurityTab(w, r)\\n  // Loads scan config (IP ranges, profile, schedule)\\n  // Loads recent scans + SIEM analyses\\n  // Renders 3 collapsible cards with localStorage persistence\\n```\\n\\n### Use Nexie Wizard\\n\\n```go\\n// User clicks \\\"Generate Scan Profile\\\" in company security tab\\nhandler.handleNexieWizard(w, r)\\n  // Loads company context (industry, agents, prior findings)\\n  // Calls AI provider with context\\n  // Returns JSON with module recommendations + rationale\\n  // Frontend renders recommendations and lets user apply\\n```\\n\\n## Error Handling\\n\\n- **RPC failures** \u2014 Logged and scan marked `failed` with error message in `notes`\\n- **Auth failures** \u2014 Scan marked `failed` with \\\"Metasploit auth failed\\\" message\\n- **Timeout** \u2014 Scan marked `completed` anyway after 30 minutes; results harvested if available\\n- **Panic recovery** \u2014 `executeMultiModuleScan` defers a panic handler that logs and marks scan `failed`\\n\\n## Performance Considerations\\n\\n- **Scan polling** \u2014 Checks every 10 seconds for up to 30 minutes (180 iterations)\\n- **Module execution** \u2014 Sequential (one module at a time) to avoid overwhelming the target\\n- **Result harvesting** \u2014 Batched INSERT statements (one per finding) \u2014 consider bulk insert for large result sets\\n- **SOCKS session** \u2014 Ephemeral, cleaned up after scan completes (deferred cleanup)\\n\\n## Testing\\n\\nKey functions are pure and unit-testable:\\n\\n- `classifyVulnSeverity(name string) string` \u2014 Heuristic severity classification\\n- `targetsToCIDRs(targets string) ([]string, error)` \u2014 Target validation\\n- `buildNexieWizardInput(c *nexieWizardContext, availableModules []string) map[string]interface{}` \u2014 AI input construction\\n\\nMessagePack decoding quirks are tested implicitly via `Authenticate()` token extraction.\",\"internal-middleware\":\"# internal \u2014 middleware\\n\\n# internal/middleware\\n\\nThe middleware package provides HTTP request/response interceptors for NexusOS PSA, handling cross-request concerns like CSRF protection, request logging, and panic recovery. These are composed into the main HTTP handler chain during server startup.\\n\\n## Overview\\n\\nThree middleware functions are exported and chained together in `cmd/psa/main.go`:\\n\\n1. **CSRF** \u2014 Double-submit-cookie CSRF protection for state-changing requests\\n2. **Logging** \u2014 Request/response metrics and timing\\n3. **Recovery** \u2014 Panic catching and graceful error responses\\n\\nThe `Chain` utility applies multiple middleware in order, with the first argument being the outermost layer.\\n\\n## CSRF Protection\\n\\n### Design\\n\\nCSRF protection uses the **double-submit-cookie pattern** (audit finding C7, 2026-04-29). A per-session token is stored in a non-HttpOnly cookie named `csrf_token`. On unsafe HTTP methods (POST, PUT, PATCH, DELETE), the server requires the client to echo this token either in the `X-CSRF-Token` header or as a `csrf_token` form field.\\n\\nThe non-HttpOnly cookie is intentional \u2014 it allows JavaScript and HTMX to read the cookie value and automatically attach it to requests. Templates inject the token into the `` attribute so HTMX includes it in every state-changing request without explicit form handling.\\n\\n### Token Generation and Validation\\n\\n```go\\nCSRF(next http.Handler) http.Handler\\n```\\n\\nOn every request:\\n\\n1. `ensureCSRFCookie()` checks for an existing `csrf_token` cookie. If missing or malformed (not 64 hex characters), it generates a new 32-byte random token, encodes it as hex, and sets it as a Secure, SameSite=Lax cookie.\\n2. `isCSRFSafe()` determines if the request needs CSRF validation:\\n   - Safe methods (GET, HEAD, OPTIONS, TRACE) skip validation\\n   - Requests to exempt path prefixes skip validation\\n3. For unsafe requests, the token from the header or form field is compared against the cookie using `subtle.ConstantTimeCompare()` to prevent timing attacks. Missing or mismatched tokens return 403 Forbidden.\\n\\n### Exempt Paths\\n\\nThe `csrfExemptPrefixes` list defines paths that bypass CSRF checks. These fall into three categories:\\n\\n**mTLS agent endpoints** \u2014 Agents authenticate via mutual TLS, not cookies:\\n- `/api/heartbeat`, `/api/inventory`, `/api/agents/` \u2014 legacy agent API\\n- `/api/devices/register`, `/api/devices/heartbeat`, `/api/devices/update/` \u2014 signature-authenticated device enrollment and telemetry\\n- `/api/devices/ws` \u2014 mTLS WebSocket with nonce challenge\\n- `/api/tunnel/` \u2014 mTLS tunnel\\n\\n**Non-cookie authentication** \u2014 Endpoints using alternative auth mechanisms:\\n- `/api/voip/webhook` \u2014 HMAC-signed webhooks (audit H6)\\n- `/api/quickbooks/oauth/` \u2014 signed OAuth callbacks\\n- `/auth/sso/` \u2014 SSO callbacks\\n\\n**Unauthenticated or cookie-minting endpoints** \u2014 No CSRF risk:\\n- `/auth/login`, `/portal/login` \u2014 mint the cookie; nothing to protect yet\\n- `/auth/refresh` \u2014 bound to refresh_token cookie + tenant check\\n- `/static/`, `/uploads/` \u2014 public assets\\n\\n**Important:** `/api/devices/credentials*` is intentionally NOT exempt. These are cookie-authenticated browser routes for the credential vault and must enforce CSRF. Only signature-authenticated device routes are exempted.\\n\\n### Helper Functions\\n\\n```go\\nCSRFTokenFromRequest(r *http.Request) string\\n```\\n\\nReturns the current request's CSRF token from the cookie. Used by templates to inject the token into the page for HTMX.\\n\\n## Request Logging\\n\\n```go\\nLogging(next http.Handler) http.Handler\\n```\\n\\nWraps handlers with request/response logging. For each request, logs:\\n- HTTP method\\n- Request path\\n- Response status code\\n- Duration (rounded to microseconds)\\n\\n**Legacy agent suppression:** 401 responses on legacy pre-1.2.6 mTLS agent paths are not logged. Old agents in the field with revoked Sectigo certificates retry every 30 seconds on `/api/heartbeat`, `/api/inventory`, `/api/agents/*/commands`, and `/api/tunnel/*`. The server rejects them with 401, but logging each retry would flood journald. The `isLegacyAgentPath()` helper identifies these paths and suppresses their log output.\\n\\n## Panic Recovery\\n\\n```go\\nRecovery(next http.Handler) http.Handler\\n```\\n\\nCatches panics in downstream handlers and returns a 500 Internal Server Error instead of crashing the server. The panic message and full stack trace are logged via `debug.Stack()` for debugging.\\n\\n## Response Writer Wrapper\\n\\nThe `statusWriter` type wraps `http.ResponseWriter` to capture the response status code (which is otherwise write-only). It implements three interfaces:\\n\\n- **http.ResponseWriter** \u2014 Standard response writing\\n- **http.Hijacker** \u2014 Enables WebSocket upgrades through middleware\\n- **http.Flusher** \u2014 Enables streaming responses (e.g., Server-Sent Events)\\n\\nThe `WriteHeader()` method intercepts the status code before delegating to the underlying writer.\\n\\n## Middleware Composition\\n\\n```go\\nChain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler\\n```\\n\\nApplies middleware in reverse order so the first argument is the outermost layer. For example:\\n\\n```go\\nChain(handler, Recovery, Logging, CSRF)\\n```\\n\\nResults in the execution order: Recovery \u2192 Logging \u2192 CSRF \u2192 handler.\\n\\nThis is called from `cmd/psa/main.go` to wrap the main HTTP handler with all three middleware.\\n\\n## Integration Points\\n\\n- **CSRF token injection:** Templates call `CSRFTokenFromRequest()` to embed the token in the page\\n- **HTMX integration:** The token is placed in `` so HTMX automatically includes it in requests\\n- **mTLS agent routes:** Exempt from CSRF because they use certificate-based authentication\\n- **Webhook and OAuth routes:** Exempt because they use HMAC signatures or signed callbacks, not cookies\\n- **Server startup:** `cmd/psa/main.go` chains all three middleware onto the main handler\",\"internal-mobile\":\"# internal \u2014 mobile\\n\\n# Mobile PWA Module\\n\\n## Overview\\n\\nThe mobile module (`internal/mobile`) provides a Progressive Web App (PWA) interface optimized for field technicians and operations staff. Unlike a shrunken desktop view, the mobile app is purpose-built around three core workflows:\\n\\n1. **Security Ops** \u2014 Nexie alerts you to security findings; you approve or reject remediations\\n2. **Project &amp; Procurement** \u2014 Task updates, PO creation, field notes\\n3. **Time Capture** \u2014 Persistent timer linked to tickets or project tasks\\n\\nThe home screen presents a unified task feed: \\\"Here's what you need to do right now.\\\" Tasks aggregate from tickets, project tasks, and SIEM remediations into priority buckets (urgent, today, this week).\\n\\n## Architecture\\n\\n### Handler &amp; Routing\\n\\n`Handler` is the central HTTP handler for all `/m/` routes. It wraps a database pool, a template renderer, and a timesheet store:\\n\\n```go\\ntype Handler struct {\\n\\tpool     *pgxpool.Pool\\n\\trenderer *ui.Renderer\\n\\tts       *timesheet.Store\\n}\\n```\\n\\n`RegisterRoutes()` mounts all mobile endpoints on a given `*http.ServeMux`. Routes fall into four categories:\\n\\n- **Pages** \u2014 HTML views (home, security, projects, time, timesheet, settings, tickets, purchase orders)\\n- **Timer API** \u2014 Persistent timer state management (`/api/m/timer/*`)\\n- **Notification API** \u2014 Aggregated alerts (`/api/m/notifications`)\\n- **Task Feed API** \u2014 JSON task data for polling\\n\\n### Template Rendering\\n\\n`renderMobile()` is the common rendering path for all page handlers. It:\\n\\n1. Extracts user claims from the request context (name, email, initials)\\n2. Injects the active timer state into every page (so the timer widget is always current)\\n3. Detects HTMX requests and renders either a content block (for partial updates) or a full page layout\\n4. Delegates to `ui.Renderer.RenderBlock()` for template execution\\n\\nThis ensures the timer state is synchronized across all views without requiring separate API calls.\\n\\n## Core Features\\n\\n### Home \u2014 Unified Task Feed\\n\\n`handleHome()` builds a prioritized task list from three sources:\\n\\n**Tickets** \u2014 Open/in-progress tickets assigned to the user, ordered by priority (critical/high \u2192 urgent; medium \u2192 today; low \u2192 this week).\\n\\n**Project Tasks** \u2014 Tasks from assigned projects, with estimated hours displayed. Priority mapping is similar but includes a \\\"this_week\\\" bucket for low-priority items.\\n\\n**SIEM Remediations** \u2014 Pending security findings that require approval, always marked urgent.\\n\\n**Escalations** \u2014 Escalated tickets are always surfaced as urgent, even if not directly assigned.\\n\\nThe feed also displays:\\n- Today's logged hours (from `work_time_entries`)\\n- Week's total hours\\n- A banner if the user has a rejected timesheet (prompting resubmission)\\n\\n### Persistent Timer\\n\\nThe timer is the most sophisticated feature. It is:\\n\\n- **Server-side** \u2014 Timer state lives in the `work_timers` table, not the browser. A timer started on mobile appears on desktop and vice versa.\\n- **Focused** \u2014 Only one timer is \\\"focused\\\" (active) at a time. Starting a new timer backgrounds all others (pauses running ones, accumulates their time).\\n- **Linkable** \u2014 Each timer is linked to either a ticket or a project task.\\n- **Resumable** \u2014 Paused timers can be resumed; existing timers for the same ticket/task are resumed instead of duplicated.\\n\\n#### Timer Lifecycle\\n\\n**Start** (`apiStartTimer`):\\n- If a timer already exists for the target ticket/task, resume and focus it.\\n- Otherwise, background all existing timers (pause running ones, accumulate time), then create a new focused timer.\\n\\n**Pause** (`apiPauseTimer`):\\n- Accumulate elapsed time into `accumulated_seconds`.\\n- Set status to `paused`.\\n\\n**Resume** (`apiResumeTimer`):\\n- Reset `started_at` to now.\\n- Set status to `running`.\\n\\n**Stop** (`apiStopTimer`):\\n- Calculate total seconds (accumulated + elapsed since last start).\\n- Mark timer as stopped and unfocus it.\\n- Promote the next most-recently-updated non-stopped timer to focused.\\n- Create a `work_time_entries` record with the total hours.\\n- If linked to a project task, increment its `actual_hours`.\\n\\nThe `getActiveTimer()` helper retrieves the focused timer (or any active timer if none is focused) and calculates elapsed time on-the-fly for the client.\\n\\n### Timesheet Management\\n\\n`handleMobileTimesheet()` and `handleMobileTimesheetSubmit()` provide a mobile-optimized timesheet interface:\\n\\n- **Week Navigation** \u2014 URL parameter `weekStart` (ISO date) selects the week; defaults to the current week.\\n- **Day Buckets** \u2014 Entries are rolled into 7 day cells (Mon\u2013Sun) for easy scanning.\\n- **Submission** \u2014 Form post to `/m/timesheet/submit` with timesheet ID and optional notes. Delegates to `timesheet.Store.Submit()` for approval workflow.\\n- **Rejection Handling** \u2014 The home page surfaces rejected timesheets so users can resubmit.\\n\\n### Tickets &amp; Projects\\n\\n**Tickets** (`handleTickets`, `handleTicketDetail`):\\n- List view shows assigned, non-closed tickets sorted by priority.\\n- Detail view loads the full ticket, notes, and a user dropdown for reassignment.\\n\\n**Projects** (`handleProjects`, `handleProjectDetail`):\\n- Placeholder pages for project task browsing and detail views.\\n\\n### Security &amp; Purchase Orders\\n\\n**Security** (`handleSecurity`):\\n- Placeholder for SIEM findings and remediation approvals (data loaded client-side or via API).\\n\\n**Purchase Orders** (`handlePurchaseOrders`, `handlePurchaseOrderNew`):\\n- Placeholder pages for PO listing and creation.\\n\\n### Settings\\n\\n`handleSettings()` provides a minimal settings page (profile, notifications, logout).\\n\\n## API Endpoints\\n\\n### Timer API\\n\\n| Endpoint | Method | Purpose |\\n|----------|--------|---------|\\n| `/api/m/timer` | GET | Fetch active timer state |\\n| `/api/m/timer/start` | POST | Start or resume a timer |\\n| `/api/m/timer/pause` | POST | Pause the focused timer |\\n| `/api/m/timer/resume` | POST | Resume the focused timer |\\n| `/api/m/timer/stop` | POST | Stop the focused timer and create a time entry |\\n\\nAll timer endpoints return JSON with timer state and status.\\n\\n### Notification API\\n\\n| Endpoint | Method | Purpose |\\n|----------|--------|---------|\\n| `/api/m/notifications` | GET | Aggregate alerts (agents, tickets, escalations, tasks, SIEM) |\\n| `/api/m/notifications/clear` | POST | Clear notifications (stub) |\\n\\nNotifications are built from multiple sources:\\n- Stale/offline agents\\n- New tickets assigned to the user\\n- Escalated tickets (always critical severity)\\n- Project task assignments\\n- Unread notifications from the `notifications` table\\n- Pending SIEM remediations\\n\\n### Task Feed API\\n\\n| Endpoint | Method | Purpose |\\n|----------|--------|---------|\\n| `/api/m/tasks` | GET | JSON task data (currently a stub) |\\n\\n## Data Flow\\n\\n```\\nUser navigates to /m/\\n  \u2193\\nhandleHome() queries:\\n  - tickets (assigned, open/in-progress)\\n  - project_tasks (assigned, not done)\\n  - siem_analyses (pending_approval)\\n  - escalated tickets\\n  - work_time_entries (today &amp; week totals)\\n  - timesheets (rejected status)\\n  \u2193\\nrenderMobile() injects:\\n  - User claims (name, email, initials)\\n  - Active timer state (via getActiveTimer)\\n  \u2193\\nTemplate renders with data\\n  \u2193\\nClient-side JS polls /api/m/timer for timer updates\\n```\\n\\n## Authentication &amp; Tenancy\\n\\nAll handlers extract tenant and user IDs from the request context via `auth.TenantIDFromContext()` and `auth.UserIDFromContext()`. These are set by the auth middleware and ensure all queries are scoped to the current tenant and user.\\n\\nThe `handleMobileTimesheetSubmit()` handler performs a secondary ownership check (verifying the timesheet belongs to the caller) as defense in depth.\\n\\n## Helper Functions\\n\\n**`initials(name string) string`** \u2014 Extracts initials from a full name for the user avatar.\\n\\n**`formatHours(h float64) string`** \u2014 Formats hours as an integer if whole, otherwise to 1 decimal place.\\n\\n**`timeAgo(t time.Time) string`** \u2014 Converts a timestamp to a human-readable relative time (\\\"just now\\\", \\\"5 min ago\\\", \\\"2 hours ago\\\", etc.).\\n\\n## Integration Points\\n\\n- **`internal/auth`** \u2014 Extracts user claims and tenant/user IDs from context.\\n- **`internal/timesheet`** \u2014 Manages timesheet creation, entry loading, and submission.\\n- **`internal/ui`** \u2014 Renders templates with data.\\n- **Database** \u2014 Direct `pgxpool.Pool` queries for tickets, projects, timers, time entries, and notifications.\\n\\n## Design Patterns\\n\\n### Single-Timer Focus\\n\\nOnly one timer is \\\"focused\\\" at a time. This simplifies the mobile UX (one obvious timer widget) and prevents accidental double-logging. When a new timer starts, all others are backgrounded.\\n\\n### Server-Side Timer State\\n\\nTimer state lives in the database, not the browser. This ensures consistency across devices and survives browser refreshes. The client calculates elapsed time on-the-fly using `started_at` and `accumulated_seconds`.\\n\\n### Aggregated Task Feed\\n\\nThe home page pulls from multiple sources (tickets, project tasks, SIEM findings, escalations) and unifies them into a single prioritized feed. This gives field staff a single \\\"what do I do now?\\\" view instead of requiring them to check multiple screens.\\n\\n### Lazy Rendering\\n\\nPage handlers query only the data needed for that page. The timer state is injected into every render, but other data (notifications, detailed task info) is loaded on-demand via API or client-side JS.\\n\\n## Future Considerations\\n\\n- **Offline Support** \u2014 The PWA could cache task data and queue timer events for sync when connectivity returns.\\n- **Push Notifications** \u2014 Escalations and urgent remediations could trigger browser push notifications.\\n- **Real-Time Updates** \u2014 WebSocket or Server-Sent Events could push timer updates and new tasks without polling.\\n- **Geolocation** \u2014 Field staff location could be tracked for dispatch optimization (with appropriate privacy controls).\",\"internal-nexie\":\"# internal \u2014 nexie\\n\\n# Nexie: Context-Aware AI Assistant\\n\\nNexie is an AI-powered natural language interface for querying NexusOS PSA data. Users ask questions in plain English, and Nexie translates them into SQL queries, executes them against the tenant's database, and synthesizes answers using Claude.\\n\\n## Overview\\n\\nNexie operates as a multi-turn conversational assistant that bridges natural language and database queries. It's designed to be safe (read-only, tenant-isolated), context-aware (understands which module and entity the user is viewing), and cost-conscious (tracks API usage).\\n\\nThe core workflow is:\\n1. User asks a question via the `/api/nexie/ask` endpoint\\n2. Claude receives a system prompt with database schema and quick context\\n3. If Claude outputs SQL, Nexie validates and executes it\\n4. Results are fed back to Claude for a final answer\\n5. The response is returned with cost tracking\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    User[\\\"User Question\\\"]\\n    Ask[\\\"POST /api/nexie/ask\\\"]\\n    Claude1[\\\"Claude: Generate SQLor Answer\\\"]\\n    Extract[\\\"Extract SQL Blocks\\\"]\\n    Validate[\\\"Validate &amp; InjectTenant Filter\\\"]\\n    Execute[\\\"Execute Query\\\"]\\n    Claude2[\\\"Claude: Answerwith Results\\\"]\\n    Response[\\\"Reply + Cost\\\"]\\n    \\n    User --&gt; Ask\\n    Ask --&gt; Claude1\\n    Claude1 --&gt; Extract\\n    Extract --&gt;|SQL found| Validate\\n    Extract --&gt;|No SQL| Response\\n    Validate --&gt; Execute\\n    Execute --&gt; Claude2\\n    Claude2 --&gt; Response\\n```\\n\\n## Key Components\\n\\n### Handler\\n\\nThe `Handler` struct is the entry point. It manages:\\n- **pool**: PostgreSQL connection pool for database access\\n- **ai**: Interface to Claude API with cost tracking\\n- **schemaMini**: Cached database schema loaded at startup\\n\\n```go\\ntype Handler struct {\\n    pool       *pgxpool.Pool\\n    ai         AIProvider\\n    schemaMini string\\n}\\n```\\n\\n**Initialization**: `NewHandler()` loads the schema once at startup (not per-request) to minimize latency and database load.\\n\\n**Routes**:\\n- `POST /api/nexie/ask` \u2014 Main conversational endpoint\\n- `GET /api/nexie/context` \u2014 Returns quick stats for the current module/entity\\n\\n### Request/Response Types\\n\\n```go\\ntype askRequest struct {\\n    Message string        // User's question\\n    Module  string        // Current module (e.g., \\\"projects\\\", \\\"helpdesk\\\")\\n    Entity  string        // Current entity ID (e.g., project ID, ticket ID)\\n    History []chatMessage // Previous turns for multi-turn context\\n}\\n\\ntype askResponse struct {\\n    Reply string  // Nexie's answer\\n    Cost  float64 // API cost in dollars\\n    Error string  // Error message if applicable\\n}\\n```\\n\\n## Request Flow: `/api/nexie/ask`\\n\\n### Step 1: System Prompt Construction\\n\\n`buildSystemPrompt()` assembles a comprehensive prompt that includes:\\n\\n- **Instructions**: Rules for safe SQL generation, formatting, and data accuracy\\n- **User context**: Current user name, module, and entity ID\\n- **Quick context**: Lightweight stats (e.g., \\\"5 open tickets, 2 critical\\\") from `buildQuickContext()`\\n- **Database schema**: Compact schema from `schemaMini`, with common columns (id, tenant_id, created_at, updated_at) omitted\\n\\nThe prompt explicitly instructs Claude to:\\n- Output SQL in ` ```sql ... ``` ` blocks if data is needed\\n- Always include `WHERE tenant_id = $1` in queries\\n- Use only SELECT statements\\n- Limit results to 50 rows\\n\\n### Step 2: First Claude Call\\n\\nClaude receives the system prompt and user message (with conversation history if provided). It either:\\n- Answers directly from the quick context, or\\n- Outputs SQL to fetch data\\n\\n### Step 3: SQL Extraction &amp; Execution\\n\\nIf Claude's response contains SQL blocks:\\n\\n1. **Extract**: `extractSQL()` pulls all ` ```sql ... ``` ` blocks (max 5 per turn)\\n2. **Validate**: `validateQuery()` ensures:\\n   - Query starts with SELECT or WITH (CTEs allowed)\\n   - No dangerous keywords (INSERT, UPDATE, DELETE, DROP, ALTER, etc.)\\n   - No stacked queries (semicolons mid-query)\\n3. **Inject tenant filter**: `injectTenantFilter()` wraps the query in a CTE that enforces `tenant_id = $1` at the outer level, ensuring tenant isolation even if Claude forgets the WHERE clause\\n4. **Execute**: `executeQuery()` runs the query with a 5-second timeout, formats results as a markdown table, and returns up to 50 rows\\n\\n### Step 4: Second Claude Call (if SQL was executed)\\n\\nResults are fed back to Claude with the instruction: \\\"Now answer the user's original question using the data above. Be specific with numbers. Do NOT output any more SQL.\\\"\\n\\nClaude synthesizes a final answer using the real data.\\n\\n### Step 5: Response\\n\\nThe response includes:\\n- **Reply**: Nexie's answer (stripped of SQL blocks if the second call failed)\\n- **Cost**: Total API cost for both Claude calls\\n- **Error**: Any error message (e.g., \\\"AI provider not configured\\\")\\n\\n## Quick Context: Module-Specific Stats\\n\\n`buildQuickContext()` dispatches to module-specific functions that query lightweight statistics:\\n\\n| Module | Stats |\\n|--------|-------|\\n| `projects` | Total projects, active projects, current project name/status |\\n| `helpdesk` | Open tickets, critical tickets, current ticket title/status |\\n| `billing` | Outstanding invoices count and total amount |\\n| `crm` | Total clients, active clients, current client name |\\n| `purchase-orders` | Total POs, pending approval |\\n| `siem` / `security` | Total sessions, active sessions |\\n| (default) | Open tickets, active projects, active clients |\\n\\nThese stats are included in the system prompt to help Claude answer simple questions without querying.\\n\\n## Query Safety &amp; Tenant Isolation\\n\\n### Validation\\n\\n`validateQuery()` enforces read-only access:\\n- Rejects any query not starting with SELECT or WITH\\n- Blocks dangerous keywords with word-boundary checks (e.g., \\\"UPDATE\\\" in \\\"update_count\\\" is allowed, but \\\"UPDATE\\\" as a statement is not)\\n- Prevents stacked queries (multiple statements separated by semicolons)\\n\\n### Tenant Isolation\\n\\n`injectTenantFilter()` wraps every query in a CTE:\\n\\n```sql\\nWITH nexie_raw AS (\\n    \\n)\\nSELECT * FROM nexie_raw LIMIT 50\\n```\\n\\nThis ensures:\\n- Tenant ID is enforced at the outer level, regardless of joins or subqueries\\n- Results are capped at 50 rows\\n- Even if Claude forgets `WHERE tenant_id = $1`, the wrapper enforces it\\n\\nThe `$1` parameter is always the tenant ID, passed by `executeQuery()`.\\n\\n## Multi-Turn Conversation\\n\\n`buildUserMessage()` reconstructs conversation history:\\n- Keeps the last 10 messages to avoid token bloat\\n- Formats as \\\"User: ...\\\\nNexie: ...\\\\n...\\\"\\n- Appends the current user message\\n\\nThis allows Claude to maintain context across multiple questions in a single session.\\n\\n## Schema Management\\n\\n`buildSchemaPrompt()` (in `schema.go`) generates a compact schema at startup:\\n\\n1. Queries `information_schema.columns` for all public tables\\n2. Omits common columns (id, tenant_id, created_at, updated_at) since Nexie already knows they exist\\n3. Shortens data types (e.g., \\\"timestamp without time zone\\\" \u2192 \\\"ts\\\", \\\"character varying\\\" \u2192 \\\"str\\\")\\n4. Returns a single-line-per-table format:\\n\\n```\\nprojects: name(str), status(str), budget(num), start_date(date)\\ntickets: title(str), priority(str), assigned_to(uuid), resolved_at(ts)\\n...\\n```\\n\\nThis is cached in `Handler.schemaMini` and included in every system prompt.\\n\\n## Cost Tracking\\n\\nEach Claude call is tracked via `ai.UsageMeta`:\\n\\n```go\\nmeta := ai.UsageMeta{TenantID: tenantID, Action: \\\"nexie_chat\\\"}\\n```\\n\\nThe `CallClaudeRawWithCost()` method returns both the response and the cost. Nexie sums costs across both calls (if applicable) and returns the total in the response.\\n\\n## Error Handling\\n\\n- **Invalid request**: Returns 400 with \\\"message is required\\\" or \\\"invalid request\\\"\\n- **AI provider not configured**: Returns 503 with \\\"AI provider not configured\\\"\\n- **API key missing**: Returns 400 with the error from `GetAPIKey()`\\n- **Claude API error**: Returns 500 with \\\"AI request failed \u2014 check API key in Settings\\\"\\n- **Query execution error**: Logs the error and includes it in the results table (e.g., \\\"**Query 1 error:** syntax error\\\")\\n- **Second Claude call fails**: Falls back to the first response with SQL blocks stripped\\n\\n## Integration Points\\n\\n### Dependencies\\n\\n- **`internal/auth`**: `TenantIDFromContext()`, `UserIDFromContext()` for extracting claims from request context\\n- **`internal/ai`**: `AIProvider` interface for Claude API calls and `UsageMeta` for cost tracking\\n- **`github.com/jackc/pgx/v5/pgxpool`**: PostgreSQL connection pool\\n\\n### Initialization\\n\\nIn `cmd/psa/main.go`:\\n\\n```go\\nnexieHandler := nexie.NewHandler(pool)\\nnexieHandler.SetAI(claudeProvider)\\nnexieHandler.RegisterRoutes(mux)\\n```\\n\\nThe handler is created with the database pool, the AI provider is injected, and routes are registered.\\n\\n## Logging\\n\\nNexie logs key events at INFO level:\\n\\n- Schema load size at startup\\n- Query execution details: user ID, module, query count, cost, elapsed time\\n- Query errors with truncated SQL (first 100 chars)\\n- Claude API errors with elapsed time\\n\\nExample:\\n```\\n[nexie] Schema loaded: 4521 bytes\\n[nexie] user=abc123 module=projects queries=1 cost=$0.0042 (1.2s)\\n```\\n\\n## Limitations &amp; Design Decisions\\n\\n1. **One SQL block per response**: Claude is instructed to output only one SQL block per turn. This simplifies parsing and prevents accidental multi-query attacks.\\n\\n2. **50-row limit**: Results are capped at 50 rows to prevent massive payloads and token usage. Claude is expected to use aggregations (COUNT, SUM, AVG) for larger datasets.\\n\\n3. **5-second query timeout**: Prevents runaway queries from blocking the request.\\n\\n4. **Schema cached at startup**: The schema is loaded once and never refreshed. If the database schema changes, the service must be restarted.\\n\\n5. **No parameterized user input in SQL**: Claude generates SQL directly. The tenant ID is injected as `$1` by the wrapper, but other user inputs (e.g., entity IDs) are not parameterized. This is acceptable because Claude is trusted to generate safe SQL, and the validation layer catches dangerous patterns.\\n\\n6. **Markdown table output**: Query results are formatted as markdown tables for readability in the UI. Very long values (&gt;80 chars) are truncated.\\n\\n## Testing Considerations\\n\\nWhen testing Nexie:\\n\\n- Mock the `AIProvider` interface to avoid Claude API calls\\n- Test `validateQuery()` with both safe and dangerous SQL patterns\\n- Verify `injectTenantFilter()` wraps queries correctly\\n- Test `extractSQL()` with various markdown formats (extra whitespace, multiple blocks)\\n- Verify tenant isolation by checking that `$1` is always passed as the tenant ID\\n- Test multi-turn conversation by providing a history array\\n- Verify quick context queries return expected stats for each module\",\"internal-nexiq\":\"# internal \u2014 nexiq\\n\\n# NexusIQ Module\\n\\nThe NexusIQ module is a business intelligence and sales management system that provides goal-setting, pipeline visibility, and real-time sales metrics. It serves four primary user personas: Owners (strategic planning), Sales Reps (deal management), Managers (team oversight), and the system itself (forecasting).\\n\\n## Architecture Overview\\n\\nNexusIQ is organized around five core surfaces:\\n\\n- **Plan Cascade**: Owner's annual goal-setting artifact with financial targets and strategic initiatives\\n- **Lever Lab**: Scenario modeling and financial projections\\n- **Sales Workspace**: Rep-facing deal management with PBR (Pipeline Business Review) command-and-control\\n- **Sales Cockpit**: Manager's real-time roll-up dashboard (PBR, activity, time, commissions)\\n- **Gut Check**: Owner's self-assessment health scoring\\n\\nAll surfaces are tenant-scoped and permission-gated (see `permissions.go`). The module integrates deeply with the deals, companies, work timers, and bid commission systems.\\n\\n## Core Components\\n\\n### Handler &amp; Routing\\n\\n`Handler` in `handler.go` is the central HTTP router. It owns:\\n- A connection pool to the database\\n- A UI renderer for page templates\\n- An `EventHub` for real-time Cockpit updates\\n\\n```go\\ntype Handler struct {\\n\\tpool     *pgxpool.Pool\\n\\trenderer *ui.Renderer\\n\\thub      *EventHub\\n}\\n```\\n\\n`RegisterRoutes()` wires all HTTP endpoints:\\n- Page handlers (GET /nexiq/*)\\n- REST APIs (GET/POST/PATCH/DELETE /api/nexiq/*)\\n- SSE stream (GET /nexiq/cockpit/stream)\\n\\n### Event Hub &amp; Real-Time Updates\\n\\nThe `EventHub` (in `events.go`) is an in-process pub/sub system that fans Workspace edits out to Cockpit subscribers over Server-Sent Events (SSE).\\n\\n**Key design:**\\n- Per-tenant subscription only (tenant A never sees tenant B's events)\\n- Non-blocking publish: slow subscribers drop events rather than stall the publisher\\n- Buffered channels (16) absorb brief client stalls\\n- Phase 1 is single-process; multi-replica deployments should back this with Redis\\n\\n**Event types** (emitted by Workspace, consumed by Cockpit):\\n- `deal.created` \u2014 new deal in PBR\\n- `deal.updated` \u2014 PBR fields changed (stage, MRR, probability, etc.)\\n- `deal.won` / `deal.lost` \u2014 stage transition\\n- `activity.*` \u2014 CRM activity logged\\n- `time.*` \u2014 work timer entry\\n\\nThe `handleCockpitStream()` endpoint subscribes to the hub and streams events as JSON over SSE with a 20-second heartbeat.\\n\\n### Plan Cascade\\n\\nThe cascade is the Owner's annual goal-setting artifact. One row per tenant per period (FY2026, etc.).\\n\\n**Data model:**\\n- `nexiq_plan_cascades`: period label, date range, vision statement, financial targets (net worth, income, net profit, revenue, MRR), plan health score\\n- `nexiq_plan_initiatives`: top-N items attached to a cascade (title, owner, due quarter, status, progress %, notes)\\n\\n**APIs:**\\n- `GET /api/nexiq/cascade?period=FY2026` \u2014 fetch cascade + initiatives (most recent if no period specified)\\n- `POST /api/nexiq/cascade` \u2014 upsert cascade (ON CONFLICT by tenant + period dates)\\n- `POST /api/nexiq/cascade/initiatives` \u2014 create or update initiative\\n- `DELETE /api/nexiq/cascade/initiatives/{id}` \u2014 remove initiative\\n\\nThe cascade's `plan_health_score` is synced from the latest full Gut Check submission.\\n\\n### Sales Goals\\n\\nGoals are per-rep, per-period targets set by the Owner. Reps see them in the Workspace goal-strip.\\n\\n**Data model:**\\n- `sales_goals`: user_id, period_start/end, MRR/ORR/NRR targets, commission target, weekly hours target, notes\\n\\n**APIs:**\\n- `GET /api/nexiq/goals` \u2014 list all goals for the tenant\\n- `GET /api/nexiq/goals/{userID}` \u2014 get the active goal for a single rep\\n- `POST /api/nexiq/goals` \u2014 upsert (ON CONFLICT by tenant + user + period)\\n- `DELETE /api/nexiq/goals/{id}` \u2014 remove goal\\n\\nDefault weekly hours target is 40 if not specified.\\n\\n### Sales Workspace\\n\\nThe rep-facing deal management surface. Reps create, edit, and track deals with PBR Command-and-Control fields.\\n\\n**Deal model** (`workspaceDeal`):\\n- Core fields: title, company, stage, value, recurring_value (MRR), win_probability, owner, close date\\n- PBR fields: section, bucket, seats, product label, last/next step (text + date), pbr_entered_at\\n- Computed: `Missing` list of unfilled PBR fields (MRR, seats, close date, source, bucket, next step, product)\\n\\n**APIs:**\\n- `GET /api/nexiq/workspace/deals` \u2014 deals owned by current user (limit 200)\\n- `GET /api/nexiq/workspace/deals/{id}` \u2014 single deal detail\\n- `POST /api/nexiq/workspace/deals` \u2014 quick-create from New Deal modal\\n- `PATCH /api/nexiq/workspace/deals/{id}/pbr` \u2014 partial update (all fields optional)\\n- `GET /api/nexiq/workspace/companies` \u2014 dropdown list for New Deal modal\\n- `GET /api/nexiq/workspace/stages` \u2014 open deal stages (not won/lost)\\n- `GET /api/nexiq/workspace/my-time` \u2014 WTD hours grouped by sales_category\\n\\n**PBR PATCH behavior:**\\n- Builds dynamic UPDATE with only supplied fields\\n- Emits `deal.updated` event to the hub so Cockpit subscribers see changes instantly\\n- Clears date fields if empty string is sent\\n\\n**Deal creation:**\\n- Validates company exists and belongs to tenant\\n- Defaults title to company name if not supplied\\n- Defaults stage to first by sort_order if not supplied\\n- Sets owner to current user\\n- Sets pbr_entered_at to now()\\n\\n### Sales Cockpit\\n\\nManager's read-only roll-up dashboard. All endpoints are live mirrors of work happening elsewhere.\\n\\n**Roll-ups:**\\n\\n1. **PBR** (`apiCockpitPBR`): Open deals grouped by stage + sales rep\\n   - Aggregates: deal count, pipeline MRR, weighted MRR (recurring_value \u00d7 win_probability/100), pipeline value\\n   - Excludes won/lost stages\\n   - Ordered by stage sort_order, then stage name, then owner\\n\\n2. **Cookbook** (`apiCockpitCookbook`): Last 7 days of CRM activity by performer + type\\n   - Counts activity records from `crm_activities` table\\n   - Grouped by performed_by user + activity_type\\n   - Window is hardcoded to 7 days\\n\\n3. **Sales Time** (`apiCockpitSalesTime`): Week-to-date hours per rep\\n   - Calculates Monday of current week (server local time)\\n   - Splits hours by: on-deal (deal_id IS NOT NULL), admin (sales_category = 'admin'), other\\n   - Ordered by total hours descending\\n\\n4. **YTD** (`apiCockpitYTD`): Year-to-date commission dollars per rep\\n   - Joins `v_user_commissions` + `bid_sheets`\\n   - Splits by: total, won (status = 'won'), open (status not in won/lost/expired)\\n   - Filtered to calendar year\\n\\nAll Cockpit endpoints are read-only and tenant-scoped.\\n\\n### Lever Lab (Scenarios)\\n\\nScenario modeling for financial projections. A scenario captures lever values + a 24-month projection.\\n\\n**Data model:**\\n- `nexiq_lever_scenarios`: name, description, levers (JSON), projection (JSON), is_baseline flag\\n\\n**APIs:**\\n- `GET /api/nexiq/scenarios` \u2014 list scenarios (baseline first, then by updated_at DESC)\\n- `POST /api/nexiq/scenarios` \u2014 create scenario\\n- `PUT /api/nexiq/scenarios/{id}` \u2014 update scenario\\n- `POST /api/nexiq/scenarios/{id}/project` \u2014 compute and store projection\\n\\n**Projection compute** (`apiProjectScenario`):\\n- Simple monthly compounding: MRR grows by monthly_growth_pct each month\\n- Revenue = MRR \u00d7 12\\n- Net profit = Revenue \u00d7 net_margin_pct\\n- Defaults to 24 months, capped at 60\\n- Returns array of {month, mrr, revenue, net_profit} points\\n\\n**Baseline logic:**\\n- Only one baseline per tenant (enforced by partial unique index)\\n- Creating/updating a scenario with is_baseline=true demotes the existing baseline\\n\\n### Gut Check (Plan Health)\\n\\nOwner's self-assessment health scoring. Drives the Plan Health pill on Plan Cascade.\\n\\n**Scoring:**\\n- Full form: 14 questions \u00d7 5 points = max 70\\n- Quick re-take: 5 questions \u00d7 5 points = max 25\\n- Each answer is clamped to [0, 5]\\n- Score is the sum of all answers\\n\\n**APIs:**\\n- `POST /api/nexiq/gut-check` \u2014 submit assessment\\n- `GET /api/nexiq/gut-check/latest` \u2014 fetch most recent submission\\n\\n**Behavior:**\\n- Stores answers as JSON map\\n- If full form (not quick re-take) and cascade_id supplied, syncs score to cascade's plan_health_score\\n- Returns {id, score, max_score}\\n\\n## Data Flow &amp; Integration Points\\n\\n```\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502                    Sales Workspace (Rep)                     \u2502\\n\u2502  \u2022 Create/edit deals (PBR fields)                            \u2502\\n\u2502  \u2022 View my goals, my time                                    \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                     \u2502 PATCH /api/nexiq/workspace/deals/{id}/pbr\\n                     \u2502 Emits deal.updated event\\n                     \u25bc\\n            \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n            \u2502   EventHub         \u2502\\n            \u2502  (in-process SSE)  \u2502\\n            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                     \u2502 Publish(Event)\\n                     \u25bc\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502              Sales Cockpit (Manager)                         \u2502\\n\u2502  \u2022 PBR roll-up (deals by stage + rep)                        \u2502\\n\u2502  \u2022 Cookbook (7-day activity)                                 \u2502\\n\u2502  \u2022 Sales Time (WTD hours)                                    \u2502\\n\u2502  \u2022 YTD (commission dollars)                                  \u2502\\n\u2502  \u2022 SSE stream: /nexiq/cockpit/stream                         \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502              Plan Cascade (Owner)                            \u2502\\n\u2502  \u2022 Set annual goals, vision, financial targets              \u2502\\n\u2502  \u2022 Attach initiatives (top-N items)                          \u2502\\n\u2502  \u2022 View plan health score (from Gut Check)                   \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502              Lever Lab (Owner)                               \u2502\\n\u2502  \u2022 Create scenarios with lever values                        \u2502\\n\u2502  \u2022 Project 24-month financials (simple compounding)          \u2502\\n\u2502  \u2022 Compare baseline vs. alternatives                         \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502              Gut Check (Owner)                               \u2502\\n\u2502  \u2022 Submit health assessment (14 or 5 questions)              \u2502\\n\u2502  \u2022 Score syncs to Plan Cascade                               \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n```\\n\\n**Cross-module dependencies:**\\n- **deals** table: PBR roll-up reads stage, owner, value, recurring_value, win_probability\\n- **deal_stages** table: stage names, sort_order, is_won/is_lost flags\\n- **companies** table: company names for deal creation\\n- **users** table: rep names for deal owner display\\n- **work_time_entries** table: Cockpit sales-time roll-up, Workspace my-time\\n- **crm_activities** table: Cockpit cookbook roll-up\\n- **bid_sheets** + **v_user_commissions**: Cockpit YTD roll-up\\n- **sales_goals** table: rep targets (set by Owner, viewed by Workspace)\\n\\n## Authentication &amp; Authorization\\n\\nAll endpoints extract `tenantID` and `userID` from the request context via `auth.TenantIDFromContext()` and `auth.UserIDFromContext()`. Missing tenant ID returns 401 Unauthorized.\\n\\n**Permission constants** (in `permissions.go`):\\n- `PermPlanView` / `PermPlanEdit` \u2014 Plan Cascade access\\n- `PermLeverView` / `PermLeverEdit` \u2014 Lever Lab access\\n- `PermGutCheckSubmit` \u2014 Gut Check submit\\n- `PermWorkspaceUse` \u2014 Sales Workspace deal edits\\n- `PermCockpitView` \u2014 Sales Cockpit read + SSE\\n- `PermGoalsAdmin` \u2014 Set/edit per-rep goals\\n\\n**Note:** Permissions are not yet enforced by middleware. Once the role admin UI lands, flip routes to use `auth.RequirePermission(...)`.\\n\\n## Error Handling\\n\\nAll endpoints follow a consistent pattern:\\n1. Extract tenant/user from context; return 401 if missing\\n2. Decode JSON request body; return 400 if invalid\\n3. Validate required fields; return 400 if missing\\n4. Execute database operation; return 500 if error\\n5. Return 200/201/204 on success\\n\\nHelper functions:\\n- `writeJSON(w, status, v)` \u2014 marshal v to JSON and write with status\\n- `writeErr(w, status, msg)` \u2014 write error object {error: msg}\\n\\n## Testing\\n\\n`events_test.go` includes two tests:\\n- `TestEventHubFanOutPerTenant`: Verifies tenant isolation (tenant A events don't leak to tenant B)\\n- `TestEventHubUnsubscribe`: Verifies cleanup when last subscriber unsubscribes\\n\\n## Common Patterns\\n\\n**Upsert with ON CONFLICT:**\\nCascade, goals, and scenarios use PostgreSQL `ON CONFLICT ... DO UPDATE` to implement upsert semantics. This ensures idempotent API calls.\\n\\n**Dynamic SQL building:**\\nThe PBR PATCH endpoint builds UPDATE statements dynamically based on supplied fields, avoiding unnecessary column updates.\\n\\n**Nullable pointers:**\\nOptional fields use `*string` and `*int` to distinguish \\\"not supplied\\\" from \\\"explicitly cleared\\\". Empty string clears date fields; nil clears owner/section fields.\\n\\n**Tenant scoping:**\\nEvery query includes `WHERE tenant_id = $1` to enforce isolation. This is non-negotiable.\\n\\n**Computed fields:**\\nThe `Missing` list on deals is computed client-side by `pbrMissing()` based on PBR field presence. This drives the Workspace UI's \\\"complete your deal\\\" prompts.\\n\\n## Future Work\\n\\n- **Phase 2B**: Lever effects engine moves to dedicated service; modeling depth increases\\n- **Multi-replica**: EventHub should back to Redis pub/sub for distributed deployments\\n- **Permission enforcement**: Middleware should gate routes with `auth.RequirePermission(...)`\\n- **Audit logging**: Track cascade/goal/scenario changes for compliance\",\"internal-notify\":\"# internal \u2014 notify\\n\\n# notify \u2014 Notification Service\\n\\nThe `notify` package provides a centralized notification system for NexusOS. Any module can send notifications to users, roles, or tenant-wide audiences without direct dependencies on other packages. Notifications support rich metadata, action buttons, and severity levels.\\n\\n## Overview\\n\\nThe service persists notifications to a PostgreSQL database and handles routing to different user groups. It includes pre-built templates for common domain events (assessments, quotes, approvals, orchestrator actions).\\n\\n**Key responsibilities:**\\n- Create and persist notifications for individual users\\n- Route notifications to user groups (by role, admin-only, or tenant-wide)\\n- Provide domain-specific notification templates\\n- Normalize severity and source fields\\n\\n## Core Components\\n\\n### Service\\n\\n```go\\ntype Service struct {\\n\\tpool *pgxpool.Pool\\n}\\n```\\n\\nThe `Service` manages all notification operations. Initialize it once at startup:\\n\\n```go\\nsvc := notify.NewService(dbPool)\\n```\\n\\n### Notification\\n\\n```go\\ntype Notification struct {\\n\\tTenantID  string\\n\\tUserID    string\\n\\tType      string\\n\\tTitle     string\\n\\tMessage   string\\n\\tSeverity  string // \\\"info\\\", \\\"warning\\\", \\\"critical\\\"\\n\\tLink      string\\n\\tEntityType string\\n\\tEntityID   string\\n\\t\\n\\tActionURL            string\\n\\tActionType           string\\n\\tActionLabel          string\\n\\tSecondaryActionURL   string\\n\\tSecondaryActionLabel string\\n\\t\\n\\tSource string // \\\"system\\\", \\\"orchestrator\\\", \\\"user\\\", \\\"module\\\"\\n}\\n```\\n\\nA `Notification` describes what to send. Fields like `Severity` and `Source` default if omitted (`\\\"info\\\"` and `\\\"system\\\"` respectively).\\n\\n**Action buttons** are optional. When both `ActionURL` and `ActionLabel` are set, the notification includes a primary action button. A secondary action is added if `SecondaryActionURL` and `SecondaryActionLabel` are provided.\\n\\n## API\\n\\n### Send(ctx, notification) error\\n\\nSends a notification to a single user.\\n\\n```go\\nerr := svc.Send(ctx, notify.Notification{\\n\\tTenantID: \\\"tenant-123\\\",\\n\\tUserID:   \\\"user-456\\\",\\n\\tType:     \\\"custom_event\\\",\\n\\tTitle:    \\\"Something happened\\\",\\n\\tMessage:  \\\"Details here\\\",\\n\\tSeverity: \\\"info\\\",\\n})\\n```\\n\\nDefaults `Severity` to `\\\"info\\\"` and `Source` to `\\\"system\\\"` if not provided. Logs errors but does not return them to the caller (fire-and-forget semantics).\\n\\n### SendToRole(ctx, tenantID, role, notification) error\\n\\nSends a notification to all active users with a specific role.\\n\\n```go\\nerr := svc.SendToRole(ctx, \\\"tenant-123\\\", \\\"analyst\\\", notify.Notification{\\n\\tType:    \\\"data_ready\\\",\\n\\tTitle:   \\\"New data available\\\",\\n\\tMessage: \\\"Your dataset is ready for analysis\\\",\\n})\\n```\\n\\nQueries the `users` table for active users matching the role, then calls `Send` for each.\\n\\n### SendToAdmins(ctx, tenantID, notification) error\\n\\nSends a notification to all active admin users in a tenant.\\n\\n```go\\nerr := svc.SendToAdmins(ctx, \\\"tenant-123\\\", notify.Notification{\\n\\tType:     \\\"system_alert\\\",\\n\\tTitle:    \\\"Critical issue detected\\\",\\n\\tSeverity: \\\"critical\\\",\\n})\\n```\\n\\nAttempts to look up the admin role by name in the `roles` table. If found, filters users by `role_id`. Falls back to all active users if the role lookup fails.\\n\\n### SendToAllUsers(ctx, tenantID, notification) error\\n\\nSends a notification to every active user in a tenant.\\n\\n```go\\nerr := svc.SendToAllUsers(ctx, \\\"tenant-123\\\", notify.Notification{\\n\\tType:    \\\"maintenance\\\",\\n\\tTitle:   \\\"Scheduled maintenance\\\",\\n\\tMessage: \\\"System will be down for 2 hours\\\",\\n})\\n```\\n\\n## Pre-built Templates\\n\\nThe service includes domain-specific notification builders for common events. These enforce consistent messaging and routing.\\n\\n### AssessmentReadyForReview\\n\\n```go\\nsvc.AssessmentReadyForReview(ctx, tenantID, assessmentID, companyName, riskScore, aiCost)\\n```\\n\\nNotifies admins that an AI assessment is complete. Severity is derived from risk score:\\n- `score &gt;= 80` \u2192 `\\\"critical\\\"`\\n- `score &gt;= 60` \u2192 `\\\"warning\\\"`\\n- otherwise \u2192 `\\\"info\\\"`\\n\\nIncludes a link to the assessment analysis tab and an \\\"Approve\\\" action button.\\n\\n### QuoteGeneratedFromAssessment\\n\\n```go\\nsvc.QuoteGeneratedFromAssessment(ctx, tenantID, quoteID, assessmentID, companyName)\\n```\\n\\nNotifies admins that a quote was generated from an assessment. Severity is always `\\\"info\\\"`.\\n\\n### ApprovalRequired\\n\\n```go\\nsvc.ApprovalRequired(ctx, tenantID, userID, entityType, entityID, title, message, approveURL, rejectURL)\\n```\\n\\nSends an approval notification to a specific user with \\\"Approve\\\" and \\\"Reject\\\" action buttons. Severity is `\\\"warning\\\"`.\\n\\n### OrchestratorEventFired\\n\\n```go\\nsvc.OrchestratorEventFired(ctx, tenantID, category, eventType, entityID, detail)\\n```\\n\\nNotifies admins about an orchestrator event (informational). Severity is `\\\"info\\\"`.\\n\\n### OrchestratorActionFailed\\n\\n```go\\nsvc.OrchestratorActionFailed(ctx, tenantID, action, entityID, errMsg)\\n```\\n\\nNotifies admins about a failed orchestrator action. Severity is `\\\"critical\\\"`.\\n\\n## Database Schema\\n\\nNotifications are persisted to a `notifications` table with the following columns:\\n\\n- `tenant_id` (string)\\n- `user_id` (UUID)\\n- `type` (string) \u2014 notification type key\\n- `title` (string)\\n- `message` (string)\\n- `severity` (string) \u2014 `\\\"info\\\"`, `\\\"warning\\\"`, or `\\\"critical\\\"`\\n- `link` (string) \u2014 optional URL\\n- `entity_type` (string) \u2014 optional entity category\\n- `entity_id` (string) \u2014 optional entity identifier\\n- `action_url` (string) \u2014 optional primary action endpoint\\n- `action_type` (string) \u2014 optional action type (e.g., `\\\"approve\\\"`)\\n- `action_label` (string) \u2014 optional button label\\n- `secondary_action_url` (string) \u2014 optional secondary action endpoint\\n- `secondary_action_label` (string) \u2014 optional secondary button label\\n- `source` (string) \u2014 origin of the notification\\n\\n## Usage Patterns\\n\\n### From Assessment Module\\n\\nThe assessment module uses `ApprovalRequired` to notify users when an assessment needs approval:\\n\\n```go\\nsvc.ApprovalRequired(ctx, tenantID, userID, \\\"assessment\\\", assessmentID,\\n\\t\\\"Assessment Approval Required\\\",\\n\\t\\\"An assessment is pending your review\\\",\\n\\t\\\"/api/assessments/\\\" + assessmentID + \\\"/approve\\\",\\n\\t\\\"/api/assessments/\\\" + assessmentID + \\\"/reject\\\")\\n```\\n\\n### From Orchestrator\\n\\nThe orchestrator sends notifications for workflow events and errors:\\n\\n```go\\n// Event notification\\nsvc.OrchestratorEventFired(ctx, tenantID, \\\"assessment\\\", \\\"analysis_complete\\\", assessmentID, \\\"AI analysis finished\\\")\\n\\n// Error notification\\nsvc.OrchestratorActionFailed(ctx, tenantID, \\\"generate_quote\\\", assessmentID, \\\"Failed to generate quote: API timeout\\\")\\n```\\n\\n### From Main Application\\n\\nThe main application initializes the service and may broadcast tenant-wide notifications:\\n\\n```go\\nsvc.SendToAllUsers(ctx, tenantID, notify.Notification{\\n\\tType:    \\\"system_maintenance\\\",\\n\\tTitle:   \\\"Maintenance Window\\\",\\n\\tMessage: \\\"System will be unavailable from 2-4 AM UTC\\\",\\n})\\n```\\n\\n## Error Handling\\n\\n- `Send` logs errors but does not return them (fire-and-forget). Callers should not rely on error handling for individual sends.\\n- `SendToRole`, `SendToAdmins`, and `SendToAllUsers` return errors from the initial query but continue sending to users even if individual sends fail.\\n- Database errors are logged via the standard `log` package.\\n\\n## Design Notes\\n\\n**No async queue:** Notifications are written synchronously to the database. For high-volume scenarios, consider adding a background worker that batches inserts.\\n\\n**Tenant isolation:** All methods require `tenantID` to ensure notifications stay within tenant boundaries.\\n\\n**Role lookup fallback:** `SendToAdmins` gracefully degrades if the admin role is not found, falling back to all active users. This prevents notification loss in misconfigured systems.\\n\\n**Utility functions:** `severityFromRisk` and `itoa` are internal helpers for template builders. They avoid external dependencies for simple conversions.\",\"internal-onboarding\":\"# internal \u2014 onboarding\\n\\n# Internal Onboarding Module\\n\\n## Overview\\n\\nThe onboarding module automates the 30-day client onboarding lifecycle, from MSA signature through compliance framework assignment. It eliminates manual busywork by orchestrating a structured 9-phase pipeline where each phase seeds checklist items, injects AI-recommended tasks from the assessment blueprint, and fires automation hooks (emails, tickets, RMM operations).\\n\\n**Core principle:** Generic templates provide structure; blueprint data provides specificity. When a client blueprint exists (output from the assessment pipeline), each phase gets actionable, AI-analyzed work items instead of placeholders.\\n\\n---\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    A[\\\"MSA Signed(e-sign webhook)\\\"] --&gt;|HandleMSATrigger| B[\\\"Create OnboardingRecord\\\"]\\n    B --&gt; C[\\\"Seed Phase 0Templates\\\"]\\n    C --&gt; D[\\\"Load Blueprintif exists\\\"]\\n    D --&gt; E[\\\"Inject BlueprintItems\\\"]\\n    E --&gt; F[\\\"Create ScheduledOperations\\\"]\\n    F --&gt; G[\\\"Fire Phase Hooksbackground\\\"]\\n    G --&gt;|Phase 0| H[\\\"RMM Token\\\"]\\n    G --&gt;|Phase 1| I[\\\"Welcome Email\\\"]\\n    G --&gt;|Phase 2| J[\\\"RMM Ticket\\\"]\\n    G --&gt;|Phase 3| K[\\\"Discovery Ticket\\\"]\\n    G --&gt;|Phase 4| L[\\\"Security Brief\\\"]\\n    G --&gt;|Phase 7| M[\\\"Hardware Audit\\\"]\\n    G --&gt;|Phase 8| N[\\\"Compliance Gap\\\"]\\n    \\n    O[\\\"handleAdvancePhase\\\"] --&gt;|Next Phase| C\\n    O --&gt; E\\n    O --&gt; G\\n```\\n\\n---\\n\\n## Key Components\\n\\n### Handler (`handler.go`)\\n\\nThe HTTP request router and primary orchestrator. Manages the onboarding lifecycle through REST endpoints and page renders.\\n\\n**Critical methods:**\\n\\n- **`HandleMSATrigger`** \u2014 Webhook entry point (Phase 0). Receives MSA signature event, creates onboarding record, seeds templates, injects blueprint data, and fires automation. Idempotent via `contract_id` dedup key.\\n\\n- **`handleAdvancePhase`** \u2014 Moves to next phase. Validates blocking items are complete, updates phase timestamps, seeds new phase items, injects blueprint data, auto-completes system items, and fires phase hooks.\\n\\n- **`handleDetail`** \u2014 Renders onboarding detail page with full checklist, audit log, and phase timeline. Computes progress and determines if phase can advance.\\n\\n- **`handleCompleteItem` / `handleUncompleteItem`** \u2014 Marks individual checklist items done/undone. Only auto-complete items cannot be uncompleted.\\n\\n- **`handleCompleteOnboarding`** \u2014 Marks entire onboarding as completed (only when on Phase 8 with all blocking items done).\\n\\n- **`seedPhaseItems`** \u2014 Inserts default checklist items from `onboarding_checklist_templates` table for a given phase.\\n\\n- **`auditLog`** \u2014 Appends immutable audit entries. INSERT-ONLY, never updated or deleted. Tracks all state changes and user actions.\\n\\n**Security:** Every database query includes `WHERE tenant_id = $1` to enforce tenant isolation. Webhook handler is idempotent.\\n\\n---\\n\\n### Blueprint Injection (`blueprint_injection.go`)\\n\\nBridges the assessment pipeline to onboarding. Loads AI-analyzed client blueprints and injects specific, actionable tasks into each phase.\\n\\n**Data flow:**\\n\\n1. `loadBlueprint` queries `client_blueprints` table for a company\\n2. `injectBlueprintItems` dispatches to phase-specific injectors\\n3. Each injector (e.g., `injectPhase4SecurityItems`) unmarshals JSON, formats titles/descriptions, and inserts via `insertBlueprintItem`\\n\\n**Phase-specific injectors:**\\n\\n- **Phase 4 (Security):** Deploys security products from `SecurityStack` (firewalls, EDR, SIEM)\\n- **Phase 5 (Tools):** Deploys SaaS/software from `SoftwareStack` (per-user, per-device, per-org)\\n- **Phase 7 (Hardware):** Applies hardware standards from `HardwareStandards` (device types, specs, refresh cycles)\\n- **Phase 8 (Compliance):** Assigns compliance frameworks from `ComplianceFrameworks` and auto-inserts into `compliance_assignments` table\\n\\n**Scheduled Operations:** `injectScheduledOps` creates recurring operations (vulnerability scans, patch cycles, etc.) from blueprint. Runs once at onboarding start (Phase 0). Auto-approves low-risk ops (scans, patches); requires approval for pen tests.\\n\\n**Generic vs. Blueprint items:** Generic template items remain as category headers (sort_order &lt; 100). Blueprint items are injected with sort_order &gt;= 100, appearing below generics. Both are stored in `onboarding_checklist_items` with `source` metadata.\\n\\n---\\n\\n### Phase Automation (`phases.go`)\\n\\nBackground goroutines that execute phase-specific work after checklist items are seeded. All hooks are best-effort; failures are logged but don't block phase advancement.\\n\\n**Hook execution:** `runPhaseHooks` spawns a 5-minute timeout goroutine for each phase. Hooks run *after* `seedPhaseItems` and `injectBlueprintItems` have completed and auto-complete items are marked done.\\n\\n**Phase hooks:**\\n\\n| Phase | Hook | Action |\\n|-------|------|--------|\\n| 0 | `hookPhase0RMM` | Generate RMM enrollment token if missing |\\n| 1 | `hookPhase1Welcome` | Send welcome email to company contact; create manual ticket if no email on file |\\n| 2 | `hookPhase2RMM` | Create helpdesk ticket with RMM deployment instructions and enrollment token |\\n| 3 | `hookPhase3Discovery` | Query agents table, generate network discovery report, create ticket with device inventory |\\n| 4 | `hookPhase4Security` | Query agents, count by OS, generate Day 1 Security Brief, create ticket |\\n| 7 | `hookPhase7Hardware` | Query agents, flag EOL devices, generate hardware audit report, create ticket |\\n| 8 | `hookPhase8Compliance` | Load blueprint or infer frameworks from industry, auto-assign frameworks, create gap analysis ticket |\\n\\n**Ticket creation:** `createOnboardingTicket` inserts into `tickets` table with `source='system'` and `tags=['onboarding']`. Techs see these as actionable work items in the helpdesk.\\n\\n**Email sending:** Phase 1 uses `billing.EmailSender` to send dark-branded HTML welcome emails. Falls back to manual ticket if no contact email.\\n\\n---\\n\\n## Data Model\\n\\n### Core Tables\\n\\n**`client_onboarding`**\\n- `id, tenant_id, company_id, contract_id` \u2014 identifiers\\n- `status` \u2014 active | paused | completed | cancelled\\n- `current_phase` \u2014 0\u20138\\n- `msa_signed_at, target_completion` \u2014 timeline\\n- `phase_N_started_at, phase_N_completed_at` \u2014 per-phase timestamps (N=0..8)\\n- `assigned_tech_id, notes` \u2014 tech assignment and internal notes\\n\\n**`onboarding_checklist_items`**\\n- `onboarding_id, phase, category` \u2014 grouping\\n- `title, description` \u2014 display\\n- `blocks_phase` \u2014 if true, must complete before advancing\\n- `auto_complete` \u2014 if true, auto-marked done at phase start\\n- `assigned_to_id, due_at, completed_at, completed_by_id` \u2014 tracking\\n- `sort_order` \u2014 display order (&lt; 100 = generic, &gt;= 100 = blueprint)\\n- `metadata` \u2014 JSON, includes `source: \\\"blueprint\\\"` for injected items\\n\\n**`onboarding_checklist_templates`**\\n- Global templates for each phase (tenant_id IS NULL = system-wide)\\n- Seeded into `onboarding_checklist_items` at phase start\\n\\n**`onboarding_audit_log`**\\n- INSERT-ONLY immutable log\\n- `event_type` \u2014 webhook_received, phase_started, phase_completed, item_completed, status_changed, onboarding_completed, etc.\\n- `actor_id, actor_label` \u2014 who triggered the event\\n- `payload` \u2014 JSON context (frameworks, device counts, etc.)\\n\\n**`client_blueprints`**\\n- Output from assessment pipeline\\n- `hardware_standards, software_stack, security_stack, compliance_frameworks, scheduled_operations` \u2014 all JSONB\\n- Loaded by `loadBlueprint`, injected by phase-specific handlers\\n\\n---\\n\\n## Request Flow: MSA Trigger to Phase 0 Complete\\n\\n```\\nPOST /api/onboarding/trigger\\n  \u2193\\nHandleMSATrigger\\n  \u251c\u2500 Parse payload (contract_id, company_id, tenant_id, signed_at)\\n  \u251c\u2500 Check idempotency (contract_id already triggered?)\\n  \u251c\u2500 INSERT client_onboarding record (status=active, phase=0)\\n  \u251c\u2500 seedPhaseItems(phase=0) \u2192 INSERT generic templates\\n  \u251c\u2500 loadBlueprint(company_id)\\n  \u251c\u2500 injectBlueprintItems(phase=0) \u2192 INSERT blueprint items\\n  \u251c\u2500 injectScheduledOps() \u2192 INSERT scheduled_operations\\n  \u251c\u2500 UPDATE auto_complete items to completed_at=now()\\n  \u251c\u2500 auditLog(\\\"webhook_received\\\", \\\"phase_started\\\")\\n  \u251c\u2500 runPhaseHooks(phase=0) [background]\\n  \u2502   \u2514\u2500 hookPhase0RMM() \u2192 INSERT rmm_enrollment_tokens\\n  \u2514\u2500 Return { ok: true, onboarding_id, phase: 0 }\\n```\\n\\n---\\n\\n## Request Flow: Advance Phase\\n\\n```\\nPOST /api/onboarding/{id}/advance\\n  \u2193\\nhandleAdvancePhase\\n  \u251c\u2500 Load current_phase, status\\n  \u251c\u2500 Validate status=active, phase &lt; 8\\n  \u251c\u2500 Check blocking items in current phase are complete\\n  \u251c\u2500 UPDATE current_phase++, set phase_N_completed_at, phase_N+1_started_at\\n  \u251c\u2500 seedPhaseItems(phase=N+1)\\n  \u251c\u2500 injectBlueprintItems(phase=N+1)\\n  \u251c\u2500 UPDATE auto_complete items in new phase\\n  \u251c\u2500 auditLog(\\\"phase_completed\\\", \\\"phase_started\\\")\\n  \u251c\u2500 runPhaseHooks(phase=N+1) [background]\\n  \u2502   \u2514\u2500 Phase-specific hook (email, ticket, discovery, etc.)\\n  \u2514\u2500 Return { ok: true, new_phase: N+1 }\\n```\\n\\n---\\n\\n## Checklist Item Lifecycle\\n\\n1. **Seeded** \u2014 `seedPhaseItems` inserts from templates when phase starts\\n2. **Injected** \u2014 `injectBlueprintItems` adds blueprint-specific items (if blueprint exists)\\n3. **Auto-completed** \u2014 Items with `auto_complete=true` are marked `completed_at=now()` immediately\\n4. **Manually completed** \u2014 Techs mark items done via `handleCompleteItem`\\n5. **Blocking validation** \u2014 `handleAdvancePhase` checks all `blocks_phase=true` items in current phase are complete before allowing advance\\n6. **Uncompleted** \u2014 Techs can uncomplete non-auto items via `handleUncompleteItem`\\n\\n**Blocking items** are critical path tasks (e.g., \\\"Verify RMM agents deployed\\\"). They must be done before phase can advance. Generic templates define which items block; blueprint items are never blocking.\\n\\n---\\n\\n## Status Lifecycle\\n\\n```\\nactive \u2500\u2500\u2192 paused \u2500\u2500\u2192 active\\n  \u2193\\n  \u2514\u2500\u2500\u2192 completed (only from phase 8 with all blocking items done)\\n  \\nactive \u2500\u2500\u2192 cancelled \u2500\u2500\u2192 active (restore)\\n```\\n\\n- **active** \u2014 Onboarding in progress, can advance phases\\n- **paused** \u2014 Temporarily halted, can resume\\n- **completed** \u2014 All 9 phases done, no further changes\\n- **cancelled** \u2014 Soft-deleted (data preserved for audit), associated scheduled operations deactivated\\n\\n---\\n\\n## Integration Points\\n\\n### Inbound\\n\\n- **E-sign webhook** (`HandleMSATrigger`) \u2014 Triggered by DocuSign/PandaDoc when MSA is fully executed\\n- **Assessment pipeline** \u2014 Populates `client_blueprints` table; onboarding reads and injects\\n- **RMM system** \u2014 Agents table queried for discovery, security, and hardware hooks\\n- **Compliance module** \u2014 Frameworks auto-assigned via `compliance_assignments` table\\n\\n### Outbound\\n\\n- **Helpdesk** \u2014 Creates tickets for manual work (RMM deployment, discovery, security brief, hardware audit, compliance gap)\\n- **Email service** (`billing.EmailSender`) \u2014 Sends welcome emails in Phase 1\\n- **Scheduled operations** \u2014 Creates recurring ops (scans, patches, backups) from blueprint\\n- **Compliance assignments** \u2014 Auto-assigns frameworks to company in Phase 8\\n- **Audit log** \u2014 Immutable record of all state changes and user actions\\n\\n---\\n\\n## Security &amp; Compliance\\n\\n**Tenant isolation:** Every query includes `WHERE tenant_id = $1`. No cross-tenant data leakage.\\n\\n**Idempotency:** `HandleMSATrigger` uses `contract_id` as dedup key. Safe to retry webhook without creating duplicate onboardings.\\n\\n**Audit trail:** All state changes logged to INSERT-ONLY `onboarding_audit_log`. No UPDATE/DELETE on audit records.\\n\\n**Soft deletes:** Cancelled onboardings set `status='cancelled'` rather than hard-deleted. Data preserved for compliance and audit.\\n\\n**Auto-complete safety:** Only system-driven items (e.g., \\\"Send welcome email\\\") are auto-completed. Manual work items require tech action.\\n\\n---\\n\\n## Common Patterns\\n\\n### Adding a New Phase\\n\\n1. Add phase number and label to `PhaseLabel()` in `types.go`\\n2. Add color to `PhaseColor()`\\n3. Create template rows in `onboarding_checklist_templates` (tenant_id IS NULL)\\n4. If blueprint data exists, add injector function in `blueprint_injection.go` and call from `injectBlueprintItems`\\n5. If automation needed, add hook function in `phases.go` and call from `runPhaseHooks`\\n6. Add phase timestamp columns to `client_onboarding` table (phase_N_started_at, phase_N_completed_at)\\n\\n### Adding Blueprint Data to a Phase\\n\\n1. Add JSON field to `ClientBlueprint` struct (e.g., `NewStack string`)\\n2. Create corresponding item struct (e.g., `blueprintNewItem`)\\n3. Add injector function that unmarshals JSON and calls `insertBlueprintItem` for each item\\n4. Call injector from `injectBlueprintItems` switch statement\\n\\n### Creating a Helpdesk Ticket from a Hook\\n\\n```go\\nh.createOnboardingTicket(ctx, tenantID, companyID, onboardingID,\\n    \\\"Ticket Title\\\",\\n    \\\"Markdown body with instructions\\\",\\n    \\\"task\\\",  // or \\\"bug\\\", \\\"feature\\\"\\n    \\\"high\\\",  // or \\\"medium\\\", \\\"low\\\"\\n)\\n```\\n\\n---\\n\\n## Testing Considerations\\n\\n- **Idempotency:** Call `HandleMSATrigger` twice with same contract_id; should return existing onboarding_id\\n- **Blocking items:** Try to advance phase with incomplete blocking items; should fail\\n- **Blueprint injection:** Create blueprint, trigger onboarding, verify blueprint items appear in checklist\\n- **Audit trail:** Verify every state change creates audit log entry\\n- **Tenant isolation:** Verify queries with wrong tenant_id return no results\\n- **Auto-complete:** Verify auto_complete items are marked done at phase start\\n- **Phase hooks:** Verify tickets/emails created in background without blocking response\\n\\n---\\n\\n## Troubleshooting\\n\\n**Onboarding stuck on a phase:**\\n- Check `onboarding_checklist_items` for incomplete `blocks_phase=true` items\\n- Verify `status='active'` (not paused/cancelled)\\n- Check audit log for any errors\\n\\n**Blueprint items not appearing:**\\n- Verify `client_blueprints` record exists for company\\n- Check JSON is valid (use `json_valid()` in psql)\\n- Verify phase number matches injector (Phase 4 = security, Phase 5 = tools, etc.)\\n\\n**Scheduled operations not created:**\\n- Verify blueprint has `scheduled_operations` JSON\\n- Check `scheduled_operations` table for duplicates (ON CONFLICT DO NOTHING)\\n- Verify `op_type` matches known types (vuln_scan, patch_cycle, etc.)\\n\\n**Tickets not created:**\\n- Check `tickets` table for errors\\n- Verify `company_id` is valid UUID\\n- Check logs for \\\"ticket create failed\\\" messages\",\"internal-performance\":\"# internal \u2014 performance\\n\\n# NexusPulse Performance Module\\n\\n## Overview\\n\\nThe **internal/performance** module is NexusOS's business intelligence engine for MSPs. It collects operational and financial data from across the platform, calculates 80+ KPIs, analyzes them against industry benchmarks, and generates actionable insights. The module powers the NexusPulse dashboard\u2014a monthly performance scorecard that helps MSP owners understand their business health and identify growth opportunities.\\n\\nThe module operates on a **monthly snapshot model**: each month, a `Snapshot` captures input metrics (collected from the database and QuickBooks), calculates output KPIs, runs diagnostic rules, and generates a finalized report. Users can manually override inputs, and the system recalculates outputs automatically.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    A[\\\"Collector(auto-populate inputs)\\\"] --&gt;|CollectAll| B[\\\"Snapshot(monthly data)\\\"]\\n    B --&gt;|Calculate| C[\\\"Calculator(80+ KPIs)\\\"]\\n    C --&gt;|Analyze| D[\\\"Analyzer(rules engine)\\\"]\\n    D --&gt;|Generate| E[\\\"Report(findings + grade)\\\"]\\n    F[\\\"Handler(HTTP)\\\"] --&gt;|orchestrates| A\\n    F --&gt;|stores| B\\n    G[\\\"Store(database)\\\"] --&gt;|persists| B\\n    G --&gt;|persists| E\\n```\\n\\n## Core Components\\n\\n### 1. Snapshot (`types.go`)\\n\\nA `Snapshot` is the atomic unit of analysis\u2014one month's complete performance data.\\n\\n```go\\ntype Snapshot struct {\\n    ID           string                      // UUID\\n    TenantID     string\\n    PeriodYear   int\\n    PeriodMonth  int\\n    Status       string                      // \\\"draft\\\", \\\"finalized\\\"\\n    Inputs       map[string]*MetricInput     // user-entered or auto-collected\\n    Outputs      map[string]*MetricOutput    // calculated KPIs\\n    CollectedAt  string                      // timestamp + log message\\n    FinalizedAt  string\\n    FinalizedBy  string\\n}\\n```\\n\\n**Key methods:**\\n- `InputValue(code)` / `OutputValue(code)` \u2014 retrieve metric values by code\\n- `PeriodLabel()` \u2014 returns \\\"Apr 2026\\\" format\\n- `IsDraft()` \u2014 checks if snapshot is editable\\n\\n### 2. Collector (`collector.go`)\\n\\nAuto-populates input metrics from NexusOS tables and QuickBooks Online.\\n\\n**CollectAll(ctx, tenantID, year, month)** runs queries for:\\n\\n- **General Information**: seat count, employee counts by role, managed agreements\\n- **Operations**: reactive tickets created/closed, time spent on reactive work\\n- **Sales**: dials, appointments, new agreements, new logo MRR\\n- **MRR Changes**: growth, decreases, churn\\n- **Revenue &amp; Expenses**: from QuickBooks P&amp;L report (if connected) or contract fallback\\n\\n**QBO Integration:**\\n- Fetches P&amp;L report from QuickBooks API\\n- Maps QBO accounts to SMART categories via `performance_qbo_account_map` table\\n- Falls back to contract-based revenue if QBO is not connected\\n- Expenses default to $0 without QBO (data quality flag in analyzer)\\n\\n**Error Handling:**\\nReturns a list of collection errors (e.g., \\\"NXP.GE.SM: database query failed\\\") so users know which metrics are incomplete.\\n\\n### 3. Calculator (`calculator.go`)\\n\\nPure functions that compute 80+ output KPIs from input metrics. No database access\u2014fully deterministic.\\n\\n**Calculate(inputs, workingDays)** computes:\\n\\n- **Operations**: close rate, reactive hours per seat, avg resolution time\\n- **Revenue**: total service revenue, % from MRR, % from services\\n- **Leverage**: ASR per employee, seats per tech, tools cost %, payroll %\\n- **Gross Margin**: MRR margin, NRR margin, combined margin, total gross margin\\n- **Business Health**: AISP, Rule of 40, unrealized profitability, net MRR gain\\n\\n**CalculateWithHistory(inputs, workingDays, priorYear)** adds year-over-year metrics:\\n- MRR YoY growth %\\n- ASR YoY growth %\\n\\n**CalculateEnterpriseValue(snapshots)** aggregates trailing 4 quarters and applies a valuation multiplier (3\u20138x) based on net profit %.\\n\\n### 4. Analyzer (`analyzer.go`)\\n\\nThe rules engine that identifies deficiencies and strengths.\\n\\n**Analyze(current, previous)** produces an `Analysis` with:\\n\\n- **Scorecard**: every metric with current value, prior value, delta, status (green/yellow/red)\\n- **Findings**: sorted by priority, categorized as critical/warning/strength\\n- **Overall Grade**: A\u2013F based on weighted metric scores\\n- **Overall Score**: 0\u2013100\\n\\n**Rules Engine (runAllRules):**\\n\\nRuns ~20 diagnostic rules in two phases:\\n\\n1. **Data Quality Checks** (priority 8\u201310): Flags missing or unrealistic data\\n   - Net profit &gt; 50% with revenue present \u2192 expenses likely missing\\n   - Total expenses = $0 but revenue &gt; $0 \u2192 QBO not mapped\\n   - MRR margin &gt; 95% \u2192 employee/tools costs not captured\\n   - 0 technical employees but revenue exists \u2192 SMART roles not assigned\\n\\n2. **Business Rules** (priority 1\u20139): Evaluates health when data is reasonable\\n   - **AISP** (Average In-Seat Price): &lt; $100 is red, $100\u2013$150 is green, &gt; $150 is yellow\\n   - **ASR/Employee**: &lt; $150K is red, &gt;= $150K is green\\n   - **Net Profit %**: &lt; 10% is red, &gt;= 10% is green\\n   - **RHEM** (Reactive Hours/Seat/Month): &gt; 0.5 is red, &lt;= 0.5 is green\\n   - **MRR Margin**: &lt; 60% is red, &gt;= 60% is green\\n   - **Rule of 40**: &lt; 40 is red, &gt;= 40 is green\\n   - **Churn**: &gt; 2% monthly is red\\n   - **Tools COGS %**: &gt; 15% of ASR is yellow\\n   - **Payroll %**: &gt; 55% of ASR is red\\n\\nEach finding includes:\\n- **Diagnosis**: plain English explanation of what's wrong\\n- **Why It Matters**: business impact\\n- **Actions**: specific steps to fix (with module links and effort/timeframe)\\n- **Dollar Impact**: estimated annual $ impact of fixing\\n- **Related Codes**: other metrics this affects\\n\\n### 5. Handler (`handler.go`)\\n\\nHTTP request handler for all NexusPulse endpoints.\\n\\n**Page Handlers (HTMX):**\\n- `GET /performance` \u2014 main dashboard with hero cards and section previews\\n- `GET /performance/section/{section}` \u2014 detailed view of one section (e.g., \\\"leverage\\\")\\n- `GET /performance/trends` \u2014 multi-month trend charts\\n- `GET /performance/enterprise-value` \u2014 valuation calculator\\n- `GET /performance/inputs/{year}/{month}` \u2014 input editor\\n- `GET /settings/performance` \u2014 QBO account mapping UI\\n\\n**API Handlers:**\\n- `POST /api/performance/collect` \u2014 trigger manual collection\\n- `POST /api/performance/inputs/{snapID}` \u2014 save manual input overrides\\n- `POST /api/performance/finalize/{snapID}` \u2014 lock snapshot and generate report\\n- `GET /api/performance/snapshot/{year}/{month}` \u2014 JSON snapshot\\n- `GET /api/performance/trends/{code}` \u2014 JSON trend data\\n- `POST /api/performance/qbo-mappings` \u2014 save QBO account mapping\\n- `GET /api/performance/qbo-suggest` \u2014 auto-suggest mappings\\n\\n**Report Handlers:**\\n- `POST /api/performance/reports/generate` \u2014 run analysis and create report\\n- `GET /performance/reports/{id}` \u2014 view report with findings\\n- `GET /performance/reports/{id}/print` \u2014 print-friendly report\\n\\n**Background Job:**\\n- `RunMonthlyCollection(ctx)` \u2014 runs on schedule (daily) to auto-collect for all tenants\\n- Auto-finalizes prior month on the 3rd of each month\\n\\n### 6. Store (`store.go`)\\n\\nDatabase persistence layer. Manages snapshots, inputs, outputs, reports, and QBO mappings.\\n\\n**Key methods:**\\n- `GetOrCreateSnapshot(ctx, tenantID, year, month)` \u2014 fetch or create draft\\n- `GetSnapshot(ctx, tenantID, year, month)` \u2014 fetch finalized snapshot\\n- `UpsertInputBatch(ctx, snapID, inputs, source)` \u2014 save collected inputs (respects manual overrides)\\n- `OverrideInput(ctx, snapID, code, value, userID, note)` \u2014 manual override with audit trail\\n- `SaveOutputs(ctx, snapID, outputs)` \u2014 persist calculated KPIs\\n- `FinalizeSnapshot(ctx, snapID, userID)` \u2014 lock snapshot, prevent edits\\n- `SaveReport(ctx, report)` \u2014 persist analysis and findings\\n- `GetTrends(ctx, tenantID, codes, months)` \u2014 fetch historical data for charts\\n- `SaveQBOAccountMapping(ctx, tenantID, qboID, qboName, smartCategory)` \u2014 map QBO account to SMART category\\n- `GetQBOAccountMappings(ctx, tenantID)` \u2014 list all mappings for settings UI\\n\\n### 7. QBO Mapper (`qbo_mapper.go`)\\n\\nIntelligent account mapping suggestion engine.\\n\\n**AutoSuggestMappings(accounts, existingMappings)** analyzes QBO account names and suggests SMART categories:\\n\\n- **Revenue accounts**: Detects \\\"managed service\\\", \\\"monthly recurring\\\", \\\"saas\\\", \\\"project\\\", \\\"hardware\\\" patterns\\n- **Expense accounts**: Detects \\\"payroll\\\", \\\"rmm\\\", \\\"backup\\\", \\\"license cost\\\", \\\"hardware cost\\\" patterns\\n- Returns confidence levels (high/medium/low) for each suggestion\\n\\nExample: A QBO account named \\\"RMM Tool Costs\\\" \u2192 suggests `NXP.EX.MTCOGS` with high confidence.\\n\\n## Data Flow\\n\\n### Monthly Collection Cycle\\n\\n```\\n1. RunMonthlyCollection (background job, daily)\\n   \u2193\\n2. For each active tenant:\\n   a. CollectAll() \u2014 query NexusOS tables + QBO\\n   b. UpsertInputBatch() \u2014 save inputs (respecting manual overrides)\\n   c. Calculate() \u2014 compute 80+ KPIs\\n   d. SaveOutputs() \u2014 persist outputs\\n   e. UpdateCollectedAt() \u2014 log collection timestamp\\n   \u2193\\n3. On 3rd of month: auto-finalize prior month's snapshot\\n```\\n\\n### Manual Input Override\\n\\n```\\n1. User edits input in /performance/inputs/{year}/{month}\\n   \u2193\\n2. handleSaveInputs() receives form data\\n   \u2193\\n3. OverrideInput() saves with audit trail (userID, timestamp, note)\\n   \u2193\\n4. Calculate() re-runs with updated inputs\\n   \u2193\\n5. SaveOutputs() persists new KPIs\\n```\\n\\n### Report Generation\\n\\n```\\n1. User clicks \\\"Generate Report\\\" on finalized snapshot\\n   \u2193\\n2. handleGenerateReport():\\n   a. Load current snapshot + previous month\\n   b. Analyze(current, previous) \u2192 findings + grade\\n   c. buildExecutiveSummary() \u2192 plain English summary\\n   d. SaveReport() \u2192 persist analysis JSON\\n   \u2193\\n3. Report appears in /performance/reports\\n```\\n\\n## Key Metrics &amp; Formulas\\n\\n### Revenue Metrics\\n- **ASR** (All Services Revenue) = MRR + ORR + NRR\\n- **Total Revenue** = ASR + Sales + Misc\\n- **% from MRR** = MRR / Total Revenue \u00d7 100\\n\\n### Leverage Metrics\\n- **ASR/Employee** = (ASR \u00d7 12) / FTE\\n- **Seats/Tech** = Seats Managed / Technical Employees\\n- **Tools COGS %** = Tools COGS / ASR \u00d7 100\\n- **Payroll %** = (MRR Emp + NRR Emp + Owner Comp) / ASR \u00d7 100\\n\\n### Margin Metrics\\n- **MRR Margin %** = (MRR - MRR Employee Exp - Tools COGS) / MRR \u00d7 100\\n- **Combined MRR &amp; NRR Margin %** = (MRR + NRR - MRR Emp - NRR Emp - Tools COGS) / (MRR + NRR) \u00d7 100\\n- **Total Gross Margin %** = Total Gross Margin $ / Total Revenue \u00d7 100\\n\\n### Business Health Metrics\\n- **AISP** (Average In-Seat Price) = MRR / Seats Managed\\n- **Rule of 40** = Net MRR Gain % + Combined Margin %\\n- **Unrealized Profitability** = (FTE \u00d7 $150K) - (ASR \u00d7 12)\\n- **Turning Point** = (MRR + ORR) - (Employee Exp + Owner Comp + Tools COGS + ORR COGS + Other Exp)\\n\\n### Operations Metrics\\n- **RHEM** (Reactive Hours/Seat/Month) = Total Reactive Hours / Seats Managed\\n- **Close Rate %** = Reactive Tickets Closed / Reactive Tickets Created \u00d7 100\\n- **Avg Resolution Time** = Total Reactive Hours / Reactive Tickets Closed\\n\\n## Integration Points\\n\\n### With Other Modules\\n\\n- **Helpdesk** (`internal/helpdesk`): Provides ticket counts and time entries\\n- **Contracts** (`internal/contracts`): Provides MRR, ORR, NRR, new agreements\\n- **CRM** (`internal/crm`): Provides sales activities (dials, appointments)\\n- **Users** (`internal/users`): Provides employee counts by SMART role\\n- **Products** (`internal/products`): Provides seat assignments\\n- **QuickBooks** (`internal/quickbooks`): Provides P&amp;L data via API\\n\\n### With Auth &amp; UI\\n\\n- **Auth** (`internal/auth`): Extracts tenant ID and user ID from request context\\n- **UI** (`internal/ui`): Renders templates with snapshot data and trends\\n\\n## Configuration &amp; Settings\\n\\n**PulseSettings** (per tenant):\\n- `GoToMarketSeatPrice` \u2014 CSP for AISP benchmarking\\n- `MinimumMRR` \u2014 threshold for new logo tracking\\n- `WorkingDaysPerMonth` \u2014 used in calculations (default 22)\\n\\n**QBO Account Mappings** (per tenant):\\n- Maps QBO account IDs to SMART categories\\n- Stored in `performance_qbo_account_map` table\\n- Auto-suggested by `AutoSuggestMappings()`\\n\\n## Error Handling &amp; Data Quality\\n\\nThe analyzer includes **data quality checks** that run before business rules:\\n\\n1. **Net Profit &gt; 50%** with revenue present \u2192 expenses likely missing (priority 10)\\n2. **Total Expenses = $0** but revenue &gt; $0 \u2192 QBO not mapped (priority 10)\\n3. **MRR Margin &gt; 95%** \u2192 employee/tools costs not captured (priority 9)\\n4. **0 Technical Employees** but revenue exists \u2192 SMART roles not assigned (priority 8)\\n\\nThese findings are marked as **red** and appear at the top of the report, blocking trust in downstream metrics until resolved.\\n\\nCollection errors are logged and returned to the user:\\n```\\n\\\"Collected 45 inputs (3 warnings: NXP.OP.TTRT: database query failed; ...)\\\"\\n```\\n\\n## Formatting &amp; Display\\n\\n**formatMetricValue(value, unit)** handles display formatting:\\n- `currency`: \\\"$1,234.56\\\"\\n- `percent`: \\\"45.3%\\\"\\n- `hours`: \\\"12.5h\\\"\\n- `count`: \\\"42\\\"\\n- `ratio`: \\\"1.5\\\"\\n\\n**formatDelta(delta, unit)** formats month-over-month changes:\\n- `+$500.00` or `-$500.00`\\n- `+5.2%` or `-5.2%`\\n\\n## Testing &amp; Validation\\n\\nThe module is designed for deterministic testing:\\n\\n- **Calculator** functions are pure (no side effects, no database access)\\n- **Analyzer** rules are stateless (given a snapshot, always produce the same findings)\\n- **Collector** queries are isolated and can be mocked\\n\\nExample test pattern:\\n```go\\nsnap := &amp;Snapshot{\\n    Inputs: map[string]*MetricInput{\\n        \\\"NXP.RV.MRR\\\": {Value: 10000},\\n        \\\"NXP.GE.SM\\\": {Value: 100},\\n    },\\n}\\noutputs := Calculate(InputsAsMap(snap), 22)\\nassert.Equal(t, 1200000, outputs[\\\"NXP.LV.ASREMP\\\"]) // $150K/employee\\n```\\n\\n## Future Enhancements\\n\\n- **Nexie AI Integration**: Replace `buildExecutiveSummary()` with LLM-generated insights\\n- **Benchmarking**: Compare metrics against industry cohorts (by size, geography, vertical)\\n- **Forecasting**: Predict future metrics based on trends\\n- **Alerts**: Notify users when metrics cross thresholds\\n- **Custom Rules**: Allow tenants to define their own diagnostic rules\\n- **Multi-Period Analysis**: Cohort analysis across multiple snapshots\",\"internal-po\":\"# internal \u2014 po\\n\\n# Purchase Order (PO) Module\\n\\nThe `internal/po` module implements the complete purchase order lifecycle for NexusOS PSA, from creation through approval, receiving, and notification. It provides both HTML pages (HTMX-driven) and JSON APIs for managing procurement workflows, with optional integrations for QuickBooks Online and Web Push notifications.\\n\\n## Overview\\n\\nThe module handles:\\n- **PO creation** with line items, auto-approval based on thresholds, and sequence generation\\n- **Approval workflows** with permission checks, signature capture, and QBO sync\\n- **Receiving** with partial receipt tracking and status transitions\\n- **Approver management** combining role-based and direct user assignments\\n- **Notifications** for requesters, approvers, and the notification center\\n- **Quick PO** for mobile field technicians with minimal data entry\\n- **CRM integration** showing POs scoped to companies, tickets, and projects\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    A[\\\"HTTP HandlerHandler struct\\\"] --&gt;|routes| B[\\\"Page HandlersHTMX + HTML\\\"]\\n    A --&gt;|routes| C[\\\"API HandlersJSON\\\"]\\n    A --&gt;|routes| D[\\\"Workflow ActionsApprove/Reject/Receive\\\"]\\n    A --&gt;|routes| E[\\\"SettingsApprovers &amp; Auto-Approve\\\"]\\n    \\n    C --&gt;|queries| F[\\\"Databasepurchase_orders, po_line_items\\\"]\\n    D --&gt;|queries| F\\n    D --&gt;|optional| G[\\\"QBO IntegrationCreatePurchaseOrder\\\"]\\n    D --&gt;|optional| H[\\\"Push NotificationsSendToUser\\\"]\\n    \\n    E --&gt;|manages| I[\\\"po_approvers tableRole + Direct assignments\\\"]\\n    \\n    A --&gt;|notifies| J[\\\"Notification Systemnotifications table\\\"]\\n    J --&gt;|polls| K[\\\"ClienthandlePollNotifications\\\"]\\n```\\n\\n## Core Components\\n\\n### Handler\\n\\nThe `Handler` struct is the entry point for all PO operations:\\n\\n```go\\ntype Handler struct {\\n    pool     *pgxpool.Pool      // Database connection pool\\n    renderer *ui.Renderer       // Template renderer for HTML pages\\n    qbo      QBOCreator         // Optional QuickBooks Online integration\\n    push     PushSender         // Optional Web Push notification sender\\n}\\n```\\n\\n**Key methods:**\\n- `NewHandler(pool, renderer)` \u2014 Creates a new handler\\n- `SetQBO(qbo)` \u2014 Registers QBO integration (optional)\\n- `SetPush(p)` \u2014 Registers push notification sender (optional)\\n- `RegisterRoutes(mux)` \u2014 Registers all HTTP endpoints\\n\\n### Route Groups\\n\\n#### Pages (HTMX)\\n- `GET /purchase-orders` \u2014 List page with status filters\\n- `GET /purchase-orders/new` \u2014 Create form\\n- `GET /purchase-orders/{id}` \u2014 Detail page\\n- `GET /purchase-orders/{id}/print` \u2014 Print/PDF view\\n- `GET /purchase-orders/quick` \u2014 Quick PO form (mobile)\\n- `GET /settings/procurement` \u2014 Settings page\\n- `GET /notifications` \u2014 Notification center\\n\\n#### APIs (JSON)\\n- `GET /api/purchase-orders` \u2014 List with optional status filter\\n- `POST /api/purchase-orders` \u2014 Create PO\\n- `GET /api/purchase-orders/{id}` \u2014 Get single PO\\n- `PUT /api/purchase-orders/{id}` \u2014 Update PO (draft/pending only)\\n- `DELETE /api/purchase-orders/{id}` \u2014 Delete PO (draft/pending only)\\n- `GET /api/purchase-orders/form-data` \u2014 Load companies, tickets, projects, products\\n- `GET /api/purchase-orders/{id}/line-items` \u2014 Get line items for receiving modal\\n\\n#### Workflow Actions\\n- `POST /api/purchase-orders/{id}/approve` \u2014 Approve with optional signature\\n- `POST /api/purchase-orders/{id}/reject` \u2014 Reject with reason\\n- `POST /api/purchase-orders/{id}/receive` \u2014 Update received quantities\\n\\n#### Settings\\n- `GET /api/settings/procurement/approvers` \u2014 List approvers\\n- `POST /api/settings/procurement/approvers` \u2014 Add approver\\n- `DELETE /api/settings/procurement/approvers/{userID}` \u2014 Remove approver\\n- `POST /api/settings/procurement/auto-approve` \u2014 Set auto-approve threshold\\n\\n#### Notifications\\n- `GET /api/notifications/poll` \u2014 Poll unread notifications\\n- `POST /api/notifications/{id}/read` \u2014 Mark notification as read\\n- `POST /api/notifications/mark-all-read` \u2014 Mark all as read\\n\\n#### CRM Integration\\n- `GET /api/company-purchase-orders/{companyID}` \u2014 POs for a company (HTML fragment)\\n\\n## PO Lifecycle\\n\\n### Creation (`handleCreate`)\\n\\n1. **Validation** \u2014 Vendor and at least one line item required\\n2. **Calculation** \u2014 Subtotal from line items, total = subtotal + tax + shipping\\n3. **Sequence generation** \u2014 Locks `po_sequences` row, increments counter, generates `PO-YYYY-NNN`\\n4. **Auto-approval check** \u2014 If total \u2264 `tenants.settings.po_auto_approve_limit`, status = `approved`; otherwise `pending_approval`\\n5. **Insert PO** \u2014 Single transaction with line items\\n6. **Notify approvers** \u2014 If pending, async notification to all approvers with `purchase_orders:approve` permission\\n\\n**Status flow:**\\n```\\npending_approval (if total &gt; limit)\\n    \u2193\\napproved (if total \u2264 limit OR manually approved)\\n    \u2193\\nordered (optional, not set by this handler)\\n    \u2193\\npartial_received / received\\n```\\n\\n### Approval (`handleApprove`)\\n\\n1. **Permission check** \u2014 User must have `purchase_orders:approve` permission (role-based or direct assignment)\\n2. **Signature capture** \u2014 Optional signature data from request body\\n3. **Audit trail** \u2014 Records approver name, IP, timestamp\\n4. **QBO sync** \u2014 If `h.qbo` is set:\\n   - Loads line items with product names and QBO item IDs\\n   - Calls `CreatePurchaseOrder(ctx, tenantID, qboPO)`\\n   - Stores returned QBO PO ID and document number\\n5. **Notification** \u2014 Creates notification for requester; sends Web Push if available\\n6. **Response** \u2014 Returns status, PO number, and optional QBO PO number\\n\\n### Rejection (`handleReject`)\\n\\n1. **Status update** \u2014 Sets status to `rejected`, records rejector and reason\\n2. **Notification** \u2014 Notifies requester with rejection reason\\n3. **No QBO sync** \u2014 Rejected POs are not sent to QBO\\n\\n### Receiving (`handleReceive`)\\n\\n1. **Line item updates** \u2014 Updates `received_qty` for each line item\\n2. **Status calculation**:\\n   - If `SUM(received_qty) &gt;= SUM(quantity)` \u2192 `received`\\n   - Else if `SUM(received_qty) &gt; 0` \u2192 `partial_received`\\n   - Else \u2192 `approved` (nothing received yet)\\n3. **Date tracking** \u2014 Sets `received_date` only when fully received\\n\\n## Approver Management\\n\\nThe module supports two approval sources:\\n\\n### Role-Based Approvers\\nUsers with the `purchase_orders:approve` permission in their role can approve POs.\\n\\n### Direct Approvers\\nThe `po_approvers` table allows explicit assignment:\\n- `is_excluded = false` \u2014 User is an approver\\n- `is_excluded = true` \u2014 User is explicitly excluded (overrides role permission)\\n\\n**Query logic** (`handleListApprovers`):\\n```sql\\nSELECT users WHERE\\n  (role has purchase_orders:approve permission OR po_approvers.is_excluded = false)\\n  AND NOT EXISTS (po_approvers WHERE is_excluded = true)\\n```\\n\\nThis allows:\\n- Role-based defaults\\n- Opt-in for users without the role\\n- Opt-out for users with the role\\n\\n## Auto-Approval\\n\\nPOs below a configured threshold are automatically approved on creation:\\n\\n1. **Threshold stored** \u2014 `tenants.settings['po_auto_approve_limit']` (JSONB)\\n2. **Check on create** \u2014 If `total &lt;= limit`, set status to `approved` and record approver as the creator\\n3. **Configuration** \u2014 `POST /api/settings/procurement/auto-approve` with `{\\\"limit\\\": 5000.00}`\\n\\n## Quick PO (Mobile)\\n\\nThe quick PO flow (`handleQuickCreate`) is optimized for field technicians:\\n\\n1. **Form submission** \u2014 Multipart form (not JSON) with minimal fields:\\n   - `description` (required)\\n   - `vendor` (optional)\\n   - `estimated_cost` (optional)\\n   - `priority` (defaults to \\\"normal\\\")\\n   - `project_id`, `ticket_id`, `survey_id` (optional context)\\n   - `notes`\\n\\n2. **Single line item** \u2014 Creates one line item with quantity=1, unit_price=estimated_cost\\n\\n3. **Notification** \u2014 Calls `notifyApproversOfNewPO` to alert all configured approvers\\n\\n4. **Redirect** \u2014 Returns `HX-Redirect` header or HTTP redirect to detail page\\n\\n## Form Data Loading\\n\\n`handleFormData` provides a single endpoint for populating dropdowns:\\n\\n```json\\n{\\n  \\\"companies\\\": [{\\\"id\\\": \\\"...\\\", \\\"name\\\": \\\"...\\\"}],\\n  \\\"tickets\\\": [{\\\"id\\\": \\\"...\\\", \\\"title\\\": \\\"...\\\", \\\"ticket_number\\\": 123, \\\"company_id\\\": \\\"...\\\"}],\\n  \\\"projects\\\": [{\\\"id\\\": \\\"...\\\", \\\"name\\\": \\\"...\\\", \\\"company_id\\\": \\\"...\\\"}],\\n  \\\"products\\\": [{\\\"id\\\": \\\"...\\\", \\\"name\\\": \\\"...\\\", \\\"price\\\": 99.99}]\\n}\\n```\\n\\n**Filters:**\\n- Companies: `is_active = true`\\n- Tickets: status NOT IN ('closed', 'resolved')\\n- Projects: status IN ('planning', 'active')\\n- Products: `is_active = true`\\n\\n## Notifications\\n\\n### Creation &amp; Approval Flow\\n\\n1. **PO created (pending)** \u2192 `notifyApprovers` inserts notifications for all approvers\\n2. **PO approved** \u2192 Notification inserted for requester with approval details\\n3. **PO rejected** \u2192 Notification inserted for requester with rejection reason\\n\\n### Notification Center\\n\\n- `GET /notifications` \u2014 Renders HTML page with last 50 notifications\\n- `GET /api/notifications/poll` \u2014 Returns unread, unresolved notifications (max 10) as JSON\\n- `POST /api/notifications/{id}/read` \u2014 Marks single notification as read\\n- `POST /api/notifications/mark-all-read` \u2014 Marks all as read\\n\\n### Web Push Integration\\n\\nIf `h.push` is set, the handler sends OS-level notifications:\\n\\n```go\\nh.push.SendToUser(ctx, tenantID, userID, map[string]string{\\n    \\\"title\\\": \\\"PO Approved\\\",\\n    \\\"body\\\":  \\\"Your purchase order PO-2024-001 has been approved\\\",\\n    \\\"link\\\":  \\\"/purchase-orders/...\\\",\\n    \\\"tag\\\":   \\\"po-approved-...\\\",  // Prevents duplicates\\n})\\n```\\n\\n## QuickBooks Online Integration\\n\\nWhen `h.qbo` is set, approved POs are synced to QBO:\\n\\n1. **Data preparation** \u2014 Loads line items with product names and QBO item IDs\\n2. **QBO call** \u2014 `CreatePurchaseOrder(ctx, tenantID, qboPO)` returns QBO ID and document number\\n3. **Storage** \u2014 Updates `purchase_orders.qbo_po_id` and `qbo_po_number`\\n4. **Error handling** \u2014 Logs failure but does not fail the approval (PO remains approved)\\n\\n**QBOPurchaseOrder struct:**\\n```go\\ntype QBOPurchaseOrder struct {\\n    VendorName  string\\n    PONumber    string\\n    TxnDate     string\\n    LineItems   []QBOPOLineItem\\n    TotalAmount float64\\n    Memo        string\\n}\\n```\\n\\n## Database Schema (Inferred)\\n\\n### purchase_orders\\n- `id` (uuid, PK)\\n- `tenant_id` (uuid, FK)\\n- `po_number` (text, unique per tenant)\\n- `vendor` (text)\\n- `description` (text)\\n- `company_id` (uuid, FK, nullable)\\n- `ticket_id` (uuid, FK, nullable)\\n- `project_id` (uuid, FK, nullable)\\n- `project_task_id` (uuid, FK, nullable)\\n- `priority` (text: \\\"normal\\\", \\\"high\\\", \\\"urgent\\\")\\n- `status` (text: \\\"draft\\\", \\\"pending_approval\\\", \\\"approved\\\", \\\"ordered\\\", \\\"partial_received\\\", \\\"received\\\", \\\"rejected\\\")\\n- `subtotal`, `tax_amount`, `shipping_amount`, `total` (numeric)\\n- `notes`, `rejection_reason` (text)\\n- `is_billable` (boolean)\\n- `requested_by` (uuid, FK to users)\\n- `approved_by` (uuid, FK to users, nullable)\\n- `approved_at` (timestamp, nullable)\\n- `rejected_by` (uuid, FK to users, nullable)\\n- `rejected_at` (timestamp, nullable)\\n- `received_date` (date, nullable)\\n- `approval_signature`, `approval_signer_name`, `approval_signer_ip` (text, nullable)\\n- `qbo_po_id`, `qbo_po_number` (text, nullable)\\n- `created_at`, `updated_at` (timestamp)\\n\\n### po_line_items\\n- `id` (uuid, PK)\\n- `po_id` (uuid, FK)\\n- `product_id` (uuid, FK, nullable)\\n- `description` (text)\\n- `quantity`, `unit_price` (numeric)\\n- `line_total` (computed or stored)\\n- `received_qty` (numeric, default 0)\\n- `sort_order` (int)\\n\\n### po_sequences\\n- `tenant_id` (uuid, PK)\\n- `next_number` (int)\\n- `prefix` (text, default \\\"PO-\\\")\\n\\n### po_approvers\\n- `tenant_id` (uuid, PK)\\n- `user_id` (uuid, PK)\\n- `is_excluded` (boolean)\\n- `added_by` (uuid, FK to users, nullable)\\n\\n### notifications\\n- `id` (uuid, PK)\\n- `tenant_id` (uuid, FK)\\n- `user_id` (uuid, FK)\\n- `type` (text: \\\"po_pending_approval\\\", \\\"po_approved\\\", \\\"po_rejected\\\")\\n- `title`, `message` (text)\\n- `severity` (text: \\\"info\\\", \\\"warning\\\", \\\"critical\\\")\\n- `link`, `entity_type`, `entity_id` (text)\\n- `is_read`, `resolved` (boolean)\\n- `action_url`, `action_label` (text, nullable)\\n- `secondary_action_url`, `secondary_action_label` (text, nullable)\\n- `source` (text, default \\\"system\\\")\\n- `created_at` (timestamp)\\n\\n## Error Handling\\n\\n- **Validation errors** \u2014 Return 400 with JSON error message\\n- **Permission errors** \u2014 Return 403 for missing `purchase_orders:approve`\\n- **Not found** \u2014 Return 404 for missing PO\\n- **Conflict** \u2014 Return 409 for invalid state transitions (e.g., updating approved PO)\\n- **Database errors** \u2014 Log and return 500 with generic error message\\n\\n## Context &amp; Auth\\n\\nAll handlers extract tenant and user IDs from request context:\\n\\n```go\\ntenantID := auth.TenantIDFromContext(ctx)\\nuserID := auth.UserIDFromContext(ctx)\\n```\\n\\nThese are set by upstream middleware and used for:\\n- Tenant isolation (all queries filter by `tenant_id`)\\n- Audit trails (recording who created/approved/rejected)\\n- Permission checks (verifying user has required role/permission)\\n\\n## HTMX Integration\\n\\nPage handlers check for `HX-Request: true` header to determine response type:\\n- **HTMX request** \u2014 Render partial template (no layout)\\n- **Regular request** \u2014 Render full page with layout\\n\\nExample:\\n```go\\nisHTMX := r.Header.Get(\\\"HX-Request\\\") == \\\"true\\\"\\nh.renderer.Render(w, \\\"po/list.html\\\", data, isHTMX)\\n```\\n\\n## Common Patterns\\n\\n### Transaction Safety\\nPO creation uses a transaction to ensure atomicity:\\n```go\\ntx, _ := h.pool.Begin(ctx)\\ndefer tx.Rollback(ctx)\\n// ... insert PO, line items, update sequence\\ntx.Commit(ctx)\\n```\\n\\n### Null Handling\\nUUIDs and optional fields use `NULLIF(..., '')::uuid` to convert empty strings to NULL:\\n```sql\\nINSERT INTO purchase_orders (..., company_id, ...)\\nVALUES (..., NULLIF($1, '')::uuid, ...)\\n```\\n\\n### Async Notifications\\nApprover notifications are sent asynchronously to avoid blocking the request:\\n```go\\ngo h.notifyApprovers(tenantID, userID, poID, poNumber, vendor, total, priority)\\n```\\n\\n### Status Transitions\\nStatus changes are validated by checking current status in the WHERE clause:\\n```sql\\nUPDATE purchase_orders\\nSET status = 'approved'\\nWHERE id = $1 AND status = 'pending_approval'\\n```\\n\\n## Testing Considerations\\n\\n- **Sequence generation** \u2014 Test concurrent PO creation to ensure unique numbers\\n- **Auto-approval** \u2014 Test threshold boundary conditions\\n- **Approver permissions** \u2014 Test role-based, direct, and excluded assignments\\n- **QBO integration** \u2014 Mock `QBOCreator` to test sync without external calls\\n- **Notifications** \u2014 Verify correct recipients and message content\\n- **Receiving** \u2014 Test partial and full receipt scenarios\\n- **Tenant isolation** \u2014 Verify queries filter by tenant_id\",\"internal-portal\":\"# internal \u2014 portal\\n\\n# Portal Module Documentation\\n\\n## Overview\\n\\nThe `internal/portal` module implements a customer-facing web portal where contacts can view tickets, invoices, contracts, and other company-specific data. It provides:\\n\\n- **Authentication** via magic links (no passwords required)\\n- **Role-based access control** with granular permissions\\n- **AI-powered assistant (Nexie)** scoped to the customer's company\\n- **MSP admin endpoints** for managing portal users and roles\\n- **Audit logging** of all portal activity\\n\\nThe portal is multi-tenant and strictly scoped: every query is bound to `tenant_id` and `company_id` from the authenticated session. A compromised handler cannot leak data across companies.\\n\\n## Architecture\\n\\n```\\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n\u2502 HTTP Requests                                           \u2502\\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                 \u2502\\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n        \u2502 Public Routes   \u2502\\n        \u2502 (login, auth)   \u2502\\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                 \u2502\\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n        \u2502 Middleware            \u2502\\n        \u2502 (JWT + session check) \u2502\\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                 \u2502\\n    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n    \u2502            \u2502                \u2502\\n\u250c\u2500\u2500\u2500\u25bc\u2500\u2500\u2510  \u250c\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2510  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2510\\n\u2502Pages \u2502  \u2502 Nexie      \u2502  \u2502 Admin     \u2502\\n\u2502      \u2502  \u2502 (AI tools) \u2502  \u2502 (MSP mgmt)\u2502\\n\u2514\u2500\u2500\u2500\u252c\u2500\u2500\u2518  \u2514\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518\\n    \u2502           \u2502                \u2502\\n    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n                \u2502\\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\\n        \u2502 Store (pgx)    \u2502\\n        \u2502 + Audit log    \u2502\\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\\n```\\n\\n## Key Components\\n\\n### Handler (`handler.go`)\\n\\nThe main HTTP handler that wires routes and manages the portal lifecycle.\\n\\n**Key methods:**\\n- `NewHandler()` \u2014 constructs the handler with dependencies (store, JWT, renderer, mailer)\\n- `RegisterPublicRoutes()` \u2014 login page, magic link request/consume, logout\\n- `RegisterAuthedRoutes()` \u2014 landing page (wrapped with middleware)\\n- `RegisterPageRoutes()` \u2014 tickets, invoices, contracts, KB, profile (in `pages.go`)\\n- `RegisterAdminRoutes()` \u2014 MSP-only endpoints for user/role management (in `admin.go`)\\n- `SetMailer()` \u2014 wires email delivery for magic links\\n- `SetRiskService()` \u2014 wires the shared risk acceptance service\\n\\n**Magic link flow:**\\n1. User enters email on `/portal/login`\\n2. `handleLoginRequest()` issues a magic link via `IssueMagicLink()`\\n3. `deliverInvite()` emails the link (or logs it if no mailer configured)\\n4. User clicks link \u2192 `handleConsumeMagicLink()` validates token, creates session, sets cookie\\n5. Subsequent requests read the cookie and verify the session server-side\\n\\n### Middleware (`middleware.go`)\\n\\nEnforces portal authentication on protected routes.\\n\\n**Key functions:**\\n- `Wrap(handler)` \u2014 returns an HTTP middleware that:\\n  1. Reads the `nexus_portal_session` cookie (or `Authorization: Bearer` header)\\n  2. Verifies the JWT signature\\n  3. Checks the session exists in the database and is not revoked\\n  4. Loads the user's role permissions fresh from the DB\\n  5. Stores claims + permissions in the request context\\n  6. Redirects to login on any failure\\n\\n**Context helpers:**\\n- `ClaimsFromContext()` \u2014 retrieves `*auth.PortalClaims` (tenant, user, company, email, name)\\n- `PermsFromContext()` \u2014 retrieves the user's granted permissions\\n- `CompanyIDFromContext()` \u2014 hot-path helper for scoping queries\\n- `HasPermission()` \u2014 checks if a permission is granted (supports `:*` wildcards)\\n\\n**Cookie management:**\\n- `SetPortalCookie()` \u2014 writes `nexus_portal_session` with `HttpOnly`, `Secure`, `SameSite=Strict`\\n- `ClearPortalCookie()` \u2014 deletes the cookie on logout\\n- `readPortalCookie()` \u2014 reads the cookie or falls back to `Authorization` header\\n\\n### Store (`store.go`)\\n\\nPersistence layer for portal users, sessions, magic links, and audit logs.\\n\\n**Portal user operations:**\\n- `GetByEmail()` \u2014 case-insensitive lookup\\n- `GetByID()` \u2014 by UUID\\n- `EnsureFromContact()` \u2014 idempotent: creates a portal user from a contact if needed, assigns default role\\n\\n**Magic link operations:**\\n- `IssueMagicLink()` \u2014 mints a one-time token, stores hash + expiry\\n- `ConsumeMagicLink()` \u2014 atomically validates and burns a link, reactivates the user\\n\\n**Session operations:**\\n- `CreateSession()` \u2014 records a new session with TTL\\n- `IsSessionValid()` \u2014 checks if a session exists and is not revoked/expired\\n- `TouchSession()` \u2014 updates `last_seen_at` (fire-and-forget)\\n- `RevokeSession()` \u2014 marks a session as revoked (logout, admin kill, role change)\\n\\n**Role operations:**\\n- `DefaultRoleID()` \u2014 returns the tenant's default role (assigned to new invites)\\n- `LoadPermissionsForPortalUser()` \u2014 loads the user's permissions from their role\\n\\n**Audit:**\\n- `Audit()` \u2014 logs an action with tenant, user, company, IP, user agent, and optional resource details\\n\\n### Admin Routes (`admin.go`)\\n\\nMSP-only endpoints for managing portal users and roles. All require `portal:invite`, `users:*`, or `*` (admin) permission.\\n\\n**User management:**\\n- `POST /api/portal/invite` \u2014 invite a contact, issue magic link, email it\\n- `POST /api/portal/users/{id}/disable` \u2014 deactivate user, revoke sessions\\n- `POST /api/portal/users/{id}/enable` \u2014 reactivate user\\n- `POST /api/portal/users/{id}/resend` \u2014 re-issue magic link\\n- `POST /api/portal/users/{id}/role` \u2014 assign a role (admin only)\\n- `POST /api/portal/impersonate/{contact_id}` \u2014 MSP user opens portal as a contact (1h session, tagged in audit)\\n\\n**Role CRUD (admin only):**\\n- `GET /api/portal/roles` \u2014 list roles with user counts and permissions\\n- `POST /api/portal/roles` \u2014 create custom role (auto-slugifies label)\\n- `PUT /api/portal/roles/{id}` \u2014 update role (system roles: label + description only; custom: name + label + perms)\\n- `DELETE /api/portal/roles/{id}` \u2014 delete custom role (fails if users assigned)\\n- `POST /api/portal/roles/{id}/default` \u2014 set as default for new invites\\n\\n**Permission gates:**\\n- `canManagePortal()` \u2014 `portal:invite` OR `portal:*` OR `users:*` OR `*`\\n- `canAdminPortalRoles()` \u2014 `users:*` OR `*` (stricter: prevents privilege escalation)\\n\\n### Pages (`pages.go`)\\n\\nCustomer-facing pages. All require the corresponding `portal:page:*` permission.\\n\\n**Implemented pages:**\\n- **Tickets** \u2014 list (filtered by status), create, detail + notes\\n- **Invoices** \u2014 list (filtered by payment status), detail with line items + payments\\n- **Quotes** \u2014 list, detail with line items\\n- **Contracts** \u2014 list, detail with signature status\\n- **Knowledge Base** \u2014 search + read articles (visibility-filtered: `customer` or `public` only)\\n- **Profile** \u2014 view + edit self-safe fields (phone, mobile, title; email/name changes require MSP)\\n\\n**Key patterns:**\\n- `baseData()` \u2014 builds shared template data (name, company, permissions, nav gating)\\n- `requirePagePerm()` \u2014 early permission check, renders branded \\\"forbidden\\\" page\\n- All queries scope by `tenant_id` AND `company_id` from claims\\n- Rate limiting on ticket/note creation to prevent spam\\n\\n### Nexie (`nexie.go`, `nexie_tools.go`)\\n\\nAI-powered assistant scoped to the customer's company. Uses Anthropic's tool-use API.\\n\\n**Architecture:**\\n- `NexieHandler` \u2014 manages the chat loop with Claude\\n- `handleAsk()` \u2014 accepts a message + conversation history, calls Claude, executes tools, loops up to 5 times\\n- `callClaude()` \u2014 HTTP call to Anthropic API with system prompt + tools + messages\\n- `runTool()` \u2014 dispatches tool calls to Go implementations, enforces permissions server-side\\n\\n**Tools (defined in `nexie_tools.go`):**\\n- `list_my_tickets` \u2014 list customer's tickets (status filter)\\n- `get_ticket` \u2014 fetch ticket detail + public notes\\n- `create_ticket` \u2014 open new ticket (rate-limited per user)\\n- `add_note_to_ticket` \u2014 append public note\\n- `list_my_invoices` \u2014 list invoices (payment status filter)\\n- `get_contract_summary` \u2014 active contracts + block hours / retainer balances\\n- `search_kb` \u2014 search knowledge base (visibility-filtered)\\n- `get_my_profile` \u2014 fetch own contact info\\n\\n**Security model:**\\n- Every tool reads `TenantID`, `CompanyID`, `ContactID` from claims \u2014 the model cannot pass these\\n- Permissions are checked twice: (1) filtered from tool list before Claude sees them, (2) enforced in `runTool()`\\n- All queries bind `tenant_id` AND `company_id` directly from claims\\n- Rate limiting per portal user to control API costs\\n\\n**Cost tracking:**\\n- `logUsage()` \u2014 fire-and-forget write to `ai_usage_log` with token counts + estimated cost\\n- Logged per portal user + company for per-contact and per-company cost pills\\n\\n### Risk Acceptances (`risk_acceptances.go`)\\n\\nIntegration with the shared `internal/risk` service for customer signature workflows.\\n\\n**Routes:**\\n- `GET /portal/risk-acceptances` \u2014 list pending + accepted acceptances\\n- `GET /portal/risk-acceptances/{id}` \u2014 detail view, logs view event\\n- `POST /portal/risk-acceptances/{id}/sign` \u2014 submit signature (initials + full signature canvas data)\\n\\n**Key patterns:**\\n- Ownership guard: acceptance must belong to caller's company\\n- Audit trail: every view + signature is logged\\n- Signer info defaults to authenticated user but can be overridden\\n\\n## Permission Model\\n\\nPermissions are hierarchical strings with `:*` wildcard support:\\n\\n```\\nportal:page:tickets       \u2014 view tickets page\\nportal:page:invoices      \u2014 view invoices page\\nportal:page:contracts     \u2014 view contracts page\\nportal:page:quotes        \u2014 view quotes page\\nportal:page:kb            \u2014 view knowledge base\\nportal:page:profile       \u2014 view/edit profile\\n\\nportal:tool:list_tickets  \u2014 Nexie: list tickets\\nportal:tool:get_ticket    \u2014 Nexie: fetch ticket detail\\nportal:tool:create_ticket \u2014 Nexie + form: create ticket\\nportal:tool:add_note      \u2014 Nexie + form: add note\\nportal:tool:list_invoices \u2014 Nexie: list invoices\\nportal:tool:get_contracts \u2014 Nexie: contract summary\\nportal:tool:search_kb     \u2014 Nexie: search KB\\nportal:tool:edit_profile  \u2014 Nexie: get profile\\n\\nportal:invite             \u2014 MSP: invite contacts (manager default)\\nportal:*                  \u2014 all portal permissions\\n```\\n\\n**Role assignment:**\\n- Contacts are assigned a `portal_role_id` on invite\\n- Roles have a set of permissions stored in `portal_role_permissions`\\n- Permissions are loaded fresh on every request (role edits take effect instantly)\\n- System roles (e.g., `portal_admin`, `portal_user`) are immutable; custom roles are fully editable\\n\\n## Data Scoping\\n\\nEvery handler enforces strict multi-tenant isolation:\\n\\n```go\\n// Example: list tickets for the authenticated company\\nrows, _ := h.store.pool.Query(ctx, `\\n    SELECT ... FROM tickets\\n     WHERE tenant_id = $1 AND company_id = $2\\n`, c.TenantID, c.CompanyID)\\n```\\n\\n**Critical invariants:**\\n- `tenant_id` is always bound from claims (never from user input)\\n- `company_id` is always bound from claims (never from user input)\\n- `contact_id` is bound from claims where relevant (e.g., profile edits)\\n- Handlers that accept a resource ID (ticket number, invoice ID) verify ownership before returning data\\n\\n**Audit trail:**\\n- Every action logs `tenant_id`, `portal_user_id`, `company_id`, `contact_id`, IP, user agent\\n- Impersonation is tagged with both the MSP user ID and the portal user ID\\n- Tool calls log the tool name + input for debugging\\n\\n## Integration Points\\n\\n### With `internal/auth`\\n\\n- `JWTManager.SignPortalToken()` \u2014 signs portal JWTs with claims\\n- `JWTManager.VerifyPortalToken()` \u2014 verifies signature + audience\\n- `auth.HasPermission()` \u2014 MSP-side permission check (used in admin routes)\\n\\n### With `internal/ui`\\n\\n- `Renderer.RenderBlock()` \u2014 renders portal HTML templates\\n- Templates receive `baseData()` map with user info, company name, nav gating\\n\\n### With `internal/risk`\\n\\n- `risk.Service.List()` \u2014 fetch pending acceptances for a company\\n- `risk.Service.Get()` \u2014 fetch one acceptance\\n- `risk.Service.Sign()` \u2014 submit signature\\n- `risk.Service.LogView()` \u2014 log that a user viewed an acceptance\\n\\n### With `internal/billing` (email)\\n\\n- `SimpleMailer` \u2014 narrow interface for sending emails\\n- Caller adapts their email sender to this type\\n- Portal checks for active email accounts before claiming success\\n\\n## Common Patterns\\n\\n### Permission Checks\\n\\n```go\\n// Early check in handler\\nif !h.requirePagePerm(w, r, \\\"portal:page:tickets\\\") {\\n    return\\n}\\n\\n// Or inline\\nif !HasPermission(r.Context(), \\\"portal:tool:create_ticket\\\") {\\n    http.Error(w, \\\"forbidden\\\", http.StatusForbidden)\\n    return\\n}\\n```\\n\\n### Scoped Queries\\n\\n```go\\nc := ClaimsFromContext(r.Context())\\n// Always bind tenant_id AND company_id\\nrows, _ := h.store.pool.Query(ctx, `\\n    SELECT ... FROM table\\n     WHERE tenant_id = $1 AND company_id = $2\\n`, c.TenantID, c.CompanyID)\\n```\\n\\n### Rate Limiting\\n\\n```go\\nif !h.store.CheckRateLimit(r.Context(), \\\"create_ticket:\\\"+c.PortalUserID, 10) {\\n    http.Error(w, \\\"rate limited\\\", http.StatusTooManyRequests)\\n    return\\n}\\n```\\n\\n### Audit Logging\\n\\n```go\\nh.store.Audit(r.Context(), AuditEntry{\\n    TenantID: c.TenantID, PortalUserID: c.PortalUserID,\\n    CompanyID: c.CompanyID, ContactID: c.ContactID,\\n    Action: \\\"ticket_created\\\", ResourceType: \\\"ticket\\\", ResourceID: ticketID,\\n    IP: clientIP(r), UserAgent: r.UserAgent(),\\n})\\n```\\n\\n## Error Handling\\n\\n- **Invalid/expired magic link** \u2192 redirect to `/portal/login?error=expired_link`\\n- **Inactive user** \u2192 redirect to `/portal/login?error=inactive`\\n- **Permission denied** \u2192 render branded \\\"forbidden\\\" page or return 403\\n- **Not found** \u2192 return 404 (no enumeration leaks)\\n- **Rate limited** \u2192 return 429 or redirect with error query param\\n- **Database error** \u2192 log + return 500 (never expose SQL)\\n\\n## Testing Considerations\\n\\n- Magic links are one-time use; `ConsumeMagicLink()` atomically validates and burns\\n- Sessions are server-side revoked; a revoked JWT is still valid cryptographically but fails the DB check\\n- Role changes revoke active sessions so users re-authenticate with new permissions\\n- Impersonation sessions are tagged `msp-impersonation` and expire in 1 hour\\n- All queries are scoped by tenant + company; test cross-tenant isolation\",\"internal-procurement\":\"# internal \u2014 procurement\\n\\n# Procurement Module\\n\\nThe procurement module orchestrates the lifecycle of purchase orders from approval through distributor dispatch, retry, and receipt. It bridges the PO creation layer (internal/po) with the distributor integration layer (internal/distributor), managing idempotency, audit trails, and operator-driven remediation when dispatch fails.\\n\\n## Overview\\n\\nOnce a purchase order reaches `approved` status, the procurement service takes over:\\n\\n1. **Dispatch** \u2014 sends the PO to a distributor's provisioning API, minting an idempotency key to survive retries\\n2. **Remediation** \u2014 on failure, opens a queue entry for operator triage (SKU corrections, stock checks, auth issues)\\n3. **Retry** \u2014 operator can edit lines and re-dispatch, or retry as-is\\n4. **Abandon** \u2014 operator can cancel a failed PO without further attempts\\n5. **Receive** \u2014 for materials POs, marks lines received (warehouse scan path)\\n\\nThe module writes a complete audit trail to `purchase_order_audit` for every state transition, enabling timeline views and compliance reporting.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    Handler[\\\"HTTP Handler(handler.go)\\\"]\\n    Service[\\\"Service(service.go)\\\"]\\n    Audit[\\\"Audit Writer(audit.go)\\\"]\\n    Remediation[\\\"Remediation Queue(remediation.go)\\\"]\\n    Distributor[\\\"Distributor Wrapper(internal/distributor)\\\"]\\n    \\n    Handler --&gt;|Dispatch/Retry/Abandon| Service\\n    Service --&gt;|recordSuccess/recordFailure| Audit\\n    Service --&gt;|openRemediation| Remediation\\n    Service --&gt;|Provision call| Distributor\\n    Remediation --&gt;|suggestedFixFor| Remediation\\n```\\n\\n## Key Components\\n\\n### Service (`service.go`)\\n\\nThe core business logic. Requires two resolver functions wired at construction:\\n\\n- **`WrapperResolver`** \u2014 returns a distributor.Wrapper for a given tenant, MCP server, and distributor key. Typically backed by a per-tenant cache to avoid rebuilding MCP clients on the hot path.\\n- **`ClientCompanyResolver`** \u2014 maps a PO to its distributor-side customer ID (e.g., PAX8's \\\"companyId\\\"). Depends on tenant-specific subscription and contract data.\\n\\n#### Dispatch Flow\\n\\n`Dispatch(ctx, DispatchRequest)` is the entry point for sending an approved PO outbound:\\n\\n1. **Load PO + lines** \u2014 `loadPOForDispatch` reads the header and line items in one round-trip\\n2. **Resolve wrapper** \u2014 calls `WrapperResolver` to get the distributor client\\n3. **Check provisioner support** \u2014 returns `ErrNotProvisionable` if the wrapper doesn't implement `Provisioner` (materials-only distributors)\\n4. **Mint idempotency key** \u2014 format `po:{po_id}:attempt:{attempt_seq}`. Stamped on the PO row *before* the network call via `stampDispatchAttempt` so crash recovery reuses the same key\\n5. **Resolve company ID** \u2014 calls `ClientCompanyResolver`; fails fast if unmapped\\n6. **Call Provisioner.Provision** \u2014 sends the request to the distributor\\n7. **On success** \u2014 `recordSuccess` updates `procurement_status` to `provisioned` (or `awaiting_provision` if the distributor reported a pending state), writes audit entries, and resolves any open remediation\\n8. **On failure** \u2014 `recordFailure` opens a remediation queue entry, writes failure audit, and returns the original `ProvisionError` so the HTTP handler can decide whether to surface it to the UI\\n\\n**Idempotency:** Dispatch is safe to call repeatedly. The idempotency key is reused as long as `attempt_seq` hasn't changed. If the PO has already been dispatched (procurement_status != requested/po_approved), the attempt_seq is bumped only on explicit retry.\\n\\n#### Retry Flow\\n\\n`Retry(ctx, RetryRequest)` re-dispatches a failed PO:\\n\\n1. **Apply edits** (optional) \u2014 if `EditedLines` is supplied, `applyEditedLines` deletes the old lines and inserts the corrected ones, recomputing the PO total\\n2. **Bump attempt_seq** \u2014 increments the counter and resets `procurement_status` to `po_approved`\\n3. **Write retry audit** \u2014 records the operator's intent (with resolution label \\\"edit_and_retry\\\" or \\\"retry_as_is\\\")\\n4. **Call Dispatch** \u2014 re-dispatches with the new attempt_seq, which mints a fresh idempotency key\\n\\n#### Abandon Flow\\n\\n`Abandon(ctx, AbandonRequest)` cancels a failed PO:\\n\\n1. Sets `status = 'cancelled'` and `procurement_status = 'failed'`\\n2. Resolves the open remediation entry with resolution \\\"abandoned\\\"\\n3. Writes two audit entries: one for the cancellation, one for the remediation resolution\\n\\n#### Receive Flow\\n\\n`Receive(ctx, ReceiveRequest)` marks materials lines as received:\\n\\n1. Updates `received_qty` on each line\\n2. Checks if all lines are fully received via `bool_and(received_qty &gt;= quantity)`\\n3. If fully received, sets `procurement_status = 'received'` and stamps `received_date`\\n4. Writes audit entry with status \\\"partial_received\\\" or \\\"received\\\"\\n\\n**Note:** The contract line does NOT activate here. For SaaS POs, activation happens in `recordSuccess` when `procurement_status` reaches `provisioned`. For materials POs, activation is deferred to the QBO Bill posted webhook (Q11 lock).\\n\\n### Handler (`handler.go`)\\n\\nHTTP request router and response formatter. Exposes:\\n\\n**Action endpoints** (POST):\\n- `/api/procurement/{id}/dispatch` \u2014 calls `Service.Dispatch`\\n- `/api/procurement/{id}/retry` \u2014 calls `Service.Retry` with optional line edits and notes\\n- `/api/procurement/{id}/abandon` \u2014 calls `Service.Abandon` with a reason\\n- `/api/procurement/{id}/receive` \u2014 calls `Service.Receive` with line receipts\\n\\n**Read endpoints** (GET):\\n- `/api/procurement/remediation` \u2014 returns open/in-progress queue entries (JSON), filterable by distributor and error class\\n- `/api/procurement/{id}/timeline` \u2014 returns audit entries for a single PO (JSON)\\n\\n**Page endpoints** (GET, HTMX):\\n- `/procurement` \u2014 list view of all POs, filterable by distributor, procurement status, and approval status\\n- `/procurement/{id}` \u2014 detail view with lines, timeline, and action buttons\\n- `/distributors/remediation` \u2014 operator triage queue across all distributors\\n\\n#### Error Handling\\n\\n- **ProvisionError** \u2014 expected outcome when dispatch fails. Returns HTTP 202 with error class and message so the UI can route to remediation\\n- **ErrNotProvisionable** \u2014 distributor doesn't support provisioning. Returns HTTP 400\\n- Other errors \u2014 logged and return HTTP 500\\n\\n#### Query Helpers\\n\\n- `humanizeStatus(s)` \u2014 converts snake_case to Title Case for display\\n- `itoa(n)` \u2014 minimal int-to-string without strconv import\\n- `joinAnd(clauses)` \u2014 builds WHERE clause from a slice of conditions\\n\\n### Audit (`audit.go`)\\n\\n`writeAudit(ctx, tx, auditEntry)` inserts one row into `purchase_order_audit`. Non-fatal on failure \u2014 audit gaps are surfaced via the timeline UI but must not block a successful dispatch.\\n\\n**auditEntry fields:**\\n- `TenantID`, `PurchaseOrderID` \u2014 scope\\n- `Event` \u2014 canonical event type (created, submitted, approved, dispatched, etc.)\\n- `FromStatus`, `ToStatus` \u2014 coarse approval state transitions\\n- `FromProcurementStatus`, `ToProcurementStatus` \u2014 fine-grained dispatch lifecycle\\n- `ActorID`, `ActorKind` \u2014 who triggered the event (user, system, webhook, scheduler)\\n- `AttemptSeq` \u2014 which dispatch attempt (for retry tracking)\\n- `IdempotencyKey` \u2014 the key minted for this attempt\\n- `Payload` \u2014 JSON-marshalable context (subscription ID, order ID, error class, etc.)\\n- `Note` \u2014 operator-supplied text (retry notes, abandon reason)\\n\\nEmpty strings are stored as NULL via `NULLIF($n,'')` to keep the schema clean.\\n\\n### Remediation (`remediation.go`)\\n\\n**`openRemediation(ctx, tx, RemediationOpen)`** \u2014 inserts a `po_remediation_queue` row. Idempotent on (purchase_order_id, attempt_seq): if a row already exists with the same PO and attempt, it's returned rather than duplicated.\\n\\n**`suggestedFixFor(class)`** \u2014 returns an operator hint based on the error class:\\n- `ErrClassSKUInvalid` \u2014 \\\"Verify the distributor SKU on the catalog page, then Edit &amp; Retry.\\\"\\n- `ErrClassQtyUnavailable` \u2014 \\\"Check distributor stock. Reduce qty or pick an alternate SKU, then Edit &amp; Retry.\\\"\\n- `ErrClassAuth` \u2014 \\\"MCP server credentials may be stale. Re-authenticate in Settings \u2192 MCP Servers, then Retry As-Is.\\\"\\n- `ErrClassTransport` \u2014 \\\"Transient network/5xx \u2014 Retry As-Is. Persistent failures: check distributor status page.\\\"\\n\\n**`resolveRemediation(ctx, tx, poID, resolverID, resolution)`** \u2014 closes open remediation entries for a PO. Called after a successful retry or when abandoning.\\n\\n## Data Model\\n\\nThe module reads and writes three tables:\\n\\n### purchase_orders\\n- `id`, `tenant_id`, `po_number`, `status` (approval state)\\n- `procurement_status` \u2014 fine-grained dispatch lifecycle (requested, po_approved, dispatched, provisioned, received, failed, etc.)\\n- `distributor_key`, `mcp_server_id` \u2014 distributor binding\\n- `distributor_sku` \u2014 single-SKU SaaS dispatch (multi-line support TBD)\\n- `attempt_seq`, `idempotency_key` \u2014 dispatch retry tracking\\n- `contract_line_item_id` \u2014 SaaS contract linkage (optional)\\n- `auto_approved`, `auto_approval_reason` \u2014 threshold/agreement auto-approval metadata\\n- `dispatched_at`, `provisioned_at`, `received_date` \u2014 timestamps\\n- `last_response` \u2014 JSONB of the last distributor response\\n- `provisioned_subscription_id` \u2014 SaaS subscription ID from the distributor\\n\\n### po_line_items\\n- `id`, `po_id`, `line_number`, `description`\\n- `distributor_sku`, `quantity`, `unit_price`\\n- `line_status` \u2014 per-line status (optional, for future use)\\n- `received_qty` \u2014 materials path only\\n- `sort_order` \u2014 for edit ordering\\n\\n### purchase_order_audit\\n- `id`, `tenant_id`, `purchase_order_id`\\n- `event_type` \u2014 canonical event (created, dispatched, dispatch_failed, etc.)\\n- `from_status`, `to_status` \u2014 approval state transitions (nullable)\\n- `from_procurement_status`, `to_procurement_status` \u2014 dispatch lifecycle (nullable)\\n- `actor_id`, `actor_kind` \u2014 who triggered the event (nullable actor_id)\\n- `attempt_seq` \u2014 which dispatch attempt (nullable)\\n- `idempotency_key` \u2014 the key for this attempt (nullable)\\n- `payload` \u2014 JSONB context\\n- `note` \u2014 operator text (nullable)\\n- `created_at` \u2014 timestamp\\n\\n### po_remediation_queue\\n- `id`, `tenant_id`, `purchase_order_id`\\n- `distributor_key`, `error_class`, `error_message`\\n- `raw_error` \u2014 JSONB of the distributor's error response\\n- `attempt_seq` \u2014 which attempt failed\\n- `suggested_fix` \u2014 operator hint\\n- `status` \u2014 open, in_progress, resolved, archived\\n- `assignee_id` \u2014 operator assignment (nullable)\\n- `resolution` \u2014 how it was resolved (edit_and_retry, retry_as_is, abandoned)\\n- `resolved_by`, `resolved_at` \u2014 who closed it and when\\n- `created_at`, `updated_at`\\n\\n## Integration Points\\n\\n### Incoming\\n\\n- **internal/po** \u2014 PO creation and approval. Procurement takes over once `status = 'approved'` or auto-approval fires.\\n- **internal/auth** \u2014 extracts `TenantID` and `UserID` from request context for audit and multi-tenancy.\\n- **internal/ui** \u2014 renders list, detail, and remediation pages via the `Renderer`.\\n\\n### Outgoing\\n\\n- **internal/distributor** \u2014 calls `Provisioner.Provision` to send the PO outbound. Handles `ProvisionError` and `ProvisionResult`.\\n- **Database** \u2014 reads/writes purchase_orders, po_line_items, purchase_order_audit, po_remediation_queue.\\n\\n## Typical Workflows\\n\\n### SaaS Dispatch (Happy Path)\\n\\n1. PO approved \u2192 `procurement_status = 'po_approved'`\\n2. Operator clicks \\\"Dispatch\\\" \u2192 `handleDispatch` \u2192 `Service.Dispatch`\\n3. Service loads PO, resolves wrapper and company ID, mints idempotency key, calls `Provisioner.Provision`\\n4. Distributor returns subscription ID and status \\\"provisioned\\\"\\n5. Service writes `EventDispatched` and `EventProvisioned` audits, sets `procurement_status = 'provisioned'`, activates contract line\\n6. UI shows \\\"Provisioned\\\" status\\n\\n### Dispatch Failure &amp; Remediation\\n\\n1. Dispatch fails with `ErrClassSKUInvalid`\\n2. Service calls `recordFailure`, which:\\n   - Sets `procurement_status = 'failed'`\\n   - Opens a remediation queue entry with suggested fix\\n   - Writes `EventDispatchFailed` and `EventRemediationOpened` audits\\n3. Operator sees the PO in `/distributors/remediation` queue\\n4. Operator corrects the SKU and clicks \\\"Edit &amp; Retry\\\"\\n5. `handleRetry` calls `Service.Retry`, which:\\n   - Applies the edited lines\\n   - Bumps `attempt_seq`, resets `procurement_status = 'po_approved'`\\n   - Writes `EventRetried` audit\\n   - Calls `Dispatch` again with the new attempt_seq\\n6. If successful, remediation entry resolves to \\\"edit_and_retry\\\"\\n\\n### Materials Receive\\n\\n1. PO dispatched to materials distributor \u2192 `procurement_status = 'dispatched'`\\n2. Warehouse scans receipt \u2192 `handleReceive` \u2192 `Service.Receive`\\n3. Service updates `received_qty` on each line\\n4. If all lines fully received, sets `procurement_status = 'received'` and `received_date`\\n5. Writes `EventReceived` audit\\n6. Later, QBO Bill posted webhook activates the contract line (Q11 lock)\\n\\n## Testing Considerations\\n\\n- **Idempotency:** Calling `Dispatch` twice with the same PO should reuse the same idempotency key and not create duplicate remediation entries.\\n- **Audit completeness:** Every state transition should have a corresponding audit entry. Timeline views should show the full history.\\n- **Remediation resolution:** Successful retries should close open remediation entries. Abandoned POs should mark them resolved.\\n- **Multi-tenancy:** All queries filter by `tenant_id`. No cross-tenant data leakage.\\n- **Error handling:** `ProvisionError` should surface to the UI as HTTP 202 with error details, not 500.\",\"internal-product\":\"# internal \u2014 product\\n\\n# Product Module\\n\\nThe product module manages the SaaS product catalog and project delivery system for NexusOS. It provides HTTP handlers for CRUD operations on products, product-to-company assignments, and client engagement projects with task and milestone tracking.\\n\\n## Overview\\n\\nThe module serves two primary domains:\\n\\n1. **Product Catalog** \u2014 A searchable inventory of software services, hardware, and managed services that the MSP delivers to clients. Products include pricing, cost tracking, revenue classification, and legal document associations.\\n\\n2. **Project Management** \u2014 Client engagement tracking with kanban-style boards, task decomposition, milestone tracking, and budget/hours management.\\n\\nBoth domains are tenant-isolated and support both HTML (HTMX) and JSON API responses.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    Handler[\\\"Handler(pool, renderer, ai)\\\"]\\n    \\n    Handler --&gt;|Product CRUD| ProdDB[\\\"products table\\\"]\\n    Handler --&gt;|Assignments| AssignDB[\\\"product_assignments table\\\"]\\n    Handler --&gt;|Legal Docs| LegalDB[\\\"product_legal_documents table\\\"]\\n    \\n    Handler --&gt;|Project CRUD| ProjDB[\\\"projects table\\\"]\\n    Handler --&gt;|Tasks| TaskDB[\\\"project_tasks table\\\"]\\n    Handler --&gt;|Milestones| MilestoneDB[\\\"project_milestones table\\\"]\\n    \\n    Handler --&gt;|AI Analysis| AI[\\\"ai.Provider(Claude)\\\"]\\n    Handler --&gt;|Rendering| Renderer[\\\"ui.Renderer(HTMX/HTML)\\\"]\\n    \\n    Auth[\\\"auth.TenantIDFromContext\\\"]\\n    Handler -.-&gt;|Tenant isolation| Auth\\n```\\n\\n## Core Components\\n\\n### Handler\\n\\nThe `Handler` struct is the central dispatcher for all product and project endpoints.\\n\\n```go\\ntype Handler struct {\\n    pool     *pgxpool.Pool      // Database connection pool\\n    renderer *ui.Renderer       // HTML/HTMX template renderer\\n    ai       *ai.Provider       // Optional Claude AI for product analysis\\n}\\n```\\n\\n**Initialization:**\\n- `NewHandler(pool, renderer)` creates a handler with database and rendering capabilities.\\n- `SetAI(provider)` optionally configures the AI provider for product auto-classification.\\n\\n**Route Registration:**\\n- `RegisterRoutes(mux)` registers product endpoints.\\n- `RegisterProjectRoutes(mux)` registers project endpoints.\\n\\n### Product Types\\n\\n**Product** \u2014 Represents a SaaS product or service in the catalog.\\n\\nKey fields:\\n- `ID`, `TenantID`, `Name`, `Vendor`, `Category` \u2014 Basic identification.\\n- `PriceMonthly`, `PriceYearly`, `UnitCost`, `BillingType` \u2014 Pricing and cost.\\n- `RevenueType` \u2014 Classification: `mrr` (managed recurring), `orr` (other recurring), `nrr` (non-recurring), `product_sales`, `misc`.\\n- `RMMFeatures` \u2014 JSON array of RMM agent feature flags for automated deployment.\\n- `QBIncomeAccountID`, `QBExpenseAccountName` \u2014 QuickBooks integration.\\n- `LegalDocuments` \u2014 Attached legal documents (MSA, NDA, SLA, etc.).\\n- `DistributorSourceCount`, `BestDistributorCost` \u2014 Distributor coverage metadata (Phase 2).\\n\\nHelper methods:\\n- `MarginDollars()` \u2014 Price minus cost.\\n- `MarginPercent()` \u2014 Margin as percentage of price.\\n- `RevenueTypeLabel()` \u2014 Human-readable revenue type.\\n\\n**Assignment** \u2014 Links a product to a company with quantity and status.\\n\\n**ProductLegalDoc** \u2014 Junction record associating a legal document to a product with signature type (initial/signature).\\n\\n### Project Types\\n\\n**Project** \u2014 Client engagement or service delivery project.\\n\\nFields:\\n- `ID`, `TenantID`, `CompanyID`, `Name`, `Description` \u2014 Basic info.\\n- `Status` \u2014 One of: `planning`, `active`, `on_hold`, `completed`, `cancelled`.\\n- `Priority` \u2014 One of: `critical`, `high`, `medium`, `low`.\\n- `ProjectManager`, `StartDate`, `TargetEndDate`, `ActualEndDate` \u2014 Scheduling.\\n- `BudgetHours`, `BudgetAmount` \u2014 Budget tracking.\\n- `Tags` \u2014 JSON array of custom tags.\\n- Computed fields: `CompanyName`, `ManagerName`, `TaskCount`, `CompletedTaskCount`, `HoursUsed`.\\n\\n**ProjectTask** \u2014 Individual work item within a project.\\n\\nFields:\\n- `ID`, `ProjectID`, `Title`, `Description` \u2014 Basic info.\\n- `Status` \u2014 One of: `todo`, `in_progress`, `done`.\\n- `Priority` \u2014 One of: `critical`, `high`, `medium`, `low`.\\n- `AssignedTo`, `EstimatedHours`, `ActualHours`, `DueDate` \u2014 Assignment and tracking.\\n- `SortOrder` \u2014 For kanban column ordering.\\n\\n**ProjectMilestone** \u2014 Key deliverable or checkpoint.\\n\\nFields:\\n- `ID`, `ProjectID`, `Name`, `Description`, `DueDate`, `CompletedAt`, `SortOrder`.\\n\\n**ProjectFilter** \u2014 Query parameters for filtering project lists (status, priority, company, search).\\n\\n## Product Endpoints\\n\\n### List &amp; Search\\n\\n**`GET /products`** \u2014 Renders the product catalog page.\\n\\n- Loads all active products (or inactive if `?inactive=true`).\\n- Includes LEFT JOIN to distributor coverage for availability pills.\\n- Renders as HTMX partial if `HX-Request` header is present.\\n\\n**`GET /api/products/search`** \u2014 JSON search for invoice/quote line item dropdowns.\\n\\n- Query parameter: `q` (product name, category, vendor, description).\\n- Returns up to 50 results with pricing and cost fields.\\n\\n### CRUD\\n\\n**`GET /products/new`** \u2014 Renders the product creation form.\\n\\n- Defaults: `IsActive=true`, `RevenueType=mrr`, `BillingType=per_user`.\\n\\n**`POST /products`** \u2014 Creates a new product.\\n\\n- Accepts JSON body with product fields.\\n- **Auto-classification with Nexie AI:** If description, category, or QB account is missing and AI is configured, calls Claude to classify the product. Extracts description, category, revenue type, billing type, cost, and price from the response.\\n- Stores RMM features as JSON.\\n\\n**`GET /products/{id}/edit`** \u2014 Renders the product edit page.\\n\\n- Loads full product details including attached legal documents.\\n- Populates dropdown of available legal documents for attachment.\\n\\n**`PUT /products/{id}`** \u2014 Updates a product.\\n\\n- Accepts JSON body with partial or full product fields.\\n- Updates all fields including QB account references and subcontractor info.\\n\\n**`DELETE /products/{id}`** \u2014 Deletes a product.\\n\\n- Redirects to `/products` via HTMX.\\n\\n**`GET /products/{id}`** \u2014 JSON detail endpoint (backward compatibility).\\n\\n- Returns product as JSON with RMM features.\\n\\n### Image Upload\\n\\n**`POST /api/products/upload-image`** \u2014 Uploads a product image.\\n\\n- Accepts multipart form with `image` field.\\n- Validates extension: `.png`, `.jpg`, `.jpeg`, `.svg`, `.webp`, `.gif`.\\n- Generates random filename, stores in `data/uploads/products/`.\\n- Returns JSON with `image_url`.\\n\\n**`GET /uploads/products/{filename}`** \u2014 Serves uploaded images.\\n\\n- Static file handler.\\n\\n### AI Analysis\\n\\n**`POST /api/products/analyze`** \u2014 Analyzes a product name with Claude.\\n\\n- Request: `{\\\"name\\\": \\\"Microsoft 365\\\"}`.\\n- Claude returns JSON with:\\n  - `description`, `category`, `revenue_type`, `billing_type`\\n  - `avg_cost`, `avg_price`\\n  - `income_hint`, `expense_hint` (QB account hints)\\n  - `image_url` (product logo)\\n- Response includes `ai_cost` (token usage).\\n- Handles markdown-wrapped JSON responses.\\n\\n## Product Legal Document Management\\n\\nProducts can be associated with legal documents (MSA, NDA, SLA, etc.) with signature requirements.\\n\\n**`POST /products/{id}/legal-documents`** \u2014 Attaches a legal document.\\n\\n- Form parameters: `document_id`, `signature_type` (defaults to `signature`).\\n- Verifies product belongs to tenant.\\n- Upserts on conflict (allows re-attaching with different signature type).\\n- Returns HTMX partial with updated legal docs section.\\n\\n**`DELETE /products/{id}/legal-documents/{docID}`** \u2014 Detaches a legal document.\\n\\n- Verifies product belongs to tenant.\\n- Returns HTMX partial with updated legal docs section.\\n\\n**Internal helpers:**\\n- `loadProductLegalDocs(r, p)` \u2014 Loads attached documents for a product, joins with legal_documents table, and populates human-readable category labels.\\n- `renderLegalDocsSection(w, r, productID, tenantID)` \u2014 Renders the legal docs partial with available documents for the attach dropdown.\\n- `categoryLabel(cat)` \u2014 Maps category codes (msa, nda, sla, sow, toc, dpa, baa, order) to labels.\\n\\n## Product Assignments\\n\\n**`GET /products/assignments`** \u2014 Lists all product-to-company assignments.\\n\\n- Joins products, companies, and assignments tables.\\n- Renders as HTMX partial if requested.\\n\\n**`POST /products/assignments`** \u2014 Creates a new assignment.\\n\\n- Request: `{\\\"product_id\\\": \\\"...\\\", \\\"company_id\\\": \\\"...\\\", \\\"quantity\\\": 5}`.\\n- Status defaults to `active`.\\n\\n## Project Endpoints\\n\\n### List &amp; Board\\n\\n**`GET /projects`** \u2014 Renders the project list page.\\n\\n- Supports filters: `?status=active&amp;priority=high&amp;company_id=...&amp;search=...`.\\n- Loads task counts and hours used via subqueries.\\n- Displays status count badges.\\n- Renders as HTMX partial if requested.\\n\\n**`GET /projects/board`** \u2014 Renders a kanban board grouped by status.\\n\\n- Columns: `planning`, `active`, `on_hold`, `completed`, `cancelled`.\\n- Sorts by priority (critical \u2192 high \u2192 medium \u2192 low) then creation date.\\n\\n### Detail &amp; Form\\n\\n**`GET /projects/{id}`** \u2014 Renders the project detail page.\\n\\n- Loads project, tasks (with assignee names), and milestones.\\n- Tasks ordered by sort_order then creation date.\\n- Milestones ordered by sort_order then due_date.\\n\\n**`GET /projects/new`** \u2014 Renders the project creation form.\\n\\n- Populates dropdowns with active companies and users.\\n\\n### CRUD\\n\\n**`POST /api/projects`** \u2014 Creates a new project.\\n\\n- Accepts JSON or form-encoded body.\\n- Defaults: `status=planning`, `priority=medium`.\\n- Returns JSON with project ID or redirects to detail page if form POST.\\n\\n**`PATCH /api/projects/{id}`** \u2014 Updates a project.\\n\\n- Partial update: only non-empty fields are updated.\\n- Handles date/UUID nullification.\\n\\n**`DELETE /api/projects/{id}`** \u2014 Deletes a project.\\n\\n- Returns 204 No Content.\\n\\n### Tasks\\n\\n**`POST /api/projects/{id}/tasks`** \u2014 Creates a task.\\n\\n- Request: `{\\\"title\\\": \\\"...\\\", \\\"description\\\": \\\"...\\\", \\\"status\\\": \\\"todo\\\", \\\"priority\\\": \\\"medium\\\", \\\"assigned_to\\\": \\\"...\\\", \\\"estimated_hours\\\": 5, \\\"due_date\\\": \\\"2025-01-15\\\", \\\"sort_order\\\": 0}`.\\n- Defaults: `status=todo`, `priority=medium`.\\n- Returns JSON with task ID.\\n\\n**`PATCH /api/projects/{id}/tasks/{taskId}`** \u2014 Updates a task.\\n\\n- Partial update with same nullification logic as projects.\\n\\n### Milestones\\n\\n**`POST /api/projects/{id}/milestones`** \u2014 Creates a milestone.\\n\\n- Request: `{\\\"name\\\": \\\"...\\\", \\\"description\\\": \\\"...\\\", \\\"due_date\\\": \\\"2025-01-15\\\", \\\"sort_order\\\": 0}`.\\n- Returns JSON with milestone ID.\\n\\n## Tenant Isolation\\n\\nAll handlers extract `tenantID` from the request context via `auth.TenantIDFromContext(r.Context())` and include it in all database queries. This ensures:\\n\\n- Products, assignments, projects, tasks, and milestones are scoped to the tenant.\\n- No cross-tenant data leakage.\\n- Multi-tenant safety at the query level.\\n\\n## Rendering\\n\\nThe handler uses `ui.Renderer` to render templates:\\n\\n- **HTMX partials** \u2014 If `HX-Request: true` header is present, renders only the partial (no layout).\\n- **Full pages** \u2014 Otherwise renders the full HTML page with layout.\\n\\nTemplate paths:\\n- `products/catalog.html` \u2014 Product list.\\n- `products/product_form.html` \u2014 Product create/edit form.\\n- `products/product_legal_docs_partial.html` \u2014 Legal docs section (HTMX).\\n- `products/assignments.html` \u2014 Assignment list.\\n- `projects/list.html` \u2014 Project list.\\n- `projects/board.html` \u2014 Kanban board.\\n- `projects/detail.html` \u2014 Project detail with tasks and milestones.\\n- `projects/project_form.html` \u2014 Project create form.\\n\\n## Database Schema (Inferred)\\n\\n**products**\\n- `id`, `tenant_id`, `name`, `vendor`, `category`, `price_monthly`, `price_yearly`, `unit_cost`, `billing_type`, `rmm_features` (JSON), `description`, `is_active`, `revenue_type`, `qb_income_account_id`, `qb_income_account_name`, `qb_expense_account_id`, `qb_expense_account_name`, `is_subcontracted`, `preferred_contractor`, `contractor_notes`, `image_url`, `sku`, `manufacturer_part_number`, `vendor_url`, `default_supplier`, `updated_at`.\\n\\n**product_assignments**\\n- `id`, `tenant_id`, `product_id`, `company_id`, `quantity`, `status`.\\n\\n**product_legal_documents**\\n- `id`, `product_id`, `document_id`, `signature_type`, `sort_order`, `created_at`.\\n\\n**projects**\\n- `id`, `tenant_id`, `company_id`, `name`, `description`, `status`, `priority`, `project_manager`, `start_date`, `target_end_date`, `actual_end_date`, `budget_hours`, `budget_amount`, `tags` (JSON), `created_at`, `updated_at`.\\n\\n**project_tasks**\\n- `id`, `tenant_id`, `project_id`, `title`, `description`, `status`, `priority`, `assigned_to`, `estimated_hours`, `actual_hours`, `due_date`, `sort_order`, `created_at`, `updated_at`.\\n\\n**project_milestones**\\n- `id`, `tenant_id`, `project_id`, `name`, `description`, `due_date`, `completed_at`, `sort_order`, `created_at`.\\n\\n**legal_documents** (referenced, not managed by this module)\\n- `id`, `tenant_id`, `name`, `category`, `is_active`.\\n\\n**companies** (referenced, not managed by this module)\\n- `id`, `name`, `is_active`.\\n\\n**users** (referenced, not managed by this module)\\n- `id`, `full_name`, `is_active`.\\n\\n## Integration Points\\n\\n- **Auth** \u2014 `auth.TenantIDFromContext()` for tenant isolation.\\n- **UI** \u2014 `ui.Renderer` for HTMX and HTML rendering.\\n- **AI** \u2014 `ai.Provider` (Claude) for product auto-classification and analysis.\\n- **Distributor Sync** \u2014 Reads from `distributor_products` and `distributor_pricing` tables (Phase 2).\\n\\n## Common Patterns\\n\\n**Null Coalescing in Queries:**\\n```go\\nCOALESCE(p.price_monthly, 0)  // Default to 0 if NULL\\nNULLIF($1, '')::uuid          // Convert empty string to NULL for UUID\\n```\\n\\n**JSON Marshaling:**\\n```go\\nfeaturesJSON, _ := json.Marshal(p.RMMFeatures)\\n// Store as string in database\\n// Unmarshal on retrieval\\n```\\n\\n**Markdown-Wrapped JSON Parsing:**\\nWhen Claude wraps JSON in markdown code blocks, the handler extracts the JSON substring:\\n```go\\nstart := 0\\nfor i, c := range response {\\n    if c == '{' { start = i; break }\\n}\\nend := len(response)\\nfor i := len(response) - 1; i &gt;= 0; i-- {\\n    if response[i] == '}' { end = i + 1; break }\\n}\\njson.Unmarshal([]byte(response[start:end]), &amp;result)\\n```\\n\\n**Dynamic Query Building:**\\nProject list filters are built dynamically with argument indexing:\\n```go\\nargIdx := 2\\nif filter.Status != \\\"\\\" {\\n    query += \\\" AND p.status = $\\\" + fmtArgIdx(argIdx)\\n    args = append(args, filter.Status)\\n    argIdx++\\n}\\n```\\n\\n## Error Handling\\n\\n- Database errors return 500 with generic \\\"database error\\\" or specific error message.\\n- Missing resources return 404.\\n- Invalid JSON bodies return 400.\\n- AI unavailability returns 503.\\n- File upload validation returns 400 for invalid extensions.\\n\\n## Future Considerations\\n\\n- **Distributor Integration** \u2014 Phase 2 will expand distributor_products and distributor_pricing queries for real-time pricing and availability.\\n- **Bulk Operations** \u2014 CSV import/export for products and assignments.\\n- **Audit Logging** \u2014 Track product and project changes for compliance.\\n- **Webhooks** \u2014 Notify external systems when projects reach milestones.\",\"internal-project\":\"# internal \u2014 project\\n\\n# Project Module Documentation\\n\\n## Overview\\n\\nThe `internal/project` module implements a comprehensive Project Management (PMP)-aligned project management system with full support for job costing, Earned Value Management (EVM), Work Breakdown Structure (WBS), risk management, and change control. It provides HTTP handlers for CRUD operations on projects and their related entities, along with business logic for cost aggregation and EVM metrics calculation.\\n\\nThe module is designed for multi-tenant SaaS deployment with strict tenant isolation at every layer. All database queries include tenant scoping, and all user-facing endpoints extract tenant and user context from the request.\\n\\n## Core Responsibilities\\n\\n1. **Project Lifecycle Management** \u2014 Create, read, update, delete projects with full PMP process group tracking (Initiating \u2192 Planning \u2192 Executing \u2192 Monitoring &amp; Controlling \u2192 Closing)\\n2. **Task &amp; WBS Management** \u2014 Hierarchical task structures with dependencies, assignments, and milestone tracking\\n3. **Job Costing** \u2014 Aggregate labor costs (from time entries), material costs (expenses + purchase orders), and overhead into a unified cost summary\\n4. **EVM Metrics** \u2014 Calculate Planned Value (PV), Earned Value (EV), Actual Cost (AC), and derived metrics (SPI, CPI, EAC, VAC, etc.)\\n5. **Change Control** \u2014 Manage change requests with internal approval workflows and client e-signature integration\\n6. **Risk &amp; Stakeholder Management** \u2014 Track project risks and stakeholder engagement\\n7. **Billing Integration** \u2014 Support multiple billing methods (time &amp; materials, fixed price, milestone-based) and invoice generation\\n\\n## Architecture\\n\\n### Handler Structure\\n\\nThe `Handler` type wraps a database pool and UI renderer:\\n\\n```go\\ntype Handler struct {\\n    pool     *pgxpool.Pool\\n    renderer *ui.Renderer\\n}\\n```\\n\\nAll HTTP handlers follow a consistent pattern:\\n- Extract tenant ID and user ID from request context (via `auth.TenantIDFromContext`, `auth.UserIDFromContext`)\\n- Parse path parameters and query strings\\n- Execute database queries with tenant scoping\\n- Return HTML (for page views) or JSON (for API endpoints)\\n\\n### Route Organization\\n\\nRoutes are registered in `RegisterRoutes()` and grouped by feature:\\n\\n- **Project CRUD**: `/projects`, `/projects/{id}`, `/projects/{id}/edit`\\n- **Tab Views** (HTMX partials): `/projects/{id}/overview`, `/projects/{id}/tasks`, `/projects/{id}/evm`, etc.\\n- **Data APIs** (JSON): `/api/projects/{id}/tasks`, `/api/projects/{id}/members`, etc.\\n- **Entity CRUD**: Task, Milestone, Phase, Member, Budget, Expense, PO, Risk, Change Request, Stakeholder, Lesson, Note\\n- **EVM**: `/api/projects/{id}/evm/snapshot`, `/api/projects/{id}/evm/history`\\n- **Change Request E-Signature**: `/sign/change-request/{token}` (public, no auth)\\n\\n### Generic CRUD Helpers\\n\\nThree helper functions power most entity operations:\\n\\n#### `genericCreate(w, r, table, projectFKCol)`\\nInserts a row from a JSON body. The table name and all body keys are validated against an allowlist (`safeTableColumns` in `safe_columns.go`) before SQL is built. This prevents SQL injection even if a table name were somehow dynamic. Tenant ID and the project FK are forced server-side.\\n\\n#### `genericJSONUpdate(w, r, table, rowID)`\\nUpdates a row from a JSON body. Same allowlist validation as `genericCreate`. Strips immutable fields (`id`, `tenant_id`, `project_id`, `created_at`) before consulting the allowlist.\\n\\n#### `genericDelete(w, r, table, rowID)`\\nDeletes a row by ID with tenant scoping. No body parsing needed.\\n\\n**Security Model**: The allowlist is the primary defense. Even if a caller somehow passed a dynamic table name (which they don't \u2014 all callers pass literals), the column allowlist would still prevent unauthorized field updates. Unknown columns are silently dropped, not rejected with an error.\\n\\n## Key Components\\n\\n### Job Costing (`costing.go`)\\n\\n**`ComputeJobCostSummary(ctx, pool, tenantID, projectID, overheadRate) \u2192 *JobCostSummary`**\\n\\nAggregates all cost data for a project into a single summary:\\n\\n1. **Labor Cost**: Sum of `(time_entry.duration_minutes / 60 * project_member.cost_rate)` across all work time entries\\n2. **Materials Cost**: Sum of approved/invoiced expenses in categories (materials, equipment, software_licenses, hosting) + received purchase order totals\\n3. **Subcontractor Cost**: Sum of approved/invoiced expenses in the \\\"subcontractor\\\" category\\n4. **Travel Cost**: Sum of approved/invoiced expenses in travel/meals categories\\n5. **Other Cost**: Sum of approved/invoiced expenses in unmapped categories\\n6. **Overhead Cost**: `labor_cost * overhead_rate`\\n7. **Total Cost**: Sum of all above\\n8. **Revenue &amp; Margin**: Pulled from `projects.actual_revenue`; margin = revenue \u2212 total cost\\n9. **Budget Variance**: `total_budget \u2212 total_cost` (positive = under budget)\\n\\nAll queries are tenant-scoped and filter by status (approved/invoiced for expenses, received for POs).\\n\\n### EVM Metrics (`evm.go`)\\n\\n**`ComputePV(tasks, snapshotDate) \u2192 float64`**\\n\\nCalculates Planned Value from task schedule and estimated costs. For each task with start/end dates:\\n- If snapshot date is before task start: PV contribution = 0\\n- If snapshot date is after task end: PV contribution = estimated_cost (task should be done)\\n- Otherwise: PV contribution = `estimated_cost * (elapsed_days / total_days)`\\n\\n**`ComputeEV(tasks) \u2192 float64`**\\n\\nCalculates Earned Value from task completion percentages:\\n```\\nEV = Sum(task.estimated_cost * task.percent_complete / 100)\\n```\\n\\n**`ComputeEVMMetrics(bac, pv, ev, ac) \u2192 EVMSnapshot`**\\n\\nDerives all EVM metrics from Budget at Completion (BAC), Planned Value, Earned Value, and Actual Cost:\\n\\n| Metric | Formula | Interpretation |\\n|--------|---------|-----------------|\\n| SV (Schedule Variance) | EV \u2212 PV | Positive = ahead of schedule |\\n| CV (Cost Variance) | EV \u2212 AC | Positive = under budget |\\n| SPI (Schedule Performance Index) | EV / PV | &gt; 1 = ahead; &lt; 1 = behind |\\n| CPI (Cost Performance Index) | EV / AC | &gt; 1 = under budget; &lt; 1 = over budget |\\n| EAC (Estimate at Completion) | BAC / CPI | Projected final cost |\\n| ETC (Estimate to Complete) | EAC \u2212 AC | Remaining work cost |\\n| VAC (Variance at Completion) | BAC \u2212 EAC | Projected final variance |\\n| TCPI (To-Complete Performance Index) | (BAC \u2212 EV) / (BAC \u2212 AC) | Required efficiency to finish on budget |\\n\\n**`GenerateEVMSnapshot(ctx, pool, tenantID, projectID, userID) \u2192 *EVMSnapshot`**\\n\\nOrchestrates the full EVM calculation:\\n1. Fetch project BAC (sum of all budget categories)\\n2. Fetch all tasks and compute PV and EV\\n3. Fetch actual cost (labor + approved expenses + received POs)\\n4. Call `ComputeEVMMetrics()` to derive all metrics\\n5. Upsert snapshot into `project_evm_snapshots` table (one per project per day)\\n\\nSnapshots are persisted for historical tracking and trend analysis.\\n\\n### Change Request E-Signature (`handler.go`)\\n\\nThe module includes a complete change request workflow with client e-signature support:\\n\\n**`handleSendCRForSigning()`**\\n- Generates a random 32-byte approval token\\n- Checks project approval threshold: if cost impact exceeds threshold and an internal approver is designated, sets status to `pending_internal_approval` (requires internal sign-off before sending to client)\\n- Otherwise sets status to `pending_signature`\\n- Sends email to stakeholder with signing link: `/sign/change-request/{token}`\\n- Logs action to project notes\\n\\n**`handleCRSigningPage()`**\\n- Public endpoint (no auth required)\\n- Loads CR by approval token\\n- Renders signing form with attached documents\\n- Tracks first view by updating `updated_at`\\n\\n**`handleSignCR()`**\\n- Processes e-signature submission\\n- Validates signer name, email, and signature data\\n- Captures client IP for audit trail\\n- Updates CR status to `approved`\\n- Optionally auto-creates a purchase order if configured\\n- Logs signature to project notes\\n\\n**`handleInternalApproveCR()`**\\n- Allows designated approver to approve CR internally\\n- Transitions status from `pending_internal_approval` \u2192 `internally_approved`\\n- Notifies project manager that CR is ready to send to client\\n\\n**`handleAttachCRDocument()` / `handleRemoveCRDocument()`**\\n- Attach/detach legal documents to/from a CR\\n- Documents are stored in `legal_documents` table; CR-document links are in `cr_documents` table\\n\\n**`handleCRDocumentDownload()`**\\n- Public endpoint for downloading documents during signing\\n- Verifies token \u2192 CR \u2192 document attachment chain before serving file\\n\\n### Data List APIs\\n\\nSeveral endpoints return JSON lists of related entities for use by Alpine.js tab components:\\n\\n- `handleListJSON(table, fkCol)` \u2014 Generic handler for tables with a project FK\\n- `handleListMembersJSON()` \u2014 Project members with hours logged and cost-to-date\\n- `handleListExpensesJSON()` \u2014 Expenses with vendor and approval status\\n- `handleListPOsJSON()` \u2014 Purchase orders with line items\\n- `handleListNotesJSON()` \u2014 Project notes with author and timestamp\\n\\nAll return empty arrays if no rows found (never null).\\n\\n## Data Model Relationships\\n\\n```\\nprojects\\n\u251c\u2500\u2500 project_tasks (hierarchical via parent_task_id)\\n\u2502   \u251c\u2500\u2500 project_task_dependencies (FS/FF/SS/SF)\\n\u2502   \u2514\u2500\u2500 project_task_assignments (user + role + hours)\\n\u251c\u2500\u2500 project_milestones (billing milestones)\\n\u251c\u2500\u2500 project_phases (PMP process groups)\\n\u251c\u2500\u2500 project_members (team roster with cost/billing rates)\\n\u251c\u2500\u2500 project_budget_items (budget line items)\\n\u251c\u2500\u2500 project_expenses (labor, materials, travel, etc.)\\n\u251c\u2500\u2500 project_purchase_orders (vendor orders)\\n\u2502   \u2514\u2500\u2500 po_line_items\\n\u251c\u2500\u2500 project_risks (risk register)\\n\u251c\u2500\u2500 project_change_requests (change control)\\n\u2502   \u2514\u2500\u2500 cr_documents (attached legal docs)\\n\u251c\u2500\u2500 project_stakeholders (engagement matrix)\\n\u251c\u2500\u2500 project_notes (audit trail + comments)\\n\u251c\u2500\u2500 project_lessons_learned (post-project review)\\n\u2514\u2500\u2500 project_evm_snapshots (historical EVM metrics)\\n```\\n\\nAll tables include `tenant_id` and `created_at` columns. Most include `updated_at` for optimistic locking.\\n\\n## Tenant Isolation\\n\\nEvery database query includes a `WHERE tenant_id = $X` clause. This is enforced at the handler level:\\n\\n```go\\ntenantID := auth.TenantIDFromContext(r.Context())\\n// All subsequent queries include tenantID as a parameter\\n```\\n\\nThe context is populated by auth middleware before the request reaches the handler. If a handler somehow fails to include tenant scoping, the query will return no rows (not an error), making the bug visible in testing.\\n\\n## Billing Integration\\n\\nThe module integrates with the `billing` package for:\\n\\n- **Email sending**: `billing.NewEmailSender()` is used to send CR signing links and approval notifications\\n- **Invoice generation**: `handleGenerateProjectInvoice()` is stubbed but would call billing APIs to create invoices from project data\\n\\nProjects support multiple billing methods:\\n- `time_and_materials` \u2014 Bill by hours worked + expenses\\n- `fixed_price` \u2014 Fixed contract amount\\n- `milestone_based` \u2014 Bill at milestone completion\\n- `not_billable` \u2014 Internal project\\n\\n## UI Rendering\\n\\nThe module uses the `ui.Renderer` to render both full pages and HTMX partials:\\n\\n- **Full pages**: `handleProjectDetail()`, `handleProjectList()`, `handleProjectForm()`\\n- **HTMX partials**: `handleProjectOverview()`, `handleProjectTasks()`, `handleProjectEVM()`, etc.\\n- **Public pages**: `handleCRSigningPage()` (no auth required)\\n\\nThe renderer loads user preferences (accent color, theme) from context and applies them to templates.\\n\\n## Error Handling\\n\\nThe module follows a consistent error pattern:\\n\\n- **Database errors**: Logged with `log.Printf()`, returned as HTTP 500 with a generic error message\\n- **Validation errors**: Returned as HTTP 400 with a specific error message\\n- **Not found**: Returned as HTTP 404\\n- **Forbidden**: Returned as HTTP 403 (e.g., only designated approver can approve CR)\\n\\nJSON responses use a simple `{\\\"error\\\": \\\"message\\\"}` format. HTML responses render error pages via the renderer.\\n\\n## Testing Considerations\\n\\nKey areas to test:\\n\\n1. **Tenant isolation**: Verify that queries for one tenant never return data from another tenant\\n2. **EVM calculations**: Test PV/EV/AC computation with various task schedules and completion percentages\\n3. **Cost aggregation**: Verify that labor, materials, subcontractor, and travel costs are correctly summed\\n4. **Change request workflow**: Test the full flow from submission \u2192 internal approval \u2192 client signature \u2192 PO creation\\n5. **Generic CRUD**: Verify that the allowlist prevents unauthorized column updates\\n6. **Dependency resolution**: Test task dependencies with various lag days and dependency types\\n\\n## Common Patterns\\n\\n### Extracting Context Values\\n```go\\ntenantID := auth.TenantIDFromContext(r.Context())\\nuserID := auth.UserIDFromContext(r.Context())\\nprojectID := r.PathValue(\\\"id\\\")\\n```\\n\\n### Scanning Optional Fields\\n```go\\nvar name string\\nrow.Scan(&amp;name)  // NULL becomes \\\"\\\"\\n// or\\nvar ptr *string\\nrow.Scan(&amp;ptr)   // NULL becomes nil\\n```\\n\\n### Building Dynamic Queries\\n```go\\nquery := \\\"SELECT ... FROM table WHERE tenant_id = $1\\\"\\nargs := []interface{}{tenantID}\\nargIdx := 2\\n\\nif filter.Status != \\\"\\\" {\\n    query += \\\" AND status = $\\\" + fmtArg(argIdx)\\n    args = append(args, filter.Status)\\n    argIdx++\\n}\\n```\\n\\n### Returning JSON\\n```go\\njsonResponse(w, http.StatusOK, map[string]interface{}{\\n    \\\"id\\\": id,\\n    \\\"status\\\": \\\"created\\\",\\n})\\n```\\n\\n## Future Enhancements\\n\\n- **Invoice generation**: Implement `handleGenerateProjectInvoice()` to create invoices from project billing data\\n- **Gantt chart rendering**: Server-side Gantt chart generation or integration with a charting library\\n- **Resource leveling**: Detect over-allocated team members and suggest rebalancing\\n- **Critical path analysis**: Automatically identify critical path tasks and highlight schedule risks\\n- **Budget forecasting**: Predict final project cost based on current burn rate and remaining work\\n- **Notifications**: Integrate with a notification service to alert stakeholders of milestones, risks, and approvals\",\"internal-push\":\"# internal \u2014 push\\n\\n# Push Notification Module\\n\\nThe `internal/push` module implements Web Push Protocol support for NexusOS PSA, enabling real-time browser notifications for events like purchase order approvals and ticket updates. It handles VAPID key generation, subscription management, and delivery to user browsers.\\n\\n## Overview\\n\\nWeb Push notifications require a public/private key pair (VAPID) that the server uses to sign requests to the push service (e.g., Firebase Cloud Messaging, Apple Push Notification service). This module:\\n\\n- Generates and caches VAPID keys per tenant\\n- Stores browser push subscriptions in the database\\n- Delivers notifications to subscribed browsers via the Web Push Protocol\\n- Cleans up expired subscriptions automatically\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    Browser[\\\"Browser(Service Worker)\\\"]\\n    Handler[\\\"Handler\\\"]\\n    DB[\\\"Database(push_subscriptions,tenant_settings)\\\"]\\n    PushService[\\\"Push Service(FCM, APNs, etc.)\\\"]\\n    \\n    Browser --&gt;|Subscribe| Handler\\n    Handler --&gt;|Store| DB\\n    Handler --&gt;|Send| PushService\\n    PushService --&gt;|Deliver| Browser\\n```\\n\\n## Core Components\\n\\n### Handler\\n\\nThe `Handler` struct is the main entry point. It manages:\\n\\n- **Database connection pool** (`pool`): PostgreSQL connection for subscriptions and VAPID keys\\n- **VAPID key cache** (`vapidKeys`): In-memory map of tenant ID \u2192 key pair, protected by `mu` to avoid repeated database lookups\\n\\n```go\\ntype Handler struct {\\n    pool      *pgxpool.Pool\\n    mu        sync.RWMutex\\n    vapidKeys map[string]*vapidPair\\n}\\n```\\n\\nInitialize with `NewHandler(pool)`.\\n\\n### VAPID Key Management\\n\\nVAPID (Voluntary Application Server Identification) keys are ECDSA P-256 key pairs. The module stores them in `tenant_settings` and caches them in memory.\\n\\n**`getOrCreateVAPID(ctx, tenantID)`** follows this flow:\\n\\n1. Check in-memory cache (read lock)\\n2. Query `tenant_settings` for existing keys\\n3. If not found, generate new P-256 key pair using `crypto/ecdsa`\\n4. Encode public key as uncompressed point in base64url format (required by Web Push spec)\\n5. Encode private key's D component in base64url\\n6. Store both in `tenant_settings` with upsert logic\\n7. Cache in memory (write lock)\\n\\nKeys are generated once per tenant and reused for all subscriptions.\\n\\n### Push Subscriptions\\n\\nSubscriptions are stored in the `push_subscriptions` table with:\\n\\n- `endpoint`: The push service URL provided by the browser\\n- `key_p256dh`: Browser's public key for encryption\\n- `key_auth`: Authentication secret\\n- `user_agent`: Browser/device identifier\\n- `last_used_at`: Timestamp of last successful delivery\\n\\nThe unique constraint is on `(user_id, endpoint)` \u2014 the same user can have multiple subscriptions (desktop, mobile, etc.), but only one per endpoint.\\n\\n### PushPayload\\n\\nThe notification content sent to browsers:\\n\\n```go\\ntype PushPayload struct {\\n    Title  string // Notification title\\n    Body   string // Notification body\\n    Icon   string // Icon URL (optional)\\n    Badge  string // Badge URL (optional)\\n    Link   string // URL to open on click (optional)\\n    Tag    string // Grouping tag (optional)\\n}\\n```\\n\\nSerialized as JSON and encrypted by the Web Push library before transmission.\\n\\n## HTTP Endpoints\\n\\n### `GET /api/push/vapid-key`\\n\\nReturns the public VAPID key for the current tenant. Called by the browser during service worker registration.\\n\\n**Response:**\\n```json\\n{\\\"public_key\\\": \\\"BEn...\\\"}\\n```\\n\\n**Auth:** Requires valid tenant context (from `auth.TenantIDFromContext`).\\n\\n### `POST /api/push/subscribe`\\n\\nRegisters a browser subscription. Called by the service worker after the user grants notification permission.\\n\\n**Request body:**\\n```json\\n{\\n  \\\"endpoint\\\": \\\"https://fcm.googleapis.com/...\\\",\\n  \\\"keys\\\": {\\n    \\\"p256dh\\\": \\\"...\\\",\\n    \\\"auth\\\": \\\"...\\\"\\n  }\\n}\\n```\\n\\n**Behavior:**\\n- Validates endpoint is present\\n- Inserts or updates subscription in database\\n- Updates `last_used_at` on re-subscription\\n- Logs subscription with truncated endpoint for debugging\\n\\n**Auth:** Requires valid user and tenant context.\\n\\n### `DELETE /api/push/subscribe`\\n\\nUnsubscribes a browser.\\n\\n**Request body:**\\n```json\\n{\\\"endpoint\\\": \\\"https://fcm.googleapis.com/...\\\"}\\n```\\n\\n**Auth:** Requires valid user context.\\n\\n## Push Delivery\\n\\n### `SendToUser(ctx, tenantID, userID, payload)`\\n\\nSends a notification to all subscriptions for a user. Called asynchronously from notification creation code (e.g., PO approval handlers, ticket update handlers).\\n\\n**Flow:**\\n\\n1. Retrieve VAPID keys for tenant (via `getOrCreateVAPID`)\\n2. Query all subscriptions for the user\\n3. For each subscription:\\n   - Construct `webpush.Subscription` from stored keys\\n   - Call `webpush.SendNotification()` with payload, VAPID keys, and TTL (24 hours)\\n   - Handle response:\\n     - **2xx**: Mark success, update `last_used_at`\\n     - **410 Gone**: Subscription expired, delete from database\\n     - **Other errors**: Log failure, attempt cleanup if 410 detected in error message\\n4. Log aggregate results (sent/failed counts)\\n\\n**Error handling:**\\n- VAPID retrieval errors are logged; delivery is skipped\\n- Database query errors are logged silently\\n- Individual subscription failures don't block others\\n- Expired subscriptions are cleaned up automatically\\n\\n**Concurrency:** Runs in a goroutine; errors are logged, not returned to caller.\\n\\n## Database Schema\\n\\nThe module expects two tables:\\n\\n**`push_subscriptions`**\\n```sql\\nCREATE TABLE push_subscriptions (\\n    id SERIAL PRIMARY KEY,\\n    tenant_id TEXT NOT NULL,\\n    user_id TEXT NOT NULL,\\n    endpoint TEXT NOT NULL,\\n    key_p256dh TEXT NOT NULL,\\n    key_auth TEXT NOT NULL,\\n    user_agent TEXT,\\n    last_used_at TIMESTAMP,\\n    UNIQUE(user_id, endpoint)\\n);\\n```\\n\\n**`tenant_settings`**\\n```sql\\nCREATE TABLE tenant_settings (\\n    tenant_id TEXT NOT NULL,\\n    key TEXT NOT NULL,\\n    value TEXT NOT NULL,\\n    PRIMARY KEY(tenant_id, key)\\n);\\n```\\n\\n## Integration Points\\n\\n### Authentication\\n\\nThe module relies on `internal/auth` middleware to populate context:\\n\\n- `auth.TenantIDFromContext(ctx)`: Extracts tenant ID from request context\\n- `auth.UserIDFromContext(ctx)`: Extracts user ID from request context\\n\\nAll HTTP handlers require these to be present.\\n\\n### Notification Triggers\\n\\nOther modules call `SendToUser()` to deliver notifications:\\n\\n```go\\nh.SendToUser(ctx, tenantID, userID, PushPayload{\\n    Title: \\\"PO Approved\\\",\\n    Body:  \\\"Your purchase order has been approved\\\",\\n    Link:  \\\"/po/12345\\\",\\n})\\n```\\n\\nThe payload can be any JSON-serializable type; `PushPayload` is a convenience struct.\\n\\n### Route Registration\\n\\nThe handler registers its routes via `RegisterRoutes(mux)`, typically called during server initialization:\\n\\n```go\\npushHandler := push.NewHandler(pool)\\npushHandler.RegisterRoutes(mux)\\n```\\n\\n## Key Design Decisions\\n\\n**Per-tenant VAPID keys:** Each tenant has its own key pair, allowing multi-tenant isolation and independent key rotation.\\n\\n**In-memory caching:** VAPID keys are cached to avoid database queries on every notification. The cache is tenant-scoped and protected by a read-write lock for concurrent access.\\n\\n**Automatic cleanup:** Expired subscriptions (410 responses) are deleted immediately, preventing accumulation of stale entries.\\n\\n**Fire-and-forget delivery:** `SendToUser()` runs asynchronously and doesn't return errors. This prevents notification failures from blocking the main request path (e.g., PO approval).\\n\\n**Uncompressed P-256 format:** Public keys are encoded as uncompressed points (65 bytes) in base64url, as required by the Web Push Protocol specification.\",\"internal-quickbooks\":\"# internal \u2014 quickbooks\\n\\n# QuickBooks Online Integration Module\\n\\n## Overview\\n\\nThe `internal/quickbooks` module integrates NexusOS with QuickBooks Online (QBO), enabling bidirectional synchronization of financial data. QuickBooks is the single source of truth for all financial records\u2014NexusOS reads products, invoices, customers, and payments from QB, and pushes estimates (from quotes) and invoices (from billable time entries) back to QB.\\n\\n**Key principle:** Data flows in strictly one direction per entity type:\\n- **QB \u2192 NexusOS (inbound):** Products/Items, Invoices, Payments, Customers, Employees, Company Info\\n- **NexusOS \u2192 QB (outbound):** Estimates, Invoices, Purchase Orders, Time Activities\\n\\n## Architecture\\n\\n```mermaid\\ngraph TB\\n    OAuth[\\\"OAuth HandlerhandleOAuthStarthandleOAuthCallback\\\"]\\n    Client[\\\"HTTP ClientQuery/Create/ReadToken Refresh\\\"]\\n    Syncer[\\\"SyncerSyncAllSyncProductsSyncInvoices\\\"]\\n    Push[\\\"Push OperationsPushEstimateCreateInvoiceFromTime\\\"]\\n    Service[\\\"ServiceOrchestratorHTTP Routes\\\"]\\n    \\n    OAuth --&gt;|stores tokens| DB[(Database)]\\n    Client --&gt;|uses tokens| OAuth\\n    Syncer --&gt;|reads QB data| Client\\n    Syncer --&gt;|writes to DB| DB\\n    Push --&gt;|creates in QB| Client\\n    Push --&gt;|updates DB| DB\\n    Service --&gt;|coordinates| OAuth\\n    Service --&gt;|coordinates| Syncer\\n    Service --&gt;|coordinates| Push\\n```\\n\\n## Core Components\\n\\n### 1. Client (`client.go`)\\n\\nThe low-level HTTP client for the QuickBooks REST API. Handles all network communication, token management, and rate limiting.\\n\\n**Key responsibilities:**\\n- **Token lifecycle:** Loads active connections, detects expiration, refreshes tokens via OAuth2 refresh token flow\\n- **Rate limiting:** Enforces a sliding-window limit of 400 requests/minute per realm (QB's limit is 500; we stay under to be safe)\\n- **HTTP methods:** `Query()` (SELECT-like queries), `Create()` (POST new entities), `Read()` (GET by ID)\\n- **Automatic retry:** On 401 Unauthorized, refreshes the token and retries once\\n\\n**Token refresh flow:**\\n```\\nIsTokenExpired() \u2192 refreshToken() \u2192 POST /oauth2/v1/tokens/bearer\\n  \u2193\\nUpdate access_token, refresh_token, token_expires_at in database\\n  \u2193\\nRetry original request\\n```\\n\\n**Rate limiting:**\\nUses per-realm sliding windows. When the limit is reached, the client blocks until the window resets (logs a warning and sleeps).\\n\\n### 2. OAuth Handler (`oauth.go`)\\n\\nManages the OAuth2 authorization code flow for connecting a tenant to their QB company.\\n\\n**Flow:**\\n1. **Start** (`handleOAuthStart`): Generates a CSRF state token, stores it in a short-lived cookie, redirects to Intuit's auth endpoint\\n2. **Callback** (`handleOAuthCallback`): Validates the state token, exchanges the authorization code for tokens, stores the connection in the database\\n3. **Disconnect** (`handleDisconnect`): Marks the connection as inactive\\n4. **Status** (`handleStatus`): Returns current connection metadata\\n\\n**Credentials:** QB client_id and client_secret are stored in the tenant's `settings` JSONB column and loaded on demand.\\n\\n### 3. Syncer (`sync.go`)\\n\\nPulls data from QuickBooks and mirrors it into local cache tables. All sync operations are idempotent and use change detection (SHA256 hash of entity JSON) to avoid unnecessary writes.\\n\\n**Sync operations:**\\n\\n| Entity | Direction | Table | Notes |\\n|--------|-----------|-------|-------|\\n| Products/Items | QB \u2192 NexusOS | `products` | Service and NonInventory types only; skips QB system items |\\n| Customers | QB \u2192 NexusOS | `companies` | Matches by QB customer ID or name; creates new companies if needed |\\n| Invoices | QB \u2192 NexusOS | `invoices` + `invoice_line_items` | Incremental (since last sync); resolves company from customer ref |\\n| Payments | QB \u2192 NexusOS | `invoice_payments` | Links payments to invoices via QB LinkedTxn references |\\n| Employees | QB \u2192 NexusOS | `qb_employees` | Used for timesheet TimeActivity mapping |\\n| Company Info | QB \u2192 NexusOS | `tenants.settings` (JSONB) | MSP's own profile; used in PDF templates |\\n\\n**Change detection:** Each entity is hashed; if the hash matches the stored `qb_sync_hash`, the sync is skipped. This prevents unnecessary database writes and keeps the sync log clean.\\n\\n**Sync logging:** Every sync operation creates a `qb_sync_log` entry with status (running \u2192 success/error), record count, and error message.\\n\\n### 4. Push Operations (`push.go`)\\n\\nCreates financial documents in QuickBooks from NexusOS data. All push operations are outbound only\u2014NexusOS never modifies QB data directly.\\n\\n**Operations:**\\n\\n| Operation | Source | QB Entity | Returns |\\n|-----------|--------|-----------|---------|\\n| `PushEstimate()` | Quote | Estimate | QB estimate ID |\\n| `CreateInvoiceFromTime()` | Time entries | Invoice | QB invoice ID |\\n| `CreateInvoiceFromTicket()` | Ticket's time entries | Invoice | QB invoice ID |\\n| `PushInvoiceFromQuote()` | Quote | Invoice | \u2014 (void) |\\n| `CreateInvoiceFromContract()` | Service contract | Invoice | QB invoice ID |\\n\\n**Line item resolution:**\\n- For quotes: Uses `client_qty` (if set) or `quantity`; applies discount_percent; links to QB item if `qb_item_id` is set\\n- For time entries: Converts duration_minutes to hours; uses effective_rate and billable_amount; links to QB item via billing_work_type\\n- For contracts: Single line item with contract value\\n\\n**Customer resolution:** All invoices/estimates require a QB customer ID. If the company isn't linked to a QB customer, the operation fails with a clear error message.\\n\\n### 5. Recurring Billing (`recurring_billing.go`)\\n\\nAutomated scheduler that fires invoices for contracts on their billing dates.\\n\\n**Flow:**\\n1. Runs on a periodic interval (e.g., every 6 hours)\\n2. Queries all active contracts with `next_billing_date &lt;= today`\\n3. For each due contract, calls `CreateInvoiceFromLegalContract()` to create a QB invoice\\n4. On success, advances `next_billing_date` by the billing cycle (monthly or annual)\\n5. Logs the result to `recurring_invoice_runs`\\n\\n**Billing cycles:** `mrr` (monthly) advances 1 month; `yrr` (annual) advances 1 year.\\n\\n### 6. Timesheet Push (`timesheet_push.go`)\\n\\nPushes approved timesheets to QuickBooks as TimeActivity records.\\n\\n**Entry point:** `PushTimesheet(ctx, tenantID, timesheetID)`\\n\\n**Process:**\\n1. Loads the timesheet and the user's `qb_employee_id` (required; fails if not set)\\n2. Queries all unpushed work_time_entries for the timesheet's week\\n3. For each entry:\\n   - Resolves the company's QB customer ID (if the entry is linked to a ticket/project)\\n   - Resolves the QB item ID (if the entry has a billing_work_type)\\n   - Creates a QBTimeActivity with EmployeeRef, CustomerRef, ItemRef, hours, minutes, rate, and billable status\\n   - Stores the returned QB TimeActivity ID on the entry\\n4. Returns a summary of successes and failures (partial failures don't abort the batch)\\n\\n**Billable status:** Set to \\\"Billable\\\" if `is_billable=true` and a rate is available; otherwise \\\"NotBillable\\\".\\n\\n### 7. Purchase Order Adapter (`po_adapter.go`)\\n\\nImplements the `po.QBOCreator` interface for pushing purchase orders to QuickBooks.\\n\\n**Process:**\\n1. Resolves or creates the vendor in QB\\n2. For each line item:\\n   - If `qb_item_id` is set, uses item-based line detail\\n   - Otherwise, looks up or creates the item in QB, then uses item-based detail\\n   - Falls back to account-based detail if item lookup fails\\n3. Creates the PurchaseOrder in QB\\n4. Returns the QB PO ID and doc number\\n\\n**Account resolution:** Uses the first available expense account for account-based lines; uses the first income account when creating new items.\\n\\n### 8. Service (`service.go`)\\n\\nTop-level orchestrator that ties together the OAuth handler, client, syncer, and push operations. Exposes HTTP routes and manages the periodic sync scheduler.\\n\\n**HTTP routes:**\\n- `GET /auth/quickbooks` \u2014 Start OAuth flow\\n- `GET /auth/quickbooks/callback` \u2014 OAuth callback\\n- `POST /api/quickbooks/disconnect` \u2014 Revoke connection\\n- `GET /api/quickbooks/status` \u2014 Connection status\\n- `GET /settings/quickbooks` \u2014 Settings page\\n- `POST /api/quickbooks/credentials` \u2014 Save client_id/secret\\n- `POST /api/quickbooks/sync` \u2014 Manual sync trigger\\n- `GET /api/quickbooks/sync-log` \u2014 Sync log entries\\n- `POST /api/quotes/{id}/push-to-quickbooks` \u2014 Push estimate\\n- `POST /api/invoices/create-from-time` \u2014 Create invoice from time entries\\n- `POST /api/invoices/create-from-contract` \u2014 Create invoice from contract\\n\\n**Scheduler:** `StartScheduler()` runs a background goroutine that syncs all active QB connections on a periodic interval (typically 1 hour).\\n\\n## Data Types (`types.go`)\\n\\nAll QB API types are defined here, modeling the subset of QB fields that NexusOS actually uses. Key types:\\n\\n- **Connection:** OAuth2 link between tenant and QB (realm_id, tokens, expiry)\\n- **QBRef:** Standard QB reference object `{value, name}`\\n- **QBItem, QBCustomer, QBInvoice, QBEstimate, QBPayment:** QB entity models\\n- **QBLine, QBSalesItemDetail:** Line item structures\\n- **QBQueryResponse:** Wrapper for QB query results\\n- **SyncLog:** Audit record for sync operations\\n\\n## Integration Points\\n\\n### Inbound (from other packages)\\n\\n- **`cmd/psa/main.go`:** Initializes the Service and registers routes\\n- **`cmd/qb-push-items/main.go`:** CLI tool that uses the Client directly to push items\\n- **`internal/rmm/device_api.go`:** Calls `nilIfEmpty()` for token handling\\n- **Timesheet package:** Calls `TimesheetPusher.PushTimesheet()` after approval\\n- **Billing/Helpdesk packages:** Call push operations to create invoices\\n\\n### Outbound (to other packages)\\n\\n- **`internal/auth`:** Calls `TenantIDFromContext()` and `UserIDFromContext()` to extract request context\\n- **`internal/ui`:** Calls `Render()` to render the settings page\\n- **Database:** Reads/writes to `qb_connections`, `products`, `companies`, `invoices`, `qb_employees`, `qb_sync_log`, etc.\\n\\n## Error Handling\\n\\n**Token expiration:** Detected proactively (5 minutes before actual expiry) and refreshed automatically. If refresh fails with 400/401, the connection is marked inactive and the user is prompted to reconnect.\\n\\n**Rate limiting:** Blocks and logs a warning; never fails the request.\\n\\n**Sync failures:** Logged per-entity; the sync continues for other entities. Only updates `last_sync_at` if all syncs succeed.\\n\\n**Push failures:** Returned to the caller with a descriptive error message. Partial failures (e.g., some time entries fail) are summarized and returned as a non-nil error.\\n\\n**Missing mappings:** If a company isn't linked to a QB customer, or a user isn't mapped to a QB employee, the operation fails with a clear message directing the user to the settings page.\\n\\n## Configuration\\n\\n**Credentials:** QB client_id and client_secret are stored in `tenants.settings` (JSONB) and must be configured before the OAuth flow can start.\\n\\n**Sandbox mode:** Controlled by the `sandbox` flag passed to `NewService()`. When true, uses the sandbox API endpoint; otherwise uses production.\\n\\n**Redirect URI:** Passed to `NewService()` and used in the OAuth flow. Must match the URI registered in the QB app settings.\\n\\n**Sync interval:** Passed to `StartScheduler()` (typically 1 hour).\\n\\n**Rate limit:** Hardcoded to 400 requests/minute per realm (QB's limit is 500).\\n\\n## Common Workflows\\n\\n### Connect a tenant to QuickBooks\\n\\n1. User navigates to Settings &gt; QuickBooks\\n2. Enters QB client_id and client_secret (saved to `tenants.settings`)\\n3. Clicks \\\"Connect to QuickBooks\\\"\\n4. Redirected to Intuit's auth endpoint\\n5. User authorizes the app\\n6. Callback handler exchanges the code for tokens and stores the connection\\n7. Sync scheduler picks up the new connection and runs the first sync\\n\\n### Push a quote as an estimate\\n\\n1. User clicks \\\"Push to QuickBooks\\\" on an approved quote\\n2. `handlePushQuoteToQB()` calls `PushEstimate()`\\n3. Loads the quote and its line items\\n4. Resolves the company's QB customer ID\\n5. Creates a QBEstimate with line items\\n6. Calls `client.Create()` to POST to `/v3/company/{realm}/estimate`\\n7. Stores the QB estimate ID on the quote\\n8. Returns the estimate ID to the UI\\n\\n### Sync invoices from QuickBooks\\n\\n1. Scheduler calls `SyncAll()` for each active tenant\\n2. `SyncInvoices()` queries QB for invoices (incremental since last sync)\\n3. For each invoice:\\n   - Computes a hash of the invoice JSON\\n   - Checks if the hash matches the stored `qb_sync_hash` (skip if unchanged)\\n   - Resolves the company from the QB customer ref\\n   - Upserts the invoice into the `invoices` table\\n   - Syncs line items into `invoice_line_items`\\n4. Updates `last_sync_at` on the connection\\n5. Logs the sync result to `qb_sync_log`\\n\\n### Create an invoice from billable time entries\\n\\n1. User selects time entries and clicks \\\"Create Invoice\\\"\\n2. `handleCreateInvoiceFromTime()` calls `CreateInvoiceFromTime()`\\n3. Loads the selected time entries with rates and billable amounts\\n4. Resolves the company's QB customer ID\\n5. Builds line items from the time entries (hours, rate, description)\\n6. Creates a QBInvoice and POSTs to QB\\n7. Marks the time entries as invoiced (`qb_invoiced=true`, stores QB invoice ID)\\n8. Immediately syncs invoices back to refresh the local cache\\n9. Returns the QB invoice ID to the UI\\n\\n## Testing &amp; Debugging\\n\\n**Sandbox mode:** Use the sandbox API endpoint for testing without affecting production QB data. Set `sandbox=true` when initializing the Service.\\n\\n**Sync logs:** Check `qb_sync_log` to see the history of sync operations, including errors.\\n\\n**Rate limiting:** Watch the logs for \\\"rate limit reached\\\" messages; if they appear frequently, the sync interval may be too aggressive.\\n\\n**Token refresh:** If a user's QB connection becomes inactive, check the logs for \\\"refresh token revoked\\\" messages. The user must reconnect via the OAuth flow.\\n\\n**Change detection:** If an entity isn't syncing despite changes in QB, check the `qb_sync_hash` in the local table. If it matches the current QB entity hash, the sync is being skipped (expected behavior). Force a re-sync by clearing the hash or deleting the row.\",\"internal-rbac\":\"# internal \u2014 rbac\\n\\n# RBAC Module\\n\\n## Overview\\n\\nThe `internal/rbac` module implements role-based access control (RBAC) for NexusOS PSA. It manages tenant-specific roles and their associated permissions, enabling fine-grained authorization across the platform.\\n\\nThe module operates on a simple permission model: permissions are strings formatted as `\\\"resource:action\\\"` (e.g., `\\\"tickets:read\\\"`, `\\\"agents:deploy\\\"`). Each role is scoped to a single tenant and contains a set of permissions. The wildcard `\\\"*\\\"` grants all permissions for a resource or all resources.\\n\\n## Core Concepts\\n\\n### Roles and Permissions\\n\\nRoles are tenant-isolated entities that bundle related permissions together. When a user is assigned a role, they inherit all permissions associated with that role.\\n\\n**Permission Format:**\\n- `\\\"resource:action\\\"` \u2014 grants a specific action on a resource (e.g., `\\\"tickets:read\\\"`)\\n- `\\\"resource:*\\\"` \u2014 grants all actions on a resource (e.g., `\\\"agents:*\\\"`)\\n- `\\\"*\\\"` \u2014 grants all permissions (admin-level access)\\n\\n### System Roles\\n\\nFour system roles are automatically created for each new tenant:\\n\\n| Role | Purpose | Key Permissions |\\n|------|---------|-----------------|\\n| **admin** | Full platform access | `*` (all permissions) |\\n| **manager** | Operational oversight | Manage agents, tickets, products, compliance; approve timesheets; invite users |\\n| **technician** | Day-to-day operations | Deploy agents, manage tickets, submit timesheets, read knowledge base |\\n| **viewer** | Read-only access | Read agents, tickets, products, compliance, knowledge base, contracts |\\n\\nCustom roles can be created through the settings UI and stored in the database alongside system roles.\\n\\n## API Reference\\n\\n### SeedDefaultRoles\\n\\n```go\\nfunc SeedDefaultRoles(ctx context.Context, pool *pgxpool.Pool, tenantID string) (adminRoleID string, err error)\\n```\\n\\nCreates the four system roles and their permissions for a newly provisioned tenant.\\n\\n**Parameters:**\\n- `ctx` \u2014 context for database operations\\n- `pool` \u2014 PostgreSQL connection pool\\n- `tenantID` \u2014 the tenant being provisioned\\n\\n**Returns:**\\n- `adminRoleID` \u2014 the ID of the created admin role (used to assign to the initial admin user)\\n- `err` \u2014 error if role or permission creation fails\\n\\n**Behavior:**\\n- Inserts each system role into the `roles` table with `is_system = true`\\n- Uses `ON CONFLICT ... DO UPDATE` to handle idempotent re-runs\\n- For each role, inserts its permissions into the `role_permissions` table\\n- Uses `ON CONFLICT ... DO NOTHING` to skip duplicate permissions\\n- Returns the admin role ID for immediate assignment to the tenant's first user\\n\\n**Example:**\\n```go\\nadminRoleID, err := rbac.SeedDefaultRoles(ctx, pool, \\\"tenant-123\\\")\\nif err != nil {\\n    return err\\n}\\n// Use adminRoleID to create the initial admin user\\n```\\n\\n### SystemRoles\\n\\n```go\\nvar SystemRoles = map[string][]string\\n```\\n\\nA package-level map defining the default roles and their permission sets. Used by `SeedDefaultRoles` to populate the database.\\n\\n## Database Schema\\n\\nThe module assumes the following tables exist:\\n\\n**roles**\\n- `id` \u2014 unique role identifier\\n- `tenant_id` \u2014 tenant this role belongs to\\n- `name` \u2014 role name (e.g., \\\"admin\\\", \\\"manager\\\")\\n- `is_system` \u2014 boolean flag indicating if this is a system role\\n- Unique constraint on `(tenant_id, name)`\\n\\n**role_permissions**\\n- `role_id` \u2014 foreign key to `roles.id`\\n- `permission` \u2014 permission string (e.g., \\\"tickets:read\\\")\\n- Unique constraint on `(role_id, permission)`\\n\\n## Integration Points\\n\\n### Tenant Provisioning\\n\\n`SeedDefaultRoles` is called during tenant onboarding to establish the initial role structure. The returned `adminRoleID` is typically assigned to the first user created for that tenant.\\n\\n### User Assignment\\n\\nWhile this module doesn't directly assign roles to users, the roles it creates are referenced by user management code that establishes the `users \u2192 roles` relationship.\\n\\n### Authorization Checks\\n\\nOther modules consume the permissions stored by this module to enforce access control. Permission evaluation typically happens at the API handler or service layer, checking if a user's assigned roles contain the required permission.\\n\\n## Design Notes\\n\\n- **Tenant Isolation:** All roles are scoped to a tenant via `tenant_id`. A user in one tenant cannot inherit permissions from another tenant's roles.\\n- **Idempotency:** Both role and permission inserts use `ON CONFLICT` clauses, making `SeedDefaultRoles` safe to call multiple times.\\n- **System Role Flag:** The `is_system` flag distinguishes built-in roles from custom ones, preventing accidental deletion or modification of core roles.\\n- **Wildcard Permissions:** The `\\\"*\\\"` permission is a convention; enforcement of wildcard matching is handled by authorization logic elsewhere in the codebase.\",\"internal-review\":\"# internal \u2014 review\\n\\n# Live Review Engine (`internal/review`)\\n\\nThe Live Review Engine is a generic, host-driven review framework that powers compliance attestations, employee reviews, training sign-offs, and other multi-party decision workflows. A host (e.g., compliance officer) guides one or more participants (e.g., business unit leads, SMEs) through a queue of items, each requiring a decision, notes, and optionally a signature. The engine handles session state, real-time synchronization via SSE, audit trails, and multi-reviewer domain scoping.\\n\\n## Architecture Overview\\n\\n```mermaid\\ngraph TB\\n    Host[\\\"Host(Browser)\\\"]\\n    Participant[\\\"Participant(iPad/Browser)\\\"]\\n    Handler[\\\"Handler(HTTP Routes)\\\"]\\n    Repo[\\\"Repository(DB Access)\\\"]\\n    Broker[\\\"Broker(In-Process Pub/Sub)\\\"]\\n    \\n    Host --&gt;|\\\"GET /reviews/{id}/host\\\"| Handler\\n    Host --&gt;|\\\"POST /api/reviews/{id}/start\\\"| Handler\\n    Host --&gt;|\\\"GET /api/reviews/{id}/events\\\"| Handler\\n    \\n    Participant --&gt;|\\\"GET /r/{token}\\\"| Handler\\n    Participant --&gt;|\\\"POST /api/r/{token}/respond\\\"| Handler\\n    Participant --&gt;|\\\"GET /api/r/{token}/events\\\"| Handler\\n    \\n    Handler --&gt;|Query/Update| Repo\\n    Handler --&gt;|Publish/Subscribe| Broker\\n    Broker --&gt;|SSE Stream| Host\\n    Broker --&gt;|SSE Stream| Participant\\n```\\n\\nThe engine decouples host control (start, pause, resume, end, navigate) from participant actions (respond, sign). A lightweight in-process broker (`Broker`) fans state changes to all connected clients via Server-Sent Events, keeping the host's monitoring matrix and participant's kiosk in sync without polling.\\n\\n## Core Types\\n\\n### Session\\n\\nA `Session` represents one review run. It tracks:\\n- **Lifecycle**: `draft` \u2192 `live` \u2192 `paused` \u2194 `live` \u2192 `complete`\\n- **Participants**: host (user ID + name) and one or more reviewers (v1 single-participant or v2 multi-reviewer)\\n- **Queue state**: `CurrentItemIdx` (which item is active), `StartedAt`, `EndedAt`\\n- **Signatures**: host and participant blobs + timestamps\\n- **Framework context**: `FrameworkSlug` (e.g., `nist_csf`), `CompanyID`, `ContextID` for domain-specific writeback\\n\\n### Item\\n\\nAn `Item` is one stop in the review queue. Fields include:\\n- **Content**: `Title`, `Body`, `WhyText` (rationale), `Severity`\\n- **Choices**: array of `Choice` objects (value, label, color, icon) presented to the participant\\n- **Response**: `ResponseValue` (ack/decline/modified), `ResponseNotes`, `SignatureBlob`\\n- **Timing**: `StartedAt` (first viewed), `SubmittedAt` (decision recorded), `TimeSeconds` (elapsed)\\n- **Routing**: `DomainCode` (e.g., \\\"GV\\\" for NIST Govern function) for multi-reviewer filtering\\n\\n### Reviewer (v2 Multi-Reviewer)\\n\\nA `Reviewer` is one SME in a multi-reviewer session. Each gets:\\n- **Unique token**: `/r/{ParticipantToken}` URL scoped to their domains\\n- **Domain assignment**: `DomainCodes` array (e.g., `[\\\"GV\\\", \\\"ID\\\"]` for two NIST functions)\\n- **Mode**: `live` (follows host's iPad over SSE) or `async` (self-paced)\\n- **Status**: `pending`, `in_progress`, `signed`, `declined`\\n- **Signature**: per-reviewer blob + hash, separate from session-level signature\\n\\n### BinderData\\n\\nDrives the printable binder (`/api/reviews/{id}/binder.pdf`). Aggregates:\\n- Session metadata, company name, duration\\n- Normalized items with response labels and tone (success/warning/danger/muted)\\n- Multi-reviewer signature blocks (v2) or legacy session-level pair (v1)\\n- Decision counts (accepted, modified, declined, skipped)\\n\\n## Handler Routes\\n\\n### Host Routes\\n\\n| Route | Method | Purpose |\\n|-------|--------|---------|\\n| `/reviews` | GET | List sessions (index page) |\\n| `/reviews/{id}/host` | GET | Host control panel with live queue, monitoring matrix, audit log |\\n| `/api/reviews/{id}/start` | POST | Transition draft\u2192live, start item 1 |\\n| `/api/reviews/{id}/pause` | POST | Pause the session (participants can't respond) |\\n| `/api/reviews/{id}/resume` | POST | Resume from paused |\\n| `/api/reviews/{id}/end` | POST | Mark complete, revoke token, fire completion hooks |\\n| `/api/reviews/{id}/goto` | POST | Jump to item N (host navigation) |\\n| `/api/reviews/{id}/host-sign` | POST | Host signature submission |\\n| `/api/reviews/{id}/qr` | GET | PNG QR code pointing to `/r/{token}` |\\n| `/api/reviews/{id}/binder.pdf` | GET | Printable binder (served as HTML, browser prints to PDF) |\\n| `/api/reviews/{id}/events` | GET | SSE stream of session events |\\n\\nAll host routes require tenant auth via `auth.TenantIDFromContext()`.\\n\\n### Participant Routes\\n\\n| Route | Method | Purpose |\\n|-------|--------|---------|\\n| `/r/{token}` | GET | Kiosk view (current item, progress, completion slide) |\\n| `/api/r/{token}/respond` | POST | Submit decision + notes + optional signature for one item |\\n| `/api/r/{token}/sign-session` | POST | Final session signature (v1) or per-reviewer signature (v2) |\\n| `/api/r/{token}/events` | GET | SSE stream of session events |\\n\\nParticipant routes use token-based auth (no tenant context). Tokens are 32-char hex, generated at session/reviewer creation, revoked on session end.\\n\\n## Request Flow: Participant Response\\n\\n```\\n1. Participant submits decision on item 5:\\n   POST /api/r/{token}/respond\\n   { \\\"idx\\\": 5, \\\"value\\\": \\\"ack\\\", \\\"notes\\\": \\\"...\\\", \\\"signature\\\": \\\"data:image/png;base64,...\\\" }\\n\\n2. Handler validates:\\n   - Token is active (not expired, not revoked, session is live/paused/complete)\\n   - Session is live (not paused)\\n   - Idx, value, notes, signature are within bounds\\n   - Signature is valid data URL (if present)\\n\\n3. Repository saves response:\\n   - SaveItemResponse() updates review_items row\\n   - Computes signature SHA256 hash\\n   - Stamps submitted_at, time_seconds\\n\\n4. Handler advances state:\\n   - If idx == current_item_idx, increment current_item_idx\\n   - MarkItemStarted() on the next item\\n\\n5. Broker publishes events:\\n   - item.confirmed (idx, value, notes_len, has_sig, next)\\n   - item.start (idx) if advancing\\n\\n6. Host and all other participants receive SSE events\\n   - Host's monitoring matrix updates decision counts\\n   - Participant's kiosk advances to next item or shows completion slide\\n```\\n\\n## Real-Time Synchronization (Broker &amp; SSE)\\n\\nThe `Broker` is a lightweight in-process pub/sub keyed by session ID. No Redis, no external dependencies \u2014 suitable for single-binary deployments.\\n\\n**Subscribe/Unsubscribe**: Clients call `broker.Subscribe(sessionID)` to get a buffered channel (16-message buffer). On disconnect, `Unsubscribe()` closes the channel and cleans up the topic if empty.\\n\\n**Publish**: Any handler can call `broker.Publish(sessionID, eventType, payload)` to fan a JSON event to all subscribers. Slow consumers (full channel) are dropped silently \u2014 the SSE stream is best-effort.\\n\\n**SSE Stream** (`streamSSE`):\\n- Sets `Content-Type: text/event-stream`, `Cache-Control: no-store`\\n- Sends initial `event: hello` handshake\\n- Subscribes to the broker\\n- Sends heartbeat pings every 20 seconds (keeps connections alive through proxies)\\n- Hard cap: 2-hour stream lifetime (prevents zombie goroutines)\\n- Exits on client disconnect or timeout\\n\\nEvent types published:\\n- `session.start`, `session.pause`, `session.resume`, `session.end`\\n- `item.start`, `item.confirmed`\\n- `session.host_signed`, `session.participant_signed`\\n\\n## Multi-Reviewer (v2) vs. Single-Participant (v1)\\n\\n**v1 (Single-Participant)**:\\n- One `Session` row with `participant_token`\\n- All items in the queue\\n- Session-level signature blob (host + participant)\\n- Binder shows legacy signature pair\\n\\n**v2 (Multi-Reviewer)**:\\n- One `Session` row (no participant_token, or empty)\\n- Multiple `Reviewer` rows, each with `participant_token` and `domain_codes`\\n- Items stamped with `domain_code` (e.g., \\\"GV\\\", \\\"ID\\\")\\n- Each reviewer's `/r/{token}` URL filters items: `domain_code == \\\"\\\" OR domain_code IN reviewer.domain_codes`\\n- Per-reviewer signature blobs in `review_reviewers` table\\n- Session-level participant signature still populated (for v1 UI compat during rollout)\\n- Binder shows one signature block per reviewer with their domain scope\\n\\n**Token Resolution** (`ResolveActiveByToken`):\\n1. Try v1: `GetActiveSessionByToken(token)` \u2192 returns `(session, nil, nil)`\\n2. On miss, try v2: `GetActiveReviewerByToken(token)` \u2192 returns `(session, reviewer, nil)`\\n3. Both paths return the same generic \\\"not found\\\" error to prevent token enumeration\\n\\n## Security\\n\\n### Token Management\\n\\n- **Generation**: `genToken()` creates 32-char lowercase hex from `crypto/rand`\\n- **Expiry**: Default 24 hours (set at session creation), configurable via `SetTokenExpiry()`\\n- **Revocation**: `RevokeToken()` NULLs the token on session end \u2014 no replay possible\\n- **Validation**: `isToken()` regex enforces format before DB queries\\n\\n### Rate Limiting\\n\\nThree independent rate limiters, all sliding-window in-memory:\\n\\n1. **Participant writes** (`rlParticip`): 120 actions/minute per token\\n   - Covers respond + sign-session\\n   - Prevents a single participant from flooding the queue\\n\\n2. **Host control** (`rlHost`): 120 actions/minute per host user ID\\n   - Covers start, pause, resume, end, goto\\n   - Allows rapid navigation without blocking legitimate power users\\n\\n3. **SSE connections** (`rlStream`): 10 new connections/minute per IP or token\\n   - Drops reconnect storms\\n   - Prevents resource exhaustion from malicious clients\\n\\nRate limiters are fail-closed: if a key exceeds its limit, the request gets HTTP 429. Stale buckets are evicted opportunistically when the map exceeds 4096 keys.\\n\\n### Body Size Limits\\n\\n- **Respond/Sign**: 2 MB max (accommodates ~800x180 PNG signature pad at ~50\u201380 KB base64)\\n- **Control actions**: 64 KB max (start/pause/resume/end/goto payloads are tiny)\\n\\n### Signature Validation\\n\\n`validSignature()` checks:\\n- Non-empty\\n- \u2264 2 MB\\n- Starts with `data:image/png;base64,` or `data:image/jpeg;base64,`\\n- Rejects script-y payloads\\n\\nSignatures are stored as-is (data URL) and hashed with SHA256 for audit/dedup.\\n\\n### Tenant Isolation\\n\\nHost routes extract `TenantID` from context and verify session ownership before any operation. Participant routes use token-based auth (token IS the secret) \u2014 no tenant context needed.\\n\\n## Repository (Data Access)\\n\\nThe `Repository` wraps `pgxpool.Pool` and provides:\\n\\n**Session CRUD**:\\n- `CreateSession()`: Insert draft session, generate token, set 24h expiry\\n- `GetSession()`: Fetch by ID\\n- `GetSessionByToken()`: Fetch by participant token (v1)\\n- `ListSessions()`: For index page, filtered by tenant + optional company\\n\\n**Reviewer CRUD** (v2):\\n- `CreateReviewer()`: Insert reviewer, generate token, set mode/status\\n- `ListReviewers()`: All reviewers for a session\\n- `GetReviewerByToken()`: Fetch by participant token (v2)\\n- `SaveReviewerSignature()`: Store blob + hash, stamp signed_at + status\\n\\n**Item CRUD**:\\n- `AddItem()`: Insert item, auto-increment idx if zero\\n- `ListItems()`: All items for a session, ordered by idx\\n- `GetItem()`: Fetch one item by idx\\n- `SaveItemResponse()`: Update response_value, response_notes, signature_blob, signature_hash, submitted_at, time_seconds\\n- `MarkItemStarted()`: Stamp started_at (idempotent)\\n\\n**Session State**:\\n- `StartSession()`: draft \u2192 live, stamp started_at, set current_item_idx \u2265 1\\n- `PauseSession()`: live \u2192 paused\\n- `ResumeSession()`: paused \u2192 live\\n- `EndSession()`: any \u2192 complete, stamp ended_at\\n- `SetCurrentIdx()`: Update current_item_idx (host navigation)\\n\\n**Signatures**:\\n- `SaveSessionSignature()`: Store host or participant blob, stamp signed_at\\n- `SaveReviewerSignature()`: Store per-reviewer blob, stamp signed_at + status='signed'\\n\\n**Audit**:\\n- `LogEvent()`: Insert review_audit_events row (actor, event_type, payload)\\n- `ListAuditEvents()`: Recent events (newest first), capped\\n\\n**Token Security**:\\n- `GetActiveSessionByToken()`: Fetch session, check token not revoked/expired, status in (draft/live/paused)\\n- `GetActiveReviewerByToken()`: Fetch reviewer + session, same checks\\n- `RevokeToken()`: NULL token + stamp revoked_at\\n- `SetTokenExpiry()`: Update token_expires_at\\n\\n## Framework Integration (Seeder)\\n\\nThe `ActionPlanSeeder` interface lets each compliance framework (NCSR, CMMC, HIPAA, etc.) plug into the review engine:\\n\\n```go\\ntype ActionPlanSeeder interface {\\n    Slug() string\\n    HasActionablePlan(ctx, tenantID, companyID) (bool, string, error)\\n    ResolveContext(ctx, tenantID, companyID) (contextID string, err error)\\n    SeedItems(ctx, sessionID, tenantID, companyID, contextID) error\\n    ApplyDecisions(ctx, sessionID) error\\n    SessionTitle(ctx, tenantID, companyID, contextID) string\\n    RecomputeScore(ctx, contextID) error\\n    Domains() []Domain\\n}\\n```\\n\\n**Lifecycle**:\\n1. **HasActionablePlan**: Gates the \\\"Start Review\\\" CTA on the dashboard. Returns `(true, reason, nil)` if there's something to review.\\n2. **ResolveContext**: Maps company \u2192 framework-specific context ID (e.g., active NCSR assessment ID).\\n3. **SeedItems**: Populates `review_items` by querying framework tables (e.g., ncsr_actions, cmmc_practices).\\n4. **ApplyDecisions**: Fired from the completion hook; fans participant decisions back to source rows (e.g., `ncsr_actions.client_decision`). Must be idempotent.\\n5. **SessionTitle**: Formats human-readable title (e.g., \\\"NIST CSF Review \u2014 Q4 2024\\\").\\n6. **RecomputeScore**: Recomputes framework score after decisions applied. Idempotent.\\n7. **Domains**: Returns reviewable buckets (e.g., NIST CSF functions: GV, ID, PR, DE, RS, RC). Used by multi-reviewer setup UI and item routing.\\n\\nA global `Registry` holds active seeders keyed by slug. Frameworks register at startup in `main.go`. The composer's generic start-review handler dispatches by slug.\\n\\n## Completion Hooks\\n\\nWhen a host calls `POST /api/reviews/{id}/end`, the handler:\\n1. Marks session complete\\n2. Revokes the participant token\\n3. Fires all registered `CompletionHook` callbacks in a fresh goroutine (so writeback completes even if the client disconnects)\\n\\nHooks are registered via `handler.RegisterCompletionHook(fn)`. The NCSR composer registers a hook to apply decisions back to `ncsr_actions` and recompute the assessment score.\\n\\nHooks must be:\\n- **Idempotent**: May fire more than once if the host clicks End twice\\n- **Tolerant of partial state**: Framework data may have changed since the review started\\n- **Fast or async**: Run on the request goroutine; kick off a goroutine for heavy work\\n\\n## Binder (Printable Review Record)\\n\\nThe binder (`/api/reviews/{id}/binder.pdf`) is a print-styled HTML page that the browser renders to PDF via the OS print dialog. No server-side PDF library \u2014 keeps the binary small and lets design iterate without a deploy.\\n\\n**Data aggregation** (`handleBinderPrint`):\\n- Fetches session, items, reviewers\\n- Normalizes item responses (ack \u2192 \\\"Accepted\\\" + success tone, etc.)\\n- Counts decisions (accepted, modified, declined, skipped)\\n- Computes session duration\\n- Looks up company name (best-effort, no fail)\\n- Builds `BinderReviewer` blocks (v2) or falls back to session-level pair (v1)\\n\\n**Caching**: `Cache-Control: no-store` \u2014 the binder reflects live DB state until the session is sealed. Caching would surface stale signatures.\\n\\n**URL**: Keeps `.pdf` suffix for back-compat with existing links, but serves `text/html`. Browsers handle the print-to-PDF flow.\\n\\n## QR Code\\n\\n`handleQR` generates a PNG QR code pointing to `/r/{token}`. Used by the host to display on a screen so the participant can scan with an iPad camera.\\n\\n- **Error correction**: Q-level (~25% recovery) \u2014 robust to camera glare and angle\\n- **Scale**: 8\u00d7 (large enough to scan from a few feet away)\\n- **Library**: `rsc.io/qr` (pure Go, zero deps, Russ Cox / Go core team)\\n- **Caching**: `Cache-Control: no-store, private` \u2014 revoked tokens' QRs must not be cached\\n\\n## Audit Trail\\n\\nEvery state change emits an `AuditEvent` row via `LogEvent()`:\\n- `actor`: \\\"host\\\", \\\"participant\\\", or \\\"system\\\"\\n- `actor_id`: User ID (host) or empty (participant)\\n- `event_type`: \\\"session.start\\\", \\\"item.confirmed\\\", \\\"session.participant_signed\\\", etc.\\n- `payload`: JSON (idx, value, has_notes, has_sig, ip, etc.)\\n- `ts`: Timestamp\\n\\nThe host's control panel displays recent events (newest first, capped at 50). Useful for compliance audits and debugging.\\n\\n## Testing &amp; Extensibility\\n\\nThe module is designed for:\\n- **Framework pluggability**: New compliance frameworks register a `Seeder` implementation\\n- **Multi-reviewer rollout**: v1 and v2 coexist; token resolution handles both\\n- **Horizontal scaling**: Broker is in-process, so multi-instance deployments need Redis or similar (future work)\\n- **Custom completion logic**: Hooks let domain packages react to review completion without importing the review engine\\n\\n## Common Patterns\\n\\n### Checking if an item is done\\n```go\\nif item.SubmittedAt != nil {\\n    // Item has a response\\n}\\n```\\n\\n### Filtering items for a reviewer\\n```go\\nfiltered := FilterItemsForReviewer(items, reviewer.DomainCodes)\\n```\\n\\n### Publishing an event\\n```go\\nh.broker.Publish(sessionID, \\\"item.confirmed\\\", map[string]any{\\n    \\\"idx\\\": 5,\\n    \\\"value\\\": \\\"ack\\\",\\n    \\\"next\\\": 6,\\n})\\n```\\n\\n### Validating a token\\n```go\\nif !isToken(token) {\\n    http.Error(w, \\\"not found\\\", 404)\\n    return\\n}\\n```\\n\\n### Rate limiting\\n```go\\nif !h.rlParticip.Allow(\\\"tok:\\\" + token) {\\n    http.Error(w, \\\"rate limited\\\", 429)\\n    return\\n}\\n```\",\"internal-risk\":\"# internal \u2014 risk\\n\\n# Risk Acceptance Module\\n\\nThe `internal/risk` module implements a generalized **Risk Acceptance** service used across all compliance frameworks (NCSR, CMMC, Composer, vulnerability scans, helpdesk exceptions). It provides a canonical way for MSPs to record that a client has formally accepted a compliance gap or finding, with full lifecycle management, e-signature capture, and audit trails.\\n\\n## Overview\\n\\nRisk acceptances flow through three main phases:\\n\\n1. **MSP records** a pending acceptance (status `pending`)\\n2. **Client countersigns** via the Client Portal (status `accepted`)\\n3. **Lifecycle actions**: renew (extend review date), revoke (pull back), or resolve (gap fixed)\\n\\nThe module is intentionally decoupled from individual compliance frameworks via a **plugin registry** \u2014 each module (NCSR, CMMC, etc.) registers a `SourceResolver` callback so the Risk Register can deep-link and trigger state flips without import cycles.\\n\\n## Core Components\\n\\n### Service (`service.go`)\\n\\nThe `Service` type is the canonical API for all risk acceptance operations. It wraps a database connection pool and exposes:\\n\\n- **`Create(ctx, req)`** \u2014 Records a pending acceptance. If signer fields are present (in-person MSP capture on iPad), the row is created in `accepted` status immediately.\\n- **`Sign(ctx, req)`** \u2014 Records a client e-signature against a pending acceptance, transitioning it to `accepted`.\\n- **`Get(ctx, tenantID, id)`** \u2014 Loads a single acceptance by ID, scoped to tenant.\\n- **`GetBySource(ctx, tenantID, module, sourceID)`** \u2014 Retrieves the active acceptance for a given source (e.g., a CMMC action plan item).\\n- **`List(ctx, tenantID, filter)`** \u2014 Returns acceptances matching a filter (company, module, status, due date, search).\\n- **`Renew(ctx, tenantID, id, newReviewDate, justification, actorUserID)`** \u2014 Extends the review due date and optionally appends justification.\\n- **`Revoke(ctx, tenantID, id, reason, actorUserID)`** \u2014 Marks as revoked and notifies the source module.\\n- **`Resolve(ctx, tenantID, id, note, actorUserID)`** \u2014 Marks as resolved (underlying gap fixed) and notifies the source module.\\n- **`Audit(ctx, tenantID, acceptanceID)`** \u2014 Returns the audit trail (newest first).\\n- **`LogView(ctx, tenantID, acceptanceID, ...)`** \u2014 Records a portal-side \\\"viewed\\\" event.\\n\\nAll operations are tenant-scoped and audit-logged.\\n\\n### Handler (`handler.go`)\\n\\nThe `Handler` type serves the MSP-side Risk Acceptance UI and JSON API. It wraps a `Service`, database pool, and UI renderer.\\n\\n**Routes:**\\n\\n| Method | Path | Purpose |\\n|--------|------|---------|\\n| `GET` | `/risk/register` | Cross-tenant risk register (MSP HQ view) |\\n| `GET` | `/risk/{company_id}/register` | Per-client risk register |\\n| `GET` | `/risk/{company_id}/register/print` | Printable RAS (Risk Acceptance Statement) for one client |\\n| `GET` | `/risk/register/print` | Printable RAS for all clients |\\n| `GET` | `/risk/{company_id}/{module}/{source_id}/modal` | Modal partial to create a new acceptance |\\n| `POST` | `/api/risk/{company_id}/{module}/{source_id}` | Create a new acceptance |\\n| `GET` | `/api/risk/{id}` | Fetch acceptance detail (JSON) |\\n| `POST` | `/api/risk/{id}/renew` | Renew review due date |\\n| `POST` | `/api/risk/{id}/revoke` | Revoke acceptance |\\n| `POST` | `/api/risk/{id}/resolve` | Mark as resolved |\\n\\n**Key handlers:**\\n\\n- **`handleRegister` / `handleRegisterAll`** \u2014 Render the risk register list page. Calls `buildFilter()` to parse query params (module, status, due date, search), then `List()` to fetch items. Enriches labels via `resolveLabel()` and computes summary counts.\\n- **`handleModal`** \u2014 Renders the form partial for creating a new acceptance. Resolves the source label (e.g., \\\"CMMC L2 AC-2.1\\\") and loads company contacts.\\n- **`handleCreate`** \u2014 Parses form data, validates dates, calls `Service.Create()`, and returns JSON or redirects via HTMX.\\n- **`handleDetail`** \u2014 Returns a single acceptance as JSON.\\n- **`handleRenew` / `handleRevoke` / `handleResolve`** \u2014 Lifecycle actions; call the corresponding service methods.\\n- **`handlePrint` / `handlePrintAll`** \u2014 Generate branded, printable Risk Acceptance Statements (HTML).\\n\\n### Types (`types.go`)\\n\\n**`Acceptance`** \u2014 One row in `risk_acceptances`. Fields include:\\n- Source metadata: `SourceModule`, `SourceID`, `SourceRef`, `SourceLabel`\\n- Status and justification: `Status`, `Justification`, `ResidualRisk`, `CompensatingControls`\\n- Signer info: `SignerName`, `SignerEmail`, `SignerTitle`, `SignerIP`, `UserAgent`\\n- Signature data: `InitialData`, `SignatureData` (base64 PNG, never leaked in JSON)\\n- Lifecycle dates: `SignedAt`, `AcceptedAt`, `ReviewDueDate`, `ReviewedAt`, `ExpiresAt`, `RevokedAt`, `ResolvedAt`\\n- Audit: `CreatedBy`, `CreatedAt`, `UpdatedAt`\\n\\n**Status constants:**\\n- `StatusPending` \u2014 MSP recorded, awaiting client signature\\n- `StatusAccepted` \u2014 Client signed\\n- `StatusUnderReview` \u2014 Review due date passed; re-review window open\\n- `StatusExpired` \u2014 Past `ExpiresAt` without renewal\\n- `StatusRevoked` \u2014 Pulled back by MSP/client\\n- `StatusResolved` \u2014 Underlying finding remediated\\n\\n**`CreateRequest`** \u2014 Input for `Service.Create()`. Includes optional in-person signature fields (MSP captures on iPad on client's behalf).\\n\\n**`SignRequest`** \u2014 Input for `Service.Sign()`. Portal POSTs this when client countersigns.\\n\\n**`AuditEntry`** \u2014 One row in `risk_acceptance_audit`. Tracks event, actor type/name/email/IP, and JSON details.\\n\\n**`ListFilter`** \u2014 Narrows a register query by company, module, status, due date, or full-text search.\\n\\n**`SourceResolver`** \u2014 Interface that each module implements:\\n  - `Resolve(tenantID, companyID, sourceID)` \u2192 `(label, url)` \u2014 Returns friendly label and deep link\\n  - `ApplyAcceptance(tenantID, sourceID, acceptanceID)` \u2192 `error` \u2014 Invoked after acceptance created; module flips its own status/badge\\n  - `RevertAcceptance(tenantID, sourceID)` \u2192 `error` \u2014 Invoked on revoke/resolve; module reverts its status\\n\\n### Registry (`registry.go`)\\n\\nA process-global registry of `SourceResolver` callbacks, protected by `sync.RWMutex`. Each module registers at startup (typically in `NewHandler` or `RegisterRoutes`).\\n\\n**Functions:**\\n- **`Register(module, resolver)`** \u2014 Install a resolver for a module\\n- **`ResolverFor(module)`** \u2014 Retrieve the resolver (or nil)\\n- **`resolveLabel(module, tenantID, companyID, sourceID)`** \u2014 Call the resolver's `Resolve()` method\\n- **`applyAcceptance(module, tenantID, sourceID, acceptanceID)`** \u2014 Fan out to module after acceptance created (best-effort)\\n- **`revertAcceptance(module, tenantID, sourceID)`** \u2014 Fan out to module on revoke/resolve (best-effort)\\n- **`Reset()`** \u2014 Clear all resolvers (testing only)\\n\\n### Print (`print.go`)\\n\\n**`RenderRAS(brandedName, scopeLabel, primaryColor, items)`** \u2014 Generates a branded, printable Risk Acceptance binder (HTML). Produces:\\n- A cover page with company name, scope, and item count\\n- One page per acceptance with:\\n  - Item description and reference\\n  - Justification and compensating controls\\n  - Signature and initial pads (rendered from base64 PNG data)\\n  - Metadata (residual risk, review date, expiration)\\n\\nThe output is invoice-style, print-optimized, and uses the company's primary brand color (or defaults to `#1a365d`).\\n\\n## Data Flow\\n\\n```mermaid\\ngraph LR\\n    A[\\\"MSP Portal(handleCreate)\\\"] --&gt;|CreateRequest| B[\\\"Service.Create\\\"]\\n    B --&gt;|INSERT| C[\\\"risk_acceptances(status=pending)\\\"]\\n    B --&gt;|applyAcceptance| D[\\\"SourceResolver(CMMC/NCSR/etc)\\\"]\\n    D --&gt;|flip status| E[\\\"Source Module(e.g. cmmc_actions)\\\"]\\n    \\n    F[\\\"Client Portal(handlePortalRiskSign)\\\"] --&gt;|SignRequest| G[\\\"Service.Sign\\\"]\\n    G --&gt;|UPDATE| C\\n    G --&gt;|applyAcceptance| D\\n    \\n    H[\\\"MSP Portal(handleRenew/Revoke/Resolve)\\\"] --&gt;|action| I[\\\"Service.Renew/Revoke/Resolve\\\"]\\n    I --&gt;|UPDATE| C\\n    I --&gt;|revertAcceptance| D\\n    \\n    C --&gt;|audit| J[\\\"risk_acceptance_audit\\\"]\\n```\\n\\n## Integration Points\\n\\n### Incoming Calls\\n\\nOther modules call the Risk service to create, query, or manage acceptances:\\n\\n- **CMMC** (`internal/cmmc/disposition.go`) \u2014 Creates acceptances for POA&amp;M items; queries via `GetBySource()` to check if a finding is already accepted\\n- **Composer** (`internal/composer/disposition.go`) \u2014 Same pattern for Composer POA&amp;M\\n- **Portal** (`internal/portal/risk_acceptances.go`) \u2014 Client-side handlers for listing, viewing, and signing acceptances\\n- **Main** (`cmd/psa/main.go`) \u2014 Initializes the handler and registers routes\\n\\n### Outgoing Calls\\n\\nThe Risk module calls:\\n\\n- **Auth** (`internal/auth/middleware.go`) \u2014 `TenantIDFromContext()`, `UserIDFromContext()` for request scoping\\n- **UI** (`internal/ui/render.go`) \u2014 `Renderer.Render()` for HTML templates\\n- **Database** (`pgxpool.Pool`) \u2014 Direct SQL for all CRUD operations\\n\\n### Module Registration\\n\\nEach module registers its `SourceResolver` at startup. For example, CMMC might do:\\n\\n```go\\nfunc init() {\\n    risk.Register(risk.ModuleCMMC, &amp;cmmc.RiskResolver{})\\n}\\n```\\n\\nThen when the Risk Register renders, it calls `ResolverFor(\\\"cmmc\\\")` to get the label and deep link for each CMMC action.\\n\\n## Key Behaviors\\n\\n### In-Person Signature (MSP on iPad)\\n\\nWhen an MSP captures a signature on-site on behalf of the client:\\n1. `handleCreate` receives `signer_name`, `signature_data`, `initial_data`\\n2. `Service.Create()` detects these fields and sets `status = \\\"accepted\\\"` immediately (no pending phase)\\n3. Audit event is logged as `\\\"signed_in_person\\\"`\\n\\n### Client Portal Signature\\n\\nWhen a client signs via the portal:\\n1. Portal calls `Service.Sign()` with `SignRequest`\\n2. Service updates the row: `status = \\\"accepted\\\"`, stores signature/initial data, sets `signed_at` and `accepted_at`\\n3. Service calls `applyAcceptance()` to notify the source module\\n4. Audit event is logged as `\\\"signed\\\"` with actor type `\\\"client\\\"`\\n\\n### Renewal\\n\\nWhen an acceptance's review due date passes:\\n1. MSP can call `Service.Renew()` to extend the date\\n2. If status was `\\\"under_review\\\"`, it transitions back to `\\\"accepted\\\"`\\n3. Justification is appended with a timestamp: `\\\"[Renewed 2024-01-15]: ...\\\"`\\n\\n### Revoke / Resolve\\n\\nBoth operations:\\n1. Update the acceptance row (set status, timestamp, reason/note)\\n2. Call `revertAcceptance()` to notify the source module\\n3. The source module reverts its own status (e.g., CMMC action back to `\\\"open\\\"`)\\n\\n## Database Schema\\n\\nTwo tables (migration 185):\\n\\n**`risk_acceptances`**\\n- `id` (UUID, PK)\\n- `tenant_id`, `company_id` (scoping)\\n- `source_module`, `source_id`, `source_ref`, `source_label` (what is being accepted)\\n- `status` (enum: pending, accepted, under_review, expired, revoked, resolved)\\n- `justification`, `residual_risk`, `compensating_controls` (risk details)\\n- `signer_name`, `signer_email`, `signer_title`, `signer_ip`, `user_agent` (who signed)\\n- `initial_data`, `signature_data` (base64 PNG)\\n- `signed_at`, `accepted_at`, `review_due_date`, `reviewed_at`, `expires_at` (dates)\\n- `revoked_at`, `revoked_reason`, `resolved_at`, `resolution_note` (lifecycle)\\n- `created_by`, `created_at`, `updated_at` (audit)\\n\\n**`risk_acceptance_audit`**\\n- `id` (UUID, PK)\\n- `acceptance_id` (FK)\\n- `event` (string: created, signed, signed_in_person, renewed, revoked, resolved, viewed)\\n- `actor_type` (msp_user, client)\\n- `actor_name`, `actor_email`, `actor_ip` (who did it)\\n- `details` (JSONB: context-specific metadata)\\n- `created_at`\\n\\n## Error Handling\\n\\n- **`ErrNotFound`** \u2014 Returned by `Get()`, `GetBySource()`, `Renew()`, `Revoke()`, `Resolve()` when the acceptance doesn't exist or is not in a valid state\\n- **Validation errors** \u2014 `Create()` requires tenant, company, module, source, and justification; returns formatted error messages\\n- **Audit failures** \u2014 Never block business logic; logged to stderr via pgx logger\\n- **Module callback failures** \u2014 `applyAcceptance()` and `revertAcceptance()` are best-effort; errors are logged but do not fail the acceptance operation\\n\\n## Testing\\n\\nThe registry provides a `Reset()` function to clear all resolvers between tests. Callers should:\\n\\n```go\\ndefer risk.Reset()\\nrisk.Register(\\\"test_module\\\", &amp;mockResolver{})\\n```\\n\\n## Common Patterns\\n\\n### Querying Active Acceptances\\n\\n```go\\nitems, err := svc.List(ctx, tenantID, risk.ListFilter{\\n    CompanyID: companyID,\\n    Statuses: []string{risk.StatusPending, risk.StatusAccepted},\\n})\\n```\\n\\n### Checking if a Source is Already Accepted\\n\\n```go\\na, err := svc.GetBySource(ctx, tenantID, \\\"cmmc\\\", actionID)\\nif err == nil {\\n    // Already accepted; can skip or show badge\\n}\\n```\\n\\n### Creating an Acceptance from Another Module\\n\\n```go\\na, err := svc.Create(ctx, risk.CreateRequest{\\n    TenantID:      tenantID,\\n    CompanyID:     companyID,\\n    SourceModule:  \\\"cmmc\\\",\\n    SourceID:      actionID,\\n    SourceRef:     \\\"AC-2.1\\\",\\n    Justification: \\\"Compensating control in place\\\",\\n    ReviewDueDate: time.Now().AddDate(0, 6, 0),\\n    CreatedByUserID: userID,\\n})\\n```\",\"internal-sentinel\":\"# internal \u2014 sentinel\\n\\n# Sentinel Module\\n\\nThe sentinel module provides cryptographic capability tokens for SOCKS stream authorization and shared types for device discovery. It implements a symmetric JWT signing scheme where the platform holds a master encryption key and per-agent HMAC keys are stored encrypted in the database.\\n\\n## Overview\\n\\nSentinel serves two distinct purposes:\\n\\n1. **Capability Token Signing** \u2014 Issues short-lived JWTs (HS256) that authorize specific SOCKS streams, scoped to stream ID, host, port, agent, and CIDR allowlist. The agent verifies these tokens before establishing connections.\\n\\n2. **Device Discovery Types** \u2014 Provides cross-package data structures for scan results, allowing orchestrators (Metasploit harvester, etc.) to hand discovered hosts and ports to the RMM device-table writer without circular imports.\\n\\n## Capability Token Architecture\\n\\n### Key Hierarchy\\n\\nThe system uses a two-tier key structure:\\n\\n- **Platform Key** \u2014 A single AES-256-GCM key held by the Signer, used to decrypt per-agent HMAC keys at signing time.\\n- **Per-Agent Keys** \u2014 256-bit HMAC keys generated at agent enrollment (migration 183+), stored encrypted in the `agents.sentinel_hmac_key_enc` column.\\n\\nThis design limits blast radius: a compromised endpoint exposes only its own HMAC key, not the platform key or other agents' keys.\\n\\n### Why HMAC\\n\\nThe WebSocket delivering tokens is already mTLS-bound to PSA. Asymmetric verification adds little security benefit while increasing complexity. HMAC keeps the surface minimal and ensures both platform and agent use identical verification logic.\\n\\n### Token Lifecycle\\n\\n```\\nAgent Enrollment\\n    \u2193\\nGenerateAgentHMACKey() \u2192 32 random bytes\\n    \u2193\\nEncrypt(platformKey, keyBytes) \u2192 base64 ciphertext\\n    \u2193\\nStore in agents.sentinel_hmac_key_enc\\n    \u2193\\n    \u2193\\nSOCKS Stream Request\\n    \u2193\\nSignForAgent(agentDBID, claims, ttl)\\n    \u2193\\nLookup agents.sentinel_hmac_key_enc\\n    \u2193\\nDecrypt(platformKey, ciphertext) \u2192 per-agent HMAC key\\n    \u2193\\nSign JWT with HS256 (SHA256 + HMAC)\\n    \u2193\\nReturn header.payload.signature\\n    \u2193\\n    \u2193\\nAgent Receives Token\\n    \u2193\\nAgent re-derives signature with its stored key\\n    \u2193\\nVerify before net.Dial\\n```\\n\\n## API Reference\\n\\n### Signer\\n\\n```go\\ntype Signer struct {\\n    platformKey []byte\\n    iss         string\\n}\\n```\\n\\nThe Signer holds the platform encryption key and issues capability tokens. It does not hold signing keys directly; those are looked up per-call.\\n\\n#### NewSigner\\n\\n```go\\nfunc NewSigner(platformKey []byte, issuer string) *Signer\\n```\\n\\nCreates a new Signer. An empty `platformKey` leaves it unconfigured; `Configured()` will return false and `SignForAgent` will return `ErrNoKey`.\\n\\n#### Configured\\n\\n```go\\nfunc (s *Signer) Configured() bool\\n```\\n\\nReports whether the signer has a usable 32-byte platform key. A configured signer can still fail per-call if the target agent has no per-agent key in the database.\\n\\n#### PlatformKey\\n\\n```go\\nfunc (s *Signer) PlatformKey() []byte\\n```\\n\\nExposes the AES-256-GCM key for callers that need to encrypt new per-agent secrets at enrollment time. Returns nil if unconfigured.\\n\\n#### SignForAgent\\n\\n```go\\nfunc (s *Signer) SignForAgent(\\n    ctx context.Context,\\n    pool *pgxpool.Pool,\\n    agentDBID string,\\n    c Claims,\\n    ttl time.Duration,\\n) (string, error)\\n```\\n\\nIssues a capability token for the given agent. The flow:\\n\\n1. Validates signer is configured and inputs are non-empty.\\n2. Queries `agents.sentinel_hmac_key_enc` for the agent's encrypted HMAC key.\\n3. Decrypts the key using the platform key.\\n4. Constructs a JWT with the provided claims, setting `iss` and `exp` fields.\\n5. Signs with HS256 using the decrypted per-agent key.\\n6. Returns the complete JWT (`header.payload.signature`).\\n\\n**Errors:**\\n- `ErrNoKey` \u2014 signer unconfigured or pool is nil.\\n- `ErrAgentKeyMissing` \u2014 agent has no `sentinel_hmac_key_enc` (predates migration 183 or enrollment failed).\\n- Database or cryptographic errors wrapped with context.\\n\\n### Claims\\n\\n```go\\ntype Claims struct {\\n    StreamID string   `json:\\\"stream_id\\\"`\\n    Host     string   `json:\\\"host\\\"`\\n    Port     int      `json:\\\"port\\\"`\\n    CIDRs    []string `json:\\\"cidrs\\\"`\\n    AgentID  string   `json:\\\"agent_id\\\"`\\n    Issuer   string   `json:\\\"iss,omitempty\\\"`\\n    Expires  int64    `json:\\\"exp\\\"`\\n}\\n```\\n\\nByte-compatible with the agent's `auth.Claims`. The platform sets `Issuer` and `Expires` at signing time; the caller provides the rest.\\n\\n## Encryption &amp; Decryption\\n\\n### Encrypt\\n\\n```go\\nfunc Encrypt(platformKey, plaintext []byte) (string, error)\\n```\\n\\nEncrypts plaintext with AES-256-GCM using the platform key. Returns base64-encoded ciphertext with the nonce prepended. The `platformKey` must be exactly 32 bytes.\\n\\nUsed at:\\n- Agent enrollment to encrypt the newly-generated per-agent HMAC key.\\n- MCP server credential storage.\\n\\n### Decrypt\\n\\n```go\\nfunc Decrypt(platformKey []byte, b64 string) ([]byte, error)\\n```\\n\\nReverses `Encrypt`. Extracts the nonce, decrypts the body, and returns plaintext. Returns `ErrCipher` on any tampering, key mismatch, or malformed input.\\n\\nCallers should treat `ErrCipher` as a hard failure (refuse the operation, don't fall back).\\n\\n### GenerateAgentHMACKey\\n\\n```go\\nfunc GenerateAgentHMACKey() ([]byte, error)\\n```\\n\\nGenerates 32 bytes of cryptographically-secure randomness for use as an HS256 signing key. The caller base64-encodes the result for transport in JSON.\\n\\n## Device Discovery Types\\n\\n### DiscoveredDevice\\n\\n```go\\ntype DiscoveredDevice struct {\\n    IP        string\\n    MAC       string\\n    Hostname  string\\n    OpenPorts []DiscoveredPort\\n}\\n```\\n\\nOne host produced by a Sentinel-routed scan. `MAC` and `Hostname` are best-effort and may be empty.\\n\\n### DiscoveredPort\\n\\n```go\\ntype DiscoveredPort struct {\\n    Port   int\\n    Proto  string\\n    Banner string\\n}\\n```\\n\\nOne open service on a discovered device. `Proto` is \\\"tcp\\\" or \\\"udp\\\" (default tcp); `Banner` is optional service info.\\n\\nThese types live in the sentinel package to avoid circular imports between scan orchestrators (e.g., Metasploit harvester) and the RMM device-table writer.\\n\\n## Integration Points\\n\\n### Agent Enrollment\\n\\nWhen an agent enrolls (`internal/rmm/enroll.go`):\\n1. `GenerateAgentHMACKey()` creates a new 32-byte key.\\n2. `Encrypt()` encrypts it with the platform key.\\n3. The ciphertext is stored in `agents.sentinel_hmac_key_enc`.\\n\\n### SOCKS Stream Authorization\\n\\nWhen opening a SOCKS stream (`internal/rmm/sentinel_socks_hub.go`):\\n1. `Configured()` checks if the signer is ready.\\n2. `SignForAgent()` issues a capability token scoped to the stream.\\n3. The token is delivered to the agent over mTLS.\\n4. The agent verifies the signature before calling `net.Dial`.\\n\\n### MCP Credential Storage\\n\\nMCP server credentials are encrypted with `Encrypt()` and decrypted with `Decrypt()` at various points:\\n- `handleCreateMCPServer`, `handleUpdateMCPServer` \u2014 store credentials.\\n- `ensureAccessToken`, `authorizationHeader`, `persistTokens` \u2014 retrieve and use credentials.\\n\\n### Device Discovery\\n\\nScan orchestrators populate `DiscoveredDevice` and `DiscoveredPort` structures, which are passed to the RMM device-table writer without requiring the orchestrator to import RMM internals.\\n\\n## Error Handling\\n\\n- **`ErrNoKey`** \u2014 Signer or per-agent key missing. The hub treats this as a hard failure (refuse to open any stream).\\n- **`ErrAgentKeyMissing`** \u2014 Agent row has no `sentinel_hmac_key_enc`. Caller surfaces this as a refusal so the operator notices and re-enrolls the agent.\\n- **`ErrCipher`** \u2014 Decrypt failed (bad base64, short ciphertext, failed GCM auth). Treat as a hard failure; do not fall back.\\n\\n## Configuration\\n\\nThe signer is initialized in `cmd/psa/main.go` with the platform key from configuration. If the key is missing or wrong length, `Configured()` returns false and all signing operations fail with `ErrNoKey`.\",\"internal-settings\":\"# internal \u2014 settings\\n\\n# Settings Module\\n\\nThe settings module manages user preferences and UI configuration, persisting them to the database and serving them via HTTP endpoints. It handles three distinct preference categories: appearance (theme and visual styling), tab ordering (per-page tab sequence), and tab visibility (which tabs are hidden per page).\\n\\n## Overview\\n\\nUser preferences are stored in the `users.ui_preferences` JSONB column, which acts as a flexible container for multiple preference types. The module provides three handler types that read from and write to this column, each managing a separate concern:\\n\\n- **Appearance**: Theme selection, accent colors, surface styles, density, and other visual properties\\n- **Tab Order**: Per-scope ordering of tabs (e.g., which tabs appear first on a client detail page)\\n- **Tab Visibility**: Per-scope lists of hidden tab IDs (stored as a hidden set; missing tabs default to visible)\\n\\nAll handlers require authentication via context-injected user and tenant IDs. They use PostgreSQL's JSONB operators to preserve unrelated preference keys across writes.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    A[\\\"AppearanceHandler\\\"]\\n    T[\\\"TabOrderHandler\\\"]\\n    V[\\\"TabVisibilityHandler\\\"]\\n    DB[\\\"users.ui_preferences(JSONB)\\\"]\\n    \\n    A --&gt;|GET /settings/appearance| Render[\\\"Render page\\\"]\\n    A --&gt;|POST /api/user/ui-preferences| DB\\n    A --&gt;|GET /api/user/ui-preferences| DB\\n    \\n    T --&gt;|GET /api/me/tab-order| DB\\n    T --&gt;|PUT /api/me/tab-order| DB\\n    \\n    V --&gt;|GET /api/me/tab-visibility| DB\\n    V --&gt;|PUT /api/me/tab-visibility| DB\\n```\\n\\n## AppearanceHandler\\n\\nManages theme and visual styling preferences. Serves both a full-page settings UI and JSON APIs for programmatic access.\\n\\n### Routes\\n\\n- `GET /settings/appearance` \u2014 Renders the appearance settings page with current preferences\\n- `POST /api/user/ui-preferences` \u2014 Saves appearance preferences and sets a cookie for immediate theme pickup\\n- `GET /api/user/ui-preferences` \u2014 Returns current preferences as JSON\\n\\n### Key Methods\\n\\n**`loadPrefs(r *http.Request) ui.UIPreferences`**\\n\\nLoads the user's appearance preferences from the database, applying defaults and migrations:\\n\\n- Queries `users.ui_preferences` for the authenticated user\\n- Falls back to `ui.DefaultUIPreferences()` if the column is empty, missing, or unparseable\\n- Coerces legacy theme values (e.g., \\\"cyberpunk\\\", \\\"steampunk\\\") to \\\"dark\\\"; only \\\"light\\\" is preserved as-is\\n- Fills empty fields (surface, corner, button, etc.) with defaults\\n- Returns a fully-populated `ui.UIPreferences` struct\\n\\n**`HandleGet(w http.ResponseWriter, r *http.Request)`**\\n\\nRenders the appearance settings page. Loads preferences via `loadPrefs`, passes them to the template, and renders `settings/appearance.html`.\\n\\n**`HandleSave(w http.ResponseWriter, r *http.Request)`**\\n\\nPersists appearance preferences to the database:\\n\\n1. Reads and unmarshals the JSON request body into `ui.UIPreferences`\\n2. Coerces the theme to \\\"dark\\\" if it's not \\\"light\\\"\\n3. Uses `JSONB || merge` to update only appearance keys while preserving other preference data (tab_order, tab_visibility, etc.)\\n4. Also updates the `ui_theme` column for quick theme lookups\\n5. Sets a `ui_theme` cookie (secure, HttpOnly=false, 1-year expiry) so the browser picks up the theme immediately without re-login\\n6. Returns `{\\\"ok\\\":true}` on success\\n\\nThe JSONB merge is critical: it ensures that when a user changes their theme, any tab orderings or visibility settings they've configured are not wiped out.\\n\\n**`HandleGetPrefs(w http.ResponseWriter, r *http.Request)`**\\n\\nReturns the current user's preferences as JSON. Used by client-side code to fetch preferences without rendering the full page.\\n\\n## TabOrderHandler\\n\\nPersists the order in which tabs appear on a given page scope. Tabs are identified by stable string keys assigned in templates (e.g., \\\"overview\\\", \\\"contacts\\\", \\\"epa\\\").\\n\\n### Data Shape\\n\\n```json\\n{\\n  \\\"tab_order\\\": {\\n    \\\"client_detail\\\": [\\\"overview\\\", \\\"contacts\\\", \\\"epa\\\"],\\n    \\\"agent_detail\\\": [\\\"summary\\\", \\\"performance\\\"]\\n  }\\n}\\n```\\n\\n### Routes\\n\\n- `GET /api/me/tab-order` \u2014 Returns the entire tab_order map (or `{}` if none exists)\\n- `PUT /api/me/tab-order` \u2014 Saves the order for a single scope\\n\\n### Key Methods\\n\\n**`handleGet(w http.ResponseWriter, r *http.Request)`**\\n\\nQueries `ui_preferences -&gt; 'tab_order'` and returns it as JSON. Returns an empty object if the key doesn't exist.\\n\\n**`handleSave(w http.ResponseWriter, r *http.Request)`**\\n\\nExpects a JSON body:\\n\\n```json\\n{\\n  \\\"scope\\\": \\\"client_detail\\\",\\n  \\\"order\\\": [\\\"overview\\\", \\\"contacts\\\", \\\"epa\\\"]\\n}\\n```\\n\\nValidates both scope and tab IDs against `safeKey` (alphanumeric, underscore, hyphen; 1\u201364 chars) to prevent control characters or Unicode injection into the JSONB blob. Uses `jsonb_set` with `create_if_missing=true` to update only the target scope's key, preserving all other preference data.\\n\\n## TabVisibilityHandler\\n\\nPersists which tabs are hidden for a given page scope. Stores the hidden set (not the visible set) so newly-added tabs default to visible without requiring user preference migrations.\\n\\n### Data Shape\\n\\n```json\\n{\\n  \\\"tab_visibility\\\": {\\n    \\\"client_detail\\\": [\\\"epa\\\", \\\"compliance\\\"],\\n    \\\"agent_detail\\\": []\\n  }\\n}\\n```\\n\\nAn empty or missing list means all tabs are visible.\\n\\n### Routes\\n\\n- `GET /api/me/tab-visibility` \u2014 Returns the entire tab_visibility map (or `{}` if none exists)\\n- `PUT /api/me/tab-visibility` \u2014 Saves the hidden list for a single scope\\n\\n### Key Methods\\n\\n**`handleGet(w http.ResponseWriter, r *http.Request)`**\\n\\nQueries `ui_preferences -&gt; 'tab_visibility'` and returns it as JSON. Returns an empty object if the key doesn't exist.\\n\\n**`handleSave(w http.ResponseWriter, r *http.Request)`**\\n\\nExpects a JSON body:\\n\\n```json\\n{\\n  \\\"scope\\\": \\\"client_detail\\\",\\n  \\\"hidden\\\": [\\\"epa\\\", \\\"compliance\\\"]\\n}\\n```\\n\\nValidates scope and tab IDs against `safeKey`. Uses `jsonb_set` to update only the target scope's hidden list, preserving all other preference data.\\n\\n## Database Schema\\n\\nAll handlers interact with the `users` table:\\n\\n- `ui_preferences` (JSONB) \u2014 Flexible container for all user preferences\\n- `ui_theme` (text, optional) \u2014 Denormalized theme value for quick lookups\\n\\nThe JSONB structure is:\\n\\n```json\\n{\\n  \\\"theme\\\": \\\"light\\\" | \\\"dark\\\",\\n  \\\"surface\\\": \\\"...\\\",\\n  \\\"corner\\\": \\\"...\\\",\\n  \\\"button\\\": \\\"...\\\",\\n  \\\"badge\\\": \\\"...\\\",\\n  \\\"progress\\\": \\\"...\\\",\\n  \\\"density\\\": \\\"...\\\",\\n  \\\"gradient\\\": \\\"...\\\",\\n  \\\"accent\\\": { \\\"hex\\\": \\\"#...\\\" },\\n  \\\"tab_order\\\": { \\\"\\\": [...] },\\n  \\\"tab_visibility\\\": { \\\"\\\": [...] }\\n}\\n```\\n\\n## Authentication &amp; Authorization\\n\\nAll handlers extract user and tenant IDs from the request context using `auth.UserIDFromContext()` and `auth.TenantIDFromContext()`. Requests without valid IDs are rejected with a 401 Unauthorized response. The database queries include both `id` and `tenant_id` in the WHERE clause to enforce tenant isolation.\\n\\n## Input Validation\\n\\n- **Appearance**: JSON unmarshaling validates the structure; theme values are coerced to \\\"light\\\" or \\\"dark\\\"\\n- **Tab Order &amp; Visibility**: Scope and tab ID strings are validated against `safeKey` regex (`^[a-zA-Z0-9_\\\\-]{1,64}$`) to prevent injection of control characters or excessive Unicode into the JSONB blob\\n\\n## Error Handling\\n\\n- Missing or invalid authentication \u2192 401 Unauthorized\\n- Malformed JSON bodies \u2192 400 Bad Request\\n- Invalid scope or tab IDs \u2192 400 Bad Request\\n- Database errors \u2192 500 Internal Server Error\\n- Template rendering errors (appearance page only) \u2192 404 Not Found\\n\\nErrors are logged but not exposed in detail to the client.\\n\\n## Integration Points\\n\\n- **`internal/auth`**: Extracts user and tenant IDs from request context\\n- **`internal/ui`**: Provides `UIPreferences` struct, `DefaultUIPreferences()`, and `Renderer` for template rendering\\n- **`github.com/jackc/pgx/v5/pgxpool`**: Database connection pool for all queries and updates\\n\\n## JSONB Merge Strategy\\n\\nThe module uses PostgreSQL's JSONB operators to safely update preferences without losing unrelated data:\\n\\n- **Appearance save**: `ui_preferences || $1::jsonb` \u2014 Merges the marshaled appearance fields into the existing JSONB, overwriting only appearance keys\\n- **Tab order/visibility save**: `jsonb_set(..., ARRAY['tab_order', $1], $2, true)` \u2014 Updates a nested key, creating it if missing, while preserving the outer object\\n\\nThis design allows independent preference handlers to coexist without conflicts.\",\"internal-siem\":\"# internal \u2014 siem\\n\\n# SIEM Module Documentation\\n\\n## Overview\\n\\nThe **internal/siem** module provides a comprehensive Security Information and Event Management (SIEM) system for NexusOS. It ingests syslog events from network devices (particularly FortiGate firewalls), parses PCAP network captures, and uses Nexie AI to analyze traffic patterns and generate security findings. The module also supports remediation planning, posture tracking, and configurable log retention.\\n\\nKey capabilities:\\n- **Syslog ingestion** \u2014 UDP/TCP listeners with TLS support and multi-session routing\\n- **PCAP analysis** \u2014 Parses network captures using tshark to extract connections, DNS, HTTP, and TLS metadata\\n- **AI-driven analysis** \u2014 Sends traffic summaries to Nexie for risk scoring and finding generation\\n- **Remediation workflows** \u2014 Generates and tracks remediation plans with audit trails and posture snapshots\\n- **Log retention** \u2014 Score-based purging of events with configurable tiers per tenant\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    A[\\\"Syslog Sources(FortiGate, etc)\\\"]\\n    B[\\\"SyslogListener(UDP/TCP/TLS)\\\"]\\n    C[\\\"Event Parser(FortiGate/BSD)\\\"]\\n    D[\\\"siem_eventsDatabase\\\"]\\n    E[\\\"PCAP Upload\\\"]\\n    F[\\\"PcapParser(tshark)\\\"]\\n    G[\\\"siem_capturesDatabase\\\"]\\n    H[\\\"Nexie AIAnalyzeTraffic\\\"]\\n    I[\\\"siem_analysesDatabase\\\"]\\n    J[\\\"RemediationEngine\\\"]\\n    K[\\\"nexie_tasksDatabase\\\"]\\n    \\n    A --&gt;|raw syslog| B\\n    B --&gt;|parsed events| C\\n    C --&gt;|store| D\\n    E --&gt;|pcap file| F\\n    F --&gt;|connections| G\\n    G --&gt;|traffic summary| H\\n    H --&gt;|findings| I\\n    I --&gt;|plan generation| J\\n    J --&gt;|track| K\\n```\\n\\n## Core Components\\n\\n### Handler (`handler.go`)\\n\\nThe HTTP request handler for all SIEM endpoints. Manages:\\n\\n- **Dashboard** (`handleDashboard`) \u2014 Aggregates event counts, sources, captures, sessions, and analyses for the UI\\n- **Event feed** (`handleEventFeed`, `handleEventStats`) \u2014 Queries and filters syslog events by severity, type, source IP, or session\\n- **Source management** (`handleSources`, `handleAddSource`, `handleDeleteSource`) \u2014 CRUD for syslog sources\\n- **Capture lifecycle** (`handleUploadCapture`, `handleCaptures`, `handleCaptureDetail`, `handleDeleteCapture`) \u2014 File upload, parsing, and deletion\\n- **AI analysis** (`handleAnalyzeCapture`, `handleAnalysisDetail`) \u2014 Triggers Nexie analysis and retrieves results\\n- **Syslog listener control** (`handleSyslogStart`, `handleSyslogStop`, `handleSyslogStatus`) \u2014 Starts/stops the listener and manages capture sessions\\n- **Capture sessions** (`handleSessions`, `handleSessionStop`, `handleDeleteSession`, `handleAnalyzeSession`) \u2014 Multi-session event collection with export\\n- **Log retention** (`handleGetRetention`, `handleSaveRetention`, `StartLogRetention`) \u2014 Configurable score-based event purging\\n- **Remediation** (`handleGenerateRemediation`, `handleBatchRemediation`, `handleRemediationStatus`, `handleRemediationAudit`, `handlePostureComparison`) \u2014 Plan generation, approval tracking, and posture snapshots\\n\\n**Key fields:**\\n- `pool` \u2014 PostgreSQL connection pool for all database operations\\n- `renderer` \u2014 UI template renderer\\n- `parser` \u2014 PCAP parser instance\\n- `ai` \u2014 Nexie AI interface (set via `SetAI()`)\\n- `encKey` \u2014 AES-256-GCM key for encrypting session exports (32 bytes)\\n- `listener` \u2014 Syslog listener instance (set via `SetListener()`)\\n- `uploadDir` \u2014 Directory for storing uploaded PCAP files and exported sessions\\n\\n### SyslogListener (`syslog.go`)\\n\\nReceives and parses syslog messages on UDP and TCP (with optional TLS). Supports:\\n\\n- **Multi-protocol** \u2014 UDP on port 5514, TCP on 5514, and optional TLS on 6514 with auto-detection\\n- **Format auto-detection** \u2014 Recognizes FortiGate key=value format, BSD RFC 3164, and generic syslog\\n- **Multi-session routing** \u2014 Maps source IPs to capture sessions; wildcard \\\"*\\\" captures all traffic\\n- **TLS support** \u2014 RFC 5425 encrypted syslog with configurable certificates\\n- **UTF-8 sanitization** \u2014 Strips null bytes and replaces invalid UTF-8 sequences\\n\\n**Key methods:**\\n- `Start()` \u2014 Begins listening on UDP and TCP\\n- `Stop()` \u2014 Gracefully shuts down all listeners\\n- `RegisterSession(sessionID, sourceIPs)` \u2014 Maps a capture session to source IPs\\n- `UnregisterSession(sessionID)` \u2014 Removes a session from routing\\n- `SetTLS(certPath, keyPath)` \u2014 Configures TLS from filesystem\\n- `SetTLSFromStore(certPEM, keyPEM)` \u2014 Configures TLS from PEM bytes\\n- `IsRunning()`, `TLSEnabled()`, `HasActiveSessions()` \u2014 Status queries\\n\\n**Event flow:**\\n1. `readUDP()` / `acceptTCP()` receive raw messages\\n2. `handleTCPConn()` / `handle6514Conn()` parse TCP streams (RFC 5425 octet-counting or newline-delimited)\\n3. `processMessage()` auto-detects format and parses\\n4. `storeEvent()` inserts into `siem_events` and routes to active sessions\\n5. Critical/high events are promoted to `security_alerts`\\n\\n### PcapParser (`pcap.go`)\\n\\nParses PCAP files using tshark (Wireshark CLI) to extract structured connection data.\\n\\n**Key methods:**\\n- `ParseCapture(ctx, capture)` \u2014 Main entry point; orchestrates all extraction steps\\n  1. Calls `getCaptureStats()` for packet count and duration\\n  2. Calls `extractConnections()` for TCP/UDP conversations\\n  3. Calls `extractDNS()`, `extractHTTP()`, `extractTLS()` for protocol-specific metadata\\n  4. Stores all connections in `siem_capture_connections`\\n  5. Updates capture status to \\\"parsed\\\"\\n\\n- `BuildTrafficSummary(ctx, capture)` \u2014 Aggregates connections into a `NexieTrafficInput` JSON structure for AI analysis\\n  - Top talkers (by bytes)\\n  - Protocol breakdown\\n  - Service breakdown\\n  - DNS queries and TLS SNI hosts\\n  - HTTP requests (limited to 100)\\n  - Suspicious flags (e.g., connections to port 4444, large DNS transfers)\\n\\n**Extraction methods:**\\n- `extractConnections()` \u2014 Uses `tshark -z conv,tcp/udp` to get IP conversations\\n- `extractDNS()` \u2014 Filters DNS queries and responses\\n- `extractHTTP()` \u2014 Extracts HTTP requests with host, method, URI, status\\n- `extractTLS()` \u2014 Extracts TLS ClientHello SNI and version\\n\\n**Limitations:**\\n- Requires tshark to be installed (`TsharkAvailable()` checks this)\\n- Limited to 500 HTTP/TLS records per capture (to avoid overwhelming AI)\\n- Parses only the first 200 connections for the AI summary\\n\\n## Data Models\\n\\nKey types defined in `types.go`:\\n\\n- **Event** \u2014 A single syslog event with source IP, severity, action, src/dst IP:port, protocol, service, message, raw log, and timestamp\\n- **Capture** \u2014 A PCAP file with metadata (name, filename, file size, packet count, duration, status)\\n- **Connection** \u2014 A parsed network connection with src/dst IP:port, protocol, service, packet/byte counts, TLS SNI, DNS query, HTTP metadata\\n- **NexieTrafficInput** \u2014 Aggregated traffic summary for AI analysis (top talkers, protocol/service breakdown, DNS/TLS/HTTP samples, flags)\\n- **CaptureSession** \u2014 A time-windowed collection of syslog events with status (collecting, exporting, complete, analyzed)\\n- **Analysis** \u2014 AI analysis result with risk score, findings, traffic profile, and raw AI response\\n\\n## Workflows\\n\\n### Syslog Ingestion\\n\\n1. **Start listener** \u2014 `handleSyslogStart()` creates a `SyslogListener` and calls `Start()`\\n2. **Create session** \u2014 Inserts a `siem_capture_sessions` row with status \\\"collecting\\\"\\n3. **Register session** \u2014 Calls `listener.RegisterSession(sessionID, sourceIPs)` to route events\\n4. **Receive events** \u2014 Listener parses incoming syslog and calls `storeEvent()` for each\\n5. **Stop session** \u2014 `handleSessionStop()` unregisters the session and calls `exportSession()`\\n6. **Export** \u2014 `exportSession()` writes all events to a JSONL file (optionally AES-256-GCM encrypted)\\n\\n### PCAP Analysis\\n\\n1. **Upload** \u2014 `handleUploadCapture()` saves the file to disk and creates a `siem_captures` row\\n2. **Parse** \u2014 Background goroutine calls `parser.ParseCapture()` to extract connections\\n3. **Analyze** \u2014 `handleAnalyzeCapture()` builds a traffic summary and calls `ai.AnalyzeTrafficRaw()`\\n4. **Store results** \u2014 Inserts findings, risk score, and traffic profile into `siem_analyses`\\n5. **Promote findings** \u2014 Critical/high findings are inserted into `security_alerts`\\n\\n### Remediation\\n\\n1. **Generate plan** \u2014 `handleGenerateRemediation()` extracts a specific finding and calls `ai.GenerateRemediationRaw()`\\n2. **Create task** \u2014 Inserts a `nexie_tasks` row with status \\\"pending_approval\\\"\\n3. **Audit discovery** \u2014 Logs the finding and plan to `siem_remediation_audit`\\n4. **Snapshot posture** \u2014 `takePostureSnapshot()` captures pre-remediation risk metrics\\n5. **Batch mode** \u2014 `handleBatchRemediation()` repeats steps 1-4 for multiple findings (filtered by scope)\\n6. **Track execution** \u2014 External systems update task status and result; `handleRemediationStatus()` queries all tasks for an analysis\\n7. **Compare posture** \u2014 `handlePostureComparison()` retrieves pre/post snapshots to measure improvement\\n\\n### Log Retention\\n\\n1. **Scheduler** \u2014 `StartLogRetention()` runs `runRetention()` hourly\\n2. **Load config** \u2014 Reads `tenants.siem_retention_tiers` (JSONB array of `{max_score, retention_days}`)\\n3. **Apply tiers** \u2014 For each tier, deletes events from sessions with risk scores in that range older than the retention period\\n4. **Default retention** \u2014 Sessions without an analysis use `tenants.siem_retention_default_days` (default 30 days)\\n5. **Orphaned events** \u2014 Events with no session_id are purged using the default retention\\n\\n## Database Schema (Key Tables)\\n\\n- **siem_events** \u2014 Individual syslog events (tenant_id, source_ip, event_type, severity, action, src_ip, dst_ip, message, session_id, event_time)\\n- **siem_sources** \u2014 Configured syslog sources (tenant_id, name, source_type, source_ip, enabled, event_count, last_event_at)\\n- **siem_captures** \u2014 Uploaded PCAP files (tenant_id, name, filename, file_path, file_size, packet_count, status, created_by)\\n- **siem_capture_connections** \u2014 Parsed connections from PCAP (capture_id, src_ip, dst_ip, protocol, service, packets, bytes, tls_sni, dns_query, http_host, http_method, http_uri)\\n- **siem_capture_sessions** \u2014 Time-windowed event collections (tenant_id, name, status, event_count, file_size, duration_secs, started_at, stopped_at, poll_interval, source_ips)\\n- **siem_analyses** \u2014 AI analysis results (tenant_id, capture_id, session_id, status, risk_score, summary, findings, traffic_profile, raw_ai_response)\\n- **nexie_tasks** \u2014 Remediation tasks (tenant_id, task_type, status, analysis_id, finding_index, remediation_plan, device_credential_id, requested_by, approved_by)\\n- **siem_remediation_audit** \u2014 Immutable audit trail (tenant_id, task_id, analysis_id, phase, actor, details, created_at)\\n- **siem_posture_snapshots** \u2014 Pre/post remediation security posture (tenant_id, task_id, snapshot_type, risk_score, findings_count, critical_count, high_count, medium_count, low_count, summary)\\n\\n## Integration Points\\n\\n### With `internal/auth`\\n\\n- `TenantIDFromContext()` \u2014 Extracts tenant ID from request context for all multi-tenant queries\\n- `UserIDFromContext()` \u2014 Extracts user ID for remediation request tracking\\n- `ClaimsFromContext()` \u2014 Retrieves user role to hide cost pills from client users\\n\\n### With `internal/ui`\\n\\n- `Renderer.Render()` \u2014 Renders the SIEM dashboard and HTMX fragments\\n\\n### With Nexie AI\\n\\n- `AITrafficAnalyzer` interface \u2014 Implemented by the AI provider\\n  - `AnalyzeTrafficRaw(ctx, tenantID, trafficJSON)` \u2014 Analyzes a traffic summary and returns findings\\n  - `GenerateRemediationRaw(ctx, tenantID, findingJSON, analysisContext)` \u2014 Generates a remediation plan for a finding\\n  - `GeneratePostureReport(ctx, tenantID, preSnapshot, postContext)` \u2014 Compares pre/post posture (optional)\\n\\n### With Device Management\\n\\n- `device_credentials` table \u2014 Stores FortiGate credentials for remediation execution\\n- `nexie_tasks` \u2014 Links to device credentials for automated remediation\\n\\n## Configuration\\n\\n**Environment variables:**\\n- `SIEM_ENCRYPTION_KEY` \u2014 64-character hex string (32 bytes) for AES-256-GCM session export encryption. If not set or invalid, exports are plaintext.\\n\\n**Database columns (per tenant):**\\n- `tenants.siem_enabled` \u2014 Boolean flag to enable/disable SIEM\\n- `tenants.siem_syslog_port` \u2014 Port for syslog listener (default 5514)\\n- `tenants.siem_retention_tiers` \u2014 JSONB array of `{max_score, retention_days}` for score-based retention\\n- `tenants.siem_retention_default_days` \u2014 Default retention for sessions without analysis (default 30)\\n\\n## Security Considerations\\n\\n1. **Encryption** \u2014 Session exports can be AES-256-GCM encrypted if a key is configured\\n2. **TLS syslog** \u2014 Supports RFC 5425 encrypted syslog with certificate validation\\n3. **UTF-8 sanitization** \u2014 Strips null bytes to prevent PostgreSQL injection\\n4. **Audit trail** \u2014 All remediation actions are logged immutably to `siem_remediation_audit`\\n5. **Multi-tenancy** \u2014 All queries filter by `tenant_id`; no cross-tenant data leakage\\n6. **Session isolation** \u2014 Capture sessions are scoped to source IPs; wildcard \\\"*\\\" is explicit\\n\\n## Performance Notes\\n\\n- **Syslog parsing** \u2014 Runs in background goroutines per message; no blocking on the listener\\n- **PCAP parsing** \u2014 Runs in background; can take minutes for large captures\\n- **AI analysis** \u2014 Async with polling; UI polls every 5 seconds until complete\\n- **Log retention** \u2014 Runs hourly; uses batch DELETE with indexed queries\\n- **Session export** \u2014 Streams events to disk; encrypts in-memory if needed\\n\\n## Common Tasks\\n\\n### Add a new syslog source\\n```\\nPOST /api/siem/sources\\nname=MyFortiGate&amp;source_type=syslog&amp;source_ip=192.168.1.1&amp;company_id=\\n```\\n\\n### Start capturing events\\n```\\nPOST /api/siem/syslog/start\\ncompany_id=&amp;poll_interval=5&amp;source_ips=192.168.1.1,192.168.1.2\\n```\\n\\n### Upload and analyze a PCAP\\n```\\nPOST /api/siem/captures/upload\\n(multipart: pcap file, name, source_device, company_id)\\n\\nPOST /api/siem/captures/{id}/analyze\\n(triggers Nexie analysis)\\n```\\n\\n### Generate remediation for a finding\\n```\\nPOST /api/siem/analyses/{id}/findings/{idx}/remediate\\ndevice_credential_id= (optional)\\n```\\n\\n### Configure log retention\\n```\\nPOST /api/siem/retention\\n{\\n  \\\"tiers\\\": [\\n    {\\\"max_score\\\": 14, \\\"retention_days\\\": 7},\\n    {\\\"max_score\\\": 39, \\\"retention_days\\\": 14},\\n    {\\\"max_score\\\": 100, \\\"retention_days\\\": 30}\\n  ],\\n  \\\"default_days\\\": 30\\n}\\n```\",\"internal-survey\":\"# internal \u2014 survey\\n\\n# Survey Module Documentation\\n\\n## Overview\\n\\nThe **survey** module provides a comprehensive field assessment system for pre-bid site surveys. Technicians walk through customer sites, document areas and equipment, capture GPS-tagged photos, and complete trade-specific checklists. The module supports multiple survey types (cabling, access control, fire alarm, CCTV, network, etc.) and includes AI-powered photo analysis and voice-to-assessment transcription.\\n\\nKey capabilities:\\n- **Multi-area surveys** with room/floor/wing inventory\\n- **Equipment documentation** with manufacturer, model, serial number, and condition tracking\\n- **Photo management** with GPS coordinates, thumbnails, and area/item linking\\n- **Trade-specific checklists** with required item enforcement\\n- **iPad-first walkthrough mode** optimized for on-site data entry\\n- **AI vision analysis** to identify equipment, detect code violations, and flag undocumented items\\n- **Audio transcription** with Claude-powered item extraction from walkthrough narration\\n- **Survey-to-bid conversion** to pre-populate bid sections from survey findings\\n- **Pre-departure gate** to enforce completeness before leaving the site\\n\\n---\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    A[\\\"Survey Handler(CRUD, Pages)\\\"] --&gt; B[\\\"Audio Capture(Walkthrough Mic)\\\"]\\n    A --&gt; C[\\\"Photo Management(Upload, Tagging)\\\"]\\n    A --&gt; D[\\\"Vision Analysis(Claude Vision)\\\"]\\n    A --&gt; E[\\\"Checklist System(Templates, Responses)\\\"]\\n    A --&gt; F[\\\"Departure Gate(Completeness Check)\\\"]\\n    \\n    B --&gt; G[\\\"Claude API(Transcript \u2192 Items)\\\"]\\n    D --&gt; G\\n    \\n    A --&gt; H[\\\"Survey-to-BidConversion\\\"]\\n    \\n    C --&gt; I[\\\"Thumbnail Generation(Image Resize)\\\"]\\n    \\n    style A fill:#e1f5ff\\n    style G fill:#fff3e0\\n    style H fill:#f3e5f5\\n```\\n\\n---\\n\\n## Core Components\\n\\n### 1. Handler (`handler.go`)\\n\\nThe main HTTP request router and business logic coordinator.\\n\\n**Key Responsibilities:**\\n- Survey CRUD (create, read, update, delete)\\n- Area and item management\\n- Photo upload and metadata\\n- Checklist template management and responses\\n- Departure completeness checks\\n- Survey-to-bid conversion\\n- Data loading and denormalization\\n\\n**Key Methods:**\\n\\n| Method | Purpose |\\n|--------|---------|\\n| `handleList` | List surveys with filtering by status, company, type |\\n| `handleDetail` | Load full survey with areas, items, photos, checklists |\\n| `handleWalkthrough` | iPad-optimized walkthrough interface |\\n| `handleDepartureCheck` | Pre-departure completeness validation |\\n| `handleComplete` | Mark survey as completed and trigger orchestrator |\\n| `handleConvertToBid` | Create bid sheet from survey data |\\n| `handleAddArea`, `handleUpdateArea`, `handleDeleteArea` | Area CRUD |\\n| `handleAddItem`, `handleUpdateItem`, `handleDeleteItem` | Item CRUD |\\n| `handleUploadPhoto`, `handleUpdatePhoto`, `handleDeletePhoto` | Photo CRUD |\\n| `handleSaveChecklists` | Persist checklist responses |\\n| `handleAnalyzePhotos` | Trigger AI vision analysis |\\n\\n**Data Loaders:**\\n- `loadSurvey()` / `loadSurveyFull()` \u2014 Load survey with related entities\\n- `loadChecklistTemplates()` \u2014 Load active templates for a trade\\n- `loadChecklistResponses()` \u2014 Load completed checklist items\\n- `loadPhotoAnalyses()` \u2014 Load AI vision results\\n- `loadCompanies()`, `loadDeals()`, `loadUsers()`, `loadContacts()` \u2014 Reference data\\n\\n---\\n\\n### 2. Audio Capture (`audio.go`)\\n\\nConverts walkthrough narration into structured survey items via Claude.\\n\\n**Flow:**\\n1. Browser's SpeechRecognition API captures audio and sends transcript to `/api/surveys/{id}/audio`\\n2. `handleSaveAudioTranscript()` saves transcript and triggers async extraction\\n3. `extractItemsFromTranscript()` calls Claude with the transcript\\n4. Claude extracts structured items (equipment, cable runs, measurements, conditions)\\n5. Items are stored in `survey_audio.ai_extracted_items` as JSON\\n6. User accepts/rejects items via `handleAcceptAudioItem()` or `handleAcceptAllAudioItems()`\\n7. Accepted items are inserted into `survey_items` table\\n\\n**Key Types:**\\n- `AudioRecording` \u2014 Saved transcript with extraction status and results\\n- `ExtractedItem` \u2014 Single item extracted by Claude (type, description, manufacturer, model, condition, quantity, measurements, notes)\\n\\n**Claude Prompts:**\\n- **System prompt:** Positions Claude as a low-voltage/IT infrastructure field technician assistant\\n- **Extraction prompt:** Defines item types (existing_equipment, needed_equipment, cable_run, device_location, outlet, patch_panel, rack, conduit, pathway, junction_box) and area attributes (drop ceiling, raised floor, plenum, ceiling height, sqft, accessibility)\\n\\n**Critical Feature \u2014 BOM Expansion:**\\nWhen a tech says \\\"4 door access control install,\\\" Claude explodes it into individual billable components:\\n- 4\u00d7 Lock Hardware (electric strike/maglock)\\n- 4\u00d7 Controller/Panel\\n- 4\u00d7 Card Reader\\n- 4\u00d7 REX (request to exit button)\\n- 4\u00d7 DPI (door position indicator)\\n- 4\u00d7 PIR (motion sensor)\\n- Power supplies + cable runs\\n\\nThis ensures the survey captures the full scope of work, not shorthand.\\n\\n**Auto-Checklist Matching:**\\n`autoCheckChecklistFromTranscript()` cross-references the transcript against checklist template items and auto-marks matches as checked. Uses keyword matching with word-count thresholds to avoid false positives.\\n\\n---\\n\\n### 3. Vision Analysis (`vision.go`)\\n\\nAI-powered photo analysis using Claude Vision API.\\n\\n**Analysis Dimensions:**\\n\\n| Dimension | Purpose | Output |\\n|-----------|---------|--------|\\n| **Quality Scoring** | 1\u20135 scale assessment of photo usability | QualityScore, QualityNotes |\\n| **Equipment Identification** | Extract manufacturer, model, serial from nameplates | EquipmentFinding[] |\\n| **Code Violations** | Detect NEC, NFPA 72, TIA-568, UL 294 violations | CodeViolation[] |\\n| **Undocumented Items** | Flag visible items not logged as survey items | UndocumentedItem[] |\\n| **Label Extraction** | OCR-like text reading from labels/nameplates | LabelText, LabelsReadable |\\n\\n**Key Types:**\\n- `PhotoAnalysis` \u2014 Complete analysis result for one photo\\n- `EquipmentFinding` \u2014 Identified equipment with confidence level\\n- `CodeViolation` \u2014 Compliance issue with severity and code reference\\n- `UndocumentedItem` \u2014 Visible but unlogged item with estimated quantity\\n\\n**Workflow:**\\n1. `handleAnalyzePhotos()` queues all survey photos for analysis\\n2. `analyzeSurveyPhotos()` loads photos and calls `runVisionAnalysis()` for each\\n3. `runVisionAnalysis()` encodes photo as base64, calls Claude Vision API\\n4. Claude returns structured JSON with quality, equipment, violations, undocumented items\\n5. Results stored in `survey_photo_analyses` table\\n6. `handlePhotoAnalysisPage()` renders results with polling for in-progress analyses\\n7. `addVisionFindings()` integrates findings into departure gate checks\\n\\n**Departure Gate Integration:**\\n- Low quality photos (\u22642/5) \u2192 warning\\n- Critical code violations \u2192 warning\\n- Undocumented items \u2192 warning (user should add them before leaving)\\n- All photos analyzed \u2192 pass\\n\\n**Thumbnail Generation:**\\n`generateThumbnail()` creates 400px max-width JPEG thumbnails for fast loading. Runs async after photo upload.\\n\\n---\\n\\n### 4. Checklist System\\n\\nTrade-specific assessment templates with required item enforcement.\\n\\n**Entities:**\\n- `ChecklistTemplate` \u2014 Reusable template (trade, name, items array, active flag)\\n- `ChecklistItem` \u2014 Single template item (label, required flag, category)\\n- `ChecklistResponse` \u2014 User's answer to a template item (checked, notes, photo_id)\\n\\n**Workflow:**\\n1. Admin creates templates in `/settings/assessments` (e.g., \\\"Cabling Pre-Survey Checklist\\\")\\n2. Templates are trade-specific (cabling, access_control, fire_alarm, etc.)\\n3. On survey detail page, templates for the survey's trade are loaded\\n4. User checks items as they walk the site\\n5. `handleSaveChecklists()` persists responses\\n6. Departure gate enforces all required items are checked\\n\\n**Auto-Checking:**\\nWhen audio transcript is processed, `autoCheckChecklistFromTranscript()` automatically checks matching items. For example, if the transcript mentions \\\"drop ceiling,\\\" the \\\"Has drop ceiling\\\" checklist item is auto-checked.\\n\\n**Template Management:**\\n- `handleCreateTemplate()` \u2014 Create new template\\n- `handleUpdateTemplate()` \u2014 Update name/description\\n- `handleSaveAllTemplateItems()` \u2014 Save entire items array from edit form\\n- `handleMoveTemplateItemUp()` / `handleMoveTemplateItemDown()` \u2014 Reorder items\\n- `handleToggleTemplate()` \u2014 Activate/deactivate template\\n\\n---\\n\\n### 5. Departure Gate (`handler.go` \u2014 `runDepartureChecks()`)\\n\\nPre-departure completeness validation with blockers and warnings.\\n\\n**Checks Performed:**\\n\\n| Check | Blocker? | Purpose |\\n|-------|----------|---------|\\n| Has areas | Yes | At least one area must be documented |\\n| Has photos | Yes | At least one photo required |\\n| Required checklist items | Yes | All required items must be checked |\\n| Areas with photos | No | Warning if any area has zero photos |\\n| Areas with items | No | Warning if any area has zero items |\\n| Equipment details | No | Warning if existing equipment missing mfg/model |\\n| Building info | No | Warning if building type or sqft missing |\\n| Photo quality | No | Warning if any photo scores \u22642/5 |\\n| Code violations | No | Warning if critical violations detected |\\n| Undocumented items | No | Warning if visible items not logged |\\n\\n**Result Structure:**\\n```go\\ntype DepartureCheckResult struct {\\n    Passed   bool                 // true if no blockers\\n    Blockers []DepartureCheckItem // must fix before leaving\\n    Warnings []DepartureCheckItem // should review\\n    Passes   []DepartureCheckItem // completed checks\\n}\\n```\\n\\n**Integration:**\\n- Rendered at `/surveys/{id}/departure-check`\\n- Blocks survey completion if `Passed == false`\\n- Integrates vision findings via `addVisionFindings()`\\n\\n---\\n\\n### 6. Survey-to-Bid Conversion\\n\\nConverts completed survey into a bid sheet with pre-populated sections and line items.\\n\\n**Process:**\\n1. User clicks \\\"Convert to Bid\\\" on completed survey\\n2. `handleConvertToBid()` creates new bid sheet with survey metadata\\n3. For each survey area \u2192 creates bid section\\n4. For each survey item (needed_equipment, cable_run, etc.) \u2192 creates bid line item\\n5. Existing equipment in good/fair condition is skipped\\n6. Equipment marked \\\"replace\\\" is flagged in description\\n7. Survey is marked as \\\"converted\\\" and linked to bid\\n\\n**Example:**\\n```\\nSurvey Area: \\\"Server Room\\\"\\n  \u2192 Bid Section: \\\"Server Room\\\"\\n    \u2192 Line Item: \\\"Cisco Catalyst 9300 Switch \u2014 REPLACE\\\"\\n    \u2192 Line Item: \\\"Cat6A Cable Run \u2014 500 ft\\\"\\n    \u2192 Line Item: \\\"Patch Panel 48-port\\\"\\n```\\n\\n---\\n\\n## Data Model\\n\\n### Core Tables\\n\\n**site_surveys**\\n- `id`, `tenant_id`, `company_id`, `deal_id`, `bid_sheet_id`\\n- `title`, `status` (draft, in_progress, completed, converted), `survey_type`\\n- `site_address`, `site_city`, `site_state`, `site_zip`\\n- `building_type`, `total_sqft`, `floor_count`\\n- `surveyed_by`, `survey_date`, `notes`, `poc_contact_id`, `work_type_id`\\n- `created_at`, `updated_at`, `converted_at`\\n\\n**survey_areas**\\n- `id`, `tenant_id`, `survey_id`\\n- `name`, `area_type` (room, floor, wing, building, outdoor, ceiling, mdf, idf, riser, lobby, hallway, warehouse)\\n- `floor_number`, `sqft`, `ceiling_height`\\n- `has_drop_ceiling`, `has_raised_floor`, `has_plenum`\\n- `accessibility`, `notes`, `sort_order`\\n- `created_at`\\n\\n**survey_items**\\n- `id`, `tenant_id`, `survey_id`, `area_id`\\n- `item_type` (existing_equipment, needed_equipment, cable_run, device_location, outlet, patch_panel, rack, conduit, pathway, junction_box)\\n- `category`, `description`\\n- `manufacturer`, `model`, `serial_number`\\n- `condition` (good, fair, poor, replace, missing, new_install)\\n- `quantity`, `measurements` (JSONB), `notes`, `sort_order`\\n- `created_at`\\n\\n**survey_photos**\\n- `id`, `tenant_id`, `survey_id`, `area_id`, `item_id`\\n- `file_path`, `thumb_path`, `file_name`\\n- `caption`, `location_tag`, `floor_tag`, `condition_note`\\n- `latitude`, `longitude`, `taken_at`\\n- `uploaded_by`, `created_at`\\n\\n**survey_audio**\\n- `id`, `tenant_id`, `survey_id`, `area_id`\\n- `transcript`, `duration_seconds`\\n- `status` (pending, processing, complete, failed)\\n- `error_message`\\n- `ai_extracted_items` (JSONB array of ExtractedItem)\\n- `items_accepted`, `items_rejected`\\n- `ai_cost`, `created_at`, `updated_at`\\n\\n**survey_checklist_templates**\\n- `id`, `tenant_id`\\n- `trade` (general, cabling, access_control, fire_alarm, cctv, network, av, hvac, electrical)\\n- `name`, `description`\\n- `items` (JSONB array of ChecklistItem)\\n- `is_active`, `created_at`, `updated_at`\\n\\n**survey_checklist_responses**\\n- `id`, `tenant_id`, `survey_id`, `template_id`, `area_id`\\n- `item_label`, `checked`, `value`, `notes`, `photo_id`\\n- `created_at`\\n\\n**survey_photo_analyses**\\n- `id`, `tenant_id`, `survey_id`, `photo_id`\\n- `status` (pending, complete, failed)\\n- `error_message`\\n- `quality_score` (1\u20135), `quality_notes`\\n- `equipment_found` (JSONB), `code_violations` (JSONB), `undocumented_items` (JSONB)\\n- `labels_readable`, `label_text`\\n- `ai_summary`, `ai_cost`\\n- `created_at`, `updated_at`\\n\\n---\\n\\n## HTTP Routes\\n\\n### Survey Management\\n- `GET /surveys` \u2014 List surveys with filtering\\n- `GET /surveys/new` \u2014 New survey form\\n- `GET /surveys/{id}` \u2014 Survey detail page\\n- `GET /surveys/{id}/edit` \u2014 Edit survey form\\n- `GET /surveys/{id}/walkthrough` \u2014 iPad walkthrough interface\\n- `GET /surveys/{id}/departure-check` \u2014 Pre-departure gate\\n- `GET /surveys/{id}/print` \u2014 Printable survey report\\n- `POST /api/surveys` \u2014 Create survey\\n- `PUT /api/surveys/{id}` \u2014 Update survey\\n- `DELETE /api/surveys/{id}` \u2014 Delete survey\\n- `POST /api/surveys/{id}/complete` \u2014 Mark completed\\n\\n### Areas\\n- `POST /api/surveys/{id}/areas` \u2014 Add area\\n- `PUT /api/surveys/{id}/areas/{areaId}` \u2014 Update area\\n- `DELETE /api/surveys/{id}/areas/{areaId}` \u2014 Delete area\\n\\n### Items\\n- `POST /api/surveys/{id}/items` \u2014 Add item\\n- `PUT /api/surveys/{id}/items/{itemId}` \u2014 Update item\\n- `DELETE /api/surveys/{id}/items/{itemId}` \u2014 Delete item\\n\\n### Photos\\n- `POST /api/surveys/{id}/photos` \u2014 Upload photo\\n- `PUT /api/surveys/{id}/photos/{photoId}` \u2014 Update photo metadata\\n- `DELETE /api/surveys/{id}/photos/{photoId}` \u2014 Delete photo\\n- `GET /api/surveys/{id}/drawings` \u2014 List linked drawings\\n\\n### Audio Capture\\n- `POST /api/surveys/{id}/audio` \u2014 Save transcript and trigger extraction\\n- `POST /api/surveys/{id}/audio/{audioId}/accept` \u2014 Accept single extracted item\\n- `POST /api/surveys/{id}/audio/{audioId}/accept-all` \u2014 Accept all extracted items\\n- `POST /api/surveys/{id}/audio/{audioId}/decline-all` \u2014 Reject all extracted items\\n- `GET /api/surveys/{id}/audio` \u2014 List audio recordings\\n\\n### Vision Analysis\\n- `POST /api/surveys/{id}/analyze-photos` \u2014 Trigger AI analysis\\n- `GET /surveys/{id}/photo-analysis` \u2014 Analysis results page\\n- `GET /api/surveys/{id}/photo-analysis` \u2014 Analysis results (HTMX partial)\\n- `GET /api/surveys/{id}/photos/{photoId}/analysis` \u2014 Single photo analysis\\n\\n### Checklists\\n- `GET /api/surveys/{id}/checklists` \u2014 Get responses\\n- `POST /api/surveys/{id}/checklists` \u2014 Save responses\\n- `GET /api/survey-checklist-templates` \u2014 List templates\\n\\n### Conversion\\n- `POST /api/surveys/{id}/convert-to-bid` \u2014 Create bid from survey\\n\\n### Settings\\n- `GET /settings/assessments` \u2014 Template management page\\n- `POST /api/assessment-checklist-templates` \u2014 Create template\\n- `PUT /api/assessment-checklist-templates/{templateId}` \u2014 Update template\\n- `DELETE /api/assessment-checklist-templates/{templateId}` \u2014 Delete template\\n- `GET /settings/assessments/{templateId}/edit` \u2014 Edit template form\\n- `POST /api/assessment-checklist-templates/{templateId}/items` \u2014 Save items\\n- `POST /api/assessment-checklist-templates/{templateId}/items/{itemIdx}/move-up` \u2014 Reorder\\n- `POST /api/assessment-checklist-templates/{templateId}/items/{itemIdx}/move-down` \u2014 Reorder\\n- `POST /api/assessment-checklist-templates/{templateId}/toggle` \u2014 Activate/deactivate\\n\\n---\\n\\n## Integration Points\\n\\n### With Auth Module\\n- `TenantIDFromContext()` \u2014 Extract tenant from request context\\n- `UserIDFromContext()` \u2014 Extract user from request context\\n- All handlers enforce tenant isolation\\n\\n### With AI Module\\n- `ai.Provider.CallClaudeRawWithCost()` \u2014 Call Claude for audio extraction\\n- `ai.Provider.CallClaudeVisionWithCost()` \u2014 Call Claude Vision for photo analysis\\n- `ai.Provider.GetAPIKey()` \u2014 Retrieve tenant's API key\\n- `ai.UsageMeta` \u2014 Track usage for billing\\n\\n### With UI Module\\n- `ui.Renderer.Render()` \u2014 Render HTML templates\\n- HTMX integration for dynamic updates\\n\\n### With Orchestrator\\n- `OnSurveyCompleted` callback \u2014 Triggered when survey marked complete\\n- Allows downstream workflows (e.g., auto-create bid, notify sales)\\n\\n---\\n\\n## Key Workflows\\n\\n### Walkthrough (iPad)\\n1. Tech opens `/surveys/{id}/walkthrough`\\n2. Adds areas (rooms, floors, etc.)\\n3. For each area:\\n   - Captures photos with GPS and captions\\n   - Speaks observations into mic (SpeechRecognition API)\\n   - Transcript sent to `/api/surveys/{id}/audio`\\n   - Claude extracts items in real-time\\n   - Tech accepts/rejects extracted items\\n   - Manually adds items if needed\\n   - Checks off checklist items\\n4. Before leaving, visits `/surveys/{id}/departure-check`\\n5. Fixes any blockers\\n6. Marks survey complete via `/api/surveys/{id}/complete`\\n\\n### Photo Analysis\\n1. Tech uploads photos during walkthrough\\n2. After survey completion, clicks \\\"Analyze Photos\\\"\\n3. `handleAnalyzePhotos()` queues all photos\\n4. `runVisionAnalysis()` processes each photo async\\n5. Claude Vision identifies equipment, violations, undocumented items\\n6. Results displayed in `/surveys/{id}/photo-analysis`\\n7. Tech can add undocumented items to survey\\n8. Violations reviewed before departing\\n\\n### Survey-to-Bid\\n1. Survey marked as completed\\n2. Tech clicks \\\"Convert to Bid\\\"\\n3. New bid sheet created with survey metadata\\n4. Survey areas \u2192 bid sections\\n5. Survey items \u2192 bid line items\\n6. Tech opens bid sheet to add pricing and finalize\\n\\n---\\n\\n## Constants &amp; Enums\\n\\n**Survey Status:**\\n- `draft`, `in_progress`, `completed`, `converted`\\n\\n**Survey Types:**\\n- `general`, `cabling`, `access_control`, `fire_alarm`, `cctv`, `network`, `av`, `hvac`, `electrical`\\n\\n**Area Types:**\\n- `room`, `floor`, `wing`, `building`, `outdoor`, `ceiling`, `mdf`, `idf`, `riser`, `lobby`, `hallway`, `warehouse`\\n\\n**Item Types:**\\n- `existing_equipment`, `needed_equipment`, `cable_run`, `device_location`, `outlet`, `patch_panel`, `rack`, `conduit`, `pathway`, `junction_box`\\n\\n**Conditions:**\\n- `good`, `fair`, `poor`, `replace`, `missing`, `new_install`\\n\\n**Building Types:**\\n- `office`, `warehouse`, `medical`, `retail`, `industrial`, `education`, `government`, `residential`, `mixed_use`\\n\\n**Checklist Trades:**\\n- `general`, `cabling`, `access_control`, `fire_alarm`, `cctv`, `network`, `av`, `hvac`, `electrical`\\n\\n---\\n\\n## Error Handling\\n\\n- **Missing survey:** Returns 404\\n- **Unauthorized (wrong tenant):** Silently fails (no data returned)\\n- **AI API errors:** Stored in `error_message` field, status set to `failed`\\n- **Photo upload errors:** Returns 400/500 with message\\n- **Database errors:** Logged and returned as 500\\n\\n---\\n\\n## Performance Considerations\\n\\n- **Thumbnail generation:** Async after photo upload to avoid blocking\\n- **Vision analysis:** Async per-photo to allow polling UI\\n- **Audio extraction:** Async to avoid blocking transcript save\\n- **Checklist auto-matching:** Runs after transcript extraction, uses word-count heuristics\\n- **Departure checks:** Computed on-demand, not cached\\n\\n---\\n\\n## Testing Considerations\\n\\n- Mock `ai.Provider` for audio/vision tests\\n- Use test database with tenant isolation\\n- Test departure gate with various combinations of blockers/warnings\\n- Test BOM expansion for access control and cabling scenarios\\n- Test checklist auto-matching with edge cases (short words, partial matches)\",\"internal-tenant\":\"# internal \u2014 tenant\\n\\n# Tenant Module\\n\\nThe tenant module manages multi-tenant settings, user administration, and integrations for NexusOS PSA. Each tenant represents an MSP organization with isolated users, configuration, and data. The module provides both HTML pages and JSON APIs for settings management, user/role administration, MCP server configuration, and OAuth 2.0 integration flows.\\n\\n## Overview\\n\\nThe module is split into two files:\\n\\n- **handler.go**: HTTP handlers for settings pages, user management, role administration, MCP server CRUD, email accounts, and tenant configuration.\\n- **mcp_oauth.go**: OAuth 2.0 Authorization Code flow implementation for MCP server authentication.\\n\\nAll handlers extract the tenant ID from the request context (set by auth middleware) to ensure strict data isolation. Settings are stored in PostgreSQL with JSONB columns for flexible key-value storage.\\n\\n## Core Components\\n\\n### Handler\\n\\nThe `Handler` struct is the main entry point:\\n\\n```go\\ntype Handler struct {\\n    pool        *pgxpool.Pool      // Database connection pool\\n    renderer    *ui.Renderer       // HTML template renderer\\n    cfg         *config.Config     // Application config (PublicURL, RMM certs, etc.)\\n    platformKey []byte             // 32-byte AES-256-GCM key for encrypting secrets\\n}\\n```\\n\\nThe `platformKey` is the SIEM encryption key (from `SIEM_ENCRYPTION_KEY` env var) reused for encrypting OAuth tokens and MCP client secrets at rest. If nil, OAuth flows and secret writes are disabled.\\n\\n### Route Registration\\n\\n`RegisterRoutes()` wires up ~50 HTTP endpoints across six categories:\\n\\n1. **Settings pages** \u2014 General, Integrations, MCP, API Keys, Audit, MFA Setup, Profile, Sessions, Users, QB Mappings, AI Usage, Portal Roles\\n2. **Tenant API** \u2014 Update org name/slug, save polling intervals, agent staleness thresholds, security settings, session timeout, RMM/AI/doc provider config\\n3. **User management** \u2014 CRUD users, reset password/MFA, list users as JSON\\n4. **Invitations** \u2014 Create, delete, resend invite emails with 7-day expiry tokens\\n5. **Roles** \u2014 Create custom roles, update permissions, delete (system roles protected)\\n6. **MCP servers** \u2014 CRUD servers, test connectivity, OAuth authorize/callback, on-demand distributor sync\\n7. **Email accounts** \u2014 List, save, delete, test SMTP/Microsoft Graph connectivity\\n8. **Tenant settings** \u2014 Key-value store for arbitrary tenant config\\n\\n## Settings Management\\n\\n### JSONB Settings Storage\\n\\nMost tenant configuration lives in the `tenants.settings` JSONB column:\\n\\n```json\\n{\\n  \\\"rmm_protocol\\\": \\\"https\\\",\\n  \\\"rmm_host\\\": \\\"rmm.example.com\\\",\\n  \\\"rmm_port\\\": \\\"8443\\\",\\n  \\\"ai_provider\\\": \\\"anthropic\\\",\\n  \\\"ai_api_key\\\": \\\"sk-ant-...\\\",\\n  \\\"ai_model\\\": \\\"claude-sonnet-4-20250514\\\",\\n  \\\"mfa_required\\\": true,\\n  \\\"session_timeout_minutes\\\": 480,\\n  \\\"agent_stale_minutes\\\": 2,\\n  \\\"agent_offline_minutes\\\": 10,\\n  \\\"timer_warn_min\\\": 20,\\n  \\\"timer_danger_min\\\": 25,\\n  \\\"timer_crit_min\\\": 30\\n}\\n```\\n\\nHelper functions extract values with type coercion and defaults:\\n\\n- `getSettingInt(settings, key, defaultVal)` \u2014 Handles float64, int, string\\n- `getSettingBool(settings, key, defaultVal)` \u2014 Handles bool, float64, string\\n- `getSettingStr(settings, key, defaultVal)` \u2014 Returns string or default\\n\\nUpdates use PostgreSQL's `||` operator to merge new values:\\n\\n```sql\\nUPDATE tenants SET settings = settings || jsonb_build_object('mfa_required', true)\\nWHERE id = $1\\n```\\n\\n### Security Audit Logging\\n\\nChanges to security-sensitive settings (MFA enforcement, session timeout) are logged to the immutable `security_audit_log` table with:\\n\\n- User ID, email, full name\\n- Event type (`mfa_enforcement_enabled`, `session_timeout_changed`, etc.)\\n- Previous and new values\\n- Client IP and user agent\\n- Timestamp (cannot be modified or deleted)\\n\\nExample from `handleSaveSecurity`:\\n\\n```go\\nh.pool.Exec(r.Context(), `\\n    INSERT INTO security_audit_log (tenant_id, user_id, user_email, user_name, event_type, description, previous_value, new_value, ip_address, user_agent)\\n    VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,\\n    tenantID, claims.UserID, claims.Email, claims.FullName,\\n    \\\"mfa_enforcement_enabled\\\", description,\\n    fmt.Sprintf(\\\"%v\\\", previousMFA), fmt.Sprintf(\\\"%v\\\", req.MFARequired),\\n    clientIP, r.UserAgent())\\n```\\n\\n## User &amp; Role Management\\n\\n### User CRUD\\n\\nUsers are created with email, full name, password (hashed via `auth.HashPassword`), and a role ID. Duplicate emails within a tenant are rejected. Password strength is validated via `auth.ValidatePassword`.\\n\\n- **Create** \u2014 `handleCreateUser` inserts a new user with hashed password\\n- **Update** \u2014 `handleUpdateUser` allows changing name, email, role, is_active, or password (each field is optional)\\n- **Reset Password** \u2014 `handleResetPassword` force-sets a password and revokes all refresh tokens\\n- **Reset MFA** \u2014 `handleResetMFA` clears MFA and logs the action to security_audit_log\\n- **Delete** \u2014 `handleDeleteUser` soft-deactivates (sets is_active=false) and revokes sessions\\n\\n### Roles &amp; Permissions\\n\\nRoles are either system-defined (admin, manager, technician, viewer) or custom. Each role has a set of permissions stored in `role_permissions`:\\n\\n- **Create** \u2014 `handleCreateRole` inserts a custom role and its permissions\\n- **Update** \u2014 `handleUpdateRole` replaces permissions (name/description only for custom roles)\\n- **Delete** \u2014 `handleDeleteRole` removes a role if no users are assigned; system roles cannot be deleted\\n- **Get Permissions** \u2014 `handleGetRolePermissions` returns the permission list for a role\\n\\nPermission checks use `auth.HasPermission(ctx, permission)` to gate endpoints.\\n\\n### Invitations\\n\\nInvitations are time-limited tokens sent via email. The flow:\\n\\n1. Admin calls `handleCreateInvitation` with email, full name, role ID\\n2. A 32-byte random token is generated and stored with 7-day expiry\\n3. `sendInviteEmail` sends an HTML email with an invite link\\n4. User clicks the link, accepts the invitation, and creates their account\\n5. Admin can resend (`handleResendInvitation`) or cancel (`handleDeleteInvitation`)\\n\\nTokens are generated via `generateInviteToken()` using `crypto/rand` and hex-encoded.\\n\\n## MCP Server Configuration\\n\\nMCP (Model Context Protocol) servers are external AI/data providers that NexusOS integrates with. The module supports three auth types:\\n\\n- **none** \u2014 No authentication\\n- **api_key** / **bearer_token** \u2014 Static token stored encrypted\\n- **oauth_authorization_code** \u2014 OAuth 2.0 flow with refresh token\\n\\n### CRUD Operations\\n\\n- **Create** \u2014 `handleCreateMCPServer` inserts a server with URL, auth config, and optional OAuth endpoints\\n- **Get** \u2014 `handleGetMCPServer` returns editable fields (secrets are never returned)\\n- **Update** \u2014 `handleUpdateMCPServer` allows changing URL, scope, auth type; secrets are opt-in overwrite\\n- **Delete** \u2014 `handleDeleteMCPServer` removes the server\\n- **Test** \u2014 `handleTestMCPServer` checks connectivity (HEAD request for static auth, tools/list call for OAuth)\\n\\n### Secret Encryption\\n\\nFor OAuth servers, the `client_secret` is encrypted at rest using `sentinel.Encrypt(platformKey, []byte(secret))`. Access and refresh tokens are also encrypted. The encryption key is the same one used for SIEM secrets, ensuring consistent key management.\\n\\n### Health Status\\n\\nEach server tracks `health_status` (healthy/unhealthy/unknown) and `last_health_check`. The test endpoint updates these fields.\\n\\n## OAuth 2.0 Authorization Code Flow\\n\\nThe MCP OAuth flow is implemented in `mcp_oauth.go` and handles the browser-based authorization dance required by MCP servers that only accept `authorization_code` grant (not `client_credentials`).\\n\\n### Flow Diagram\\n\\n```\\nAdmin clicks \\\"Connect\\\"\\n        \u2193\\nGET /settings/mcp/{id}/authorize\\n        \u2193\\nBuild signed state + PKCE challenge\\n        \u2193\\n302 redirect to MCP server's /authorize\\n        \u2193\\nUser logs in, approves scopes\\n        \u2193\\nMCP server redirects to /settings/mcp/oauth/callback?code=...&amp;state=...\\n        \u2193\\nVerify state HMAC + expiry\\n        \u2193\\nPOST code + client_secret to /token endpoint\\n        \u2193\\nEncrypt access_token + refresh_token\\n        \u2193\\nStore in mcp_servers row\\n        \u2193\\n302 redirect to /settings/mcp\\n```\\n\\n### State Signing\\n\\nThe `state` parameter carries the server ID, a nonce, and a PKCE verifier, all signed with HMAC-SHA256:\\n\\n```go\\ntype oauthState struct {\\n    ServerID string `json:\\\"sid\\\"`\\n    Nonce    string `json:\\\"n\\\"`\\n    IssuedAt int64  `json:\\\"iat\\\"`\\n    Verifier string `json:\\\"v,omitempty\\\"` // PKCE code_verifier\\n}\\n```\\n\\n`signState()` returns `base64url(JSON).base64url(HMAC)`. `verifyState()` checks the signature and rejects tokens older than 10 minutes.\\n\\n### PKCE Support\\n\\nThe flow includes PKCE (RFC 7636) to prevent authorization code interception attacks:\\n\\n1. Generate a random 96-character verifier\\n2. Compute `code_challenge = base64url(SHA256(verifier))`\\n3. Send `code_challenge` and `code_challenge_method=S256` in the authorize request\\n4. Include `code_verifier` in the token exchange\\n\\n### Token Persistence\\n\\nOn successful token exchange, both tokens are encrypted and stored:\\n\\n```sql\\nUPDATE mcp_servers\\nSET access_token_enc = $1,\\n    access_token_expires_at = $2,\\n    refresh_token_enc = $3,\\n    health_status = 'healthy',\\n    last_health_check = now(),\\n    updated_at = now()\\nWHERE id = $4 AND tenant_id = $5\\n```\\n\\nThe `access_token_expires_at` is set to `now() + expires_in` (or 1 hour if not provided). The `mcpclient` package uses this to refresh silently when needed.\\n\\n## Email Accounts\\n\\nEmail accounts support both SMTP and Microsoft Graph API (OAuth). The module provides:\\n\\n- **List** \u2014 `handleListEmailAccounts` returns all accounts for the tenant\\n- **Save** \u2014 `handleSaveEmailAccount` creates or updates an account; secrets are opt-in\\n- **Delete** \u2014 `handleDeleteEmailAccount` removes an account\\n- **Test** \u2014 `handleTestEmail` sends a test email to the current user\\n\\nAccounts can be marked as auto-create-tickets (incoming emails create support tickets) and have a default team assignment.\\n\\n## Distributor Sync\\n\\nThe module integrates with the `distributor` package to sync product catalogs from cloud distributors (PAX8, TD Synnex, Ingram, D&amp;H, Microsoft NCE). The `/settings/mcp` page displays per-distributor sync status under each vendor MCP server card.\\n\\n`handleDistributorSync` is the on-demand \\\"Refresh\\\" button:\\n\\n1. Verify the server belongs to the tenant\\n2. Create an MCP client and distributor wrapper (currently only PAX8)\\n3. Call `syncer.SyncOne()` to fetch and ingest the latest catalog\\n4. Redirect back to `/settings/mcp` to show updated counts and timestamps\\n\\nThe scheduler in the distributor package handles nightly syncs; this endpoint is the recovery path for admins who want immediate results.\\n\\n## Integration Points\\n\\n### Authentication &amp; Authorization\\n\\nAll handlers extract context via:\\n\\n- `auth.TenantIDFromContext(r.Context())` \u2014 Tenant ID (UUID)\\n- `auth.UserIDFromContext(r.Context())` \u2014 Current user ID (UUID)\\n- `auth.ClaimsFromContext(r.Context())` \u2014 JWT claims (email, full name, etc.)\\n- `auth.HasPermission(r.Context(), permission)` \u2014 Permission check\\n\\nThe auth middleware (in `internal/auth`) sets these before the handler runs.\\n\\n### Database\\n\\nAll queries use `pgxpool.Pool` with parameterized statements to prevent SQL injection. Tenant ID is always included in WHERE clauses to enforce isolation.\\n\\n### Rendering\\n\\nHTML pages use `ui.Renderer.Render(w, template, data, isHTMX)`:\\n\\n- `template` \u2014 Path to .html file in `templates/settings/`\\n- `data` \u2014 Map of variables for template substitution\\n- `isHTMX` \u2014 If true, render only the content fragment (no layout); used for HTMX partial updates\\n\\n### External Services\\n\\n- **Email** \u2014 `billing.NewEmailSender(pool)` sends invites and test emails via SMTP or Microsoft Graph\\n- **MCP Client** \u2014 `mcpclient.New(pool, platformKey, serverID)` calls MCP server tools\\n- **Sentinel** \u2014 `sentinel.Encrypt/Decrypt` handles AES-256-GCM encryption of secrets\\n- **Distributor** \u2014 `distributor.NewSyncer(pool)` syncs product catalogs\\n\\n## Common Patterns\\n\\n### Null-Safe String Handling\\n\\n`nullableStr(s)` returns `nil` for empty strings so PostgreSQL stores NULL instead of empty string. This keeps COALESCE semantics clean:\\n\\n```go\\nfunc nullableStr(s string) interface{} {\\n    if s == \\\"\\\" {\\n        return nil\\n    }\\n    return s\\n}\\n```\\n\\n### Relative Time Formatting\\n\\n`formatRelative(t *time.Time)` renders timestamps as \\\"5m ago\\\", \\\"3h ago\\\", \\\"2d ago\\\", or \\\"\u2014\\\" for nil. Used in the distributor sync status display.\\n\\n### Settings Extraction with Defaults\\n\\nThe three `getSetting*` helpers handle JSON unmarshaling and type coercion:\\n\\n```go\\nstaleMinutes := getSettingInt(settings, \\\"agent_stale_minutes\\\", 2)\\nmfaRequired := getSettingBool(settings, \\\"mfa_required\\\", false)\\nrmmHost := getSettingStr(settings, \\\"rmm_host\\\", \\\"localhost\\\")\\n```\\n\\n### Form Value Clamping\\n\\nNumeric settings are clamped to sane ranges before saving:\\n\\n```go\\nif serverS &lt; 10 { serverS = 10 }\\nif serverS &gt; 600 { serverS = 600 }\\n```\\n\\n## Error Handling\\n\\nHandlers return HTTP errors with descriptive messages:\\n\\n- `400 Bad Request` \u2014 Invalid JSON, missing required fields, validation failure\\n- `404 Not Found` \u2014 Resource not found or doesn't belong to tenant\\n- `409 Conflict` \u2014 Duplicate email, role in use, etc.\\n- `500 Internal Server Error` \u2014 Database or encryption errors\\n\\nDatabase errors are logged to stdout but not exposed to the client (except for specific cases like \\\"duplicate key\\\").\\n\\n## Security Considerations\\n\\n1. **Tenant Isolation** \u2014 Every query includes `tenant_id = $X` to prevent cross-tenant data leaks\\n2. **Secret Encryption** \u2014 OAuth tokens, MCP client secrets, and email passwords are encrypted at rest\\n3. **CSRF Protection** \u2014 OAuth state is HMAC-signed and time-limited (10 min)\\n4. **PKCE** \u2014 Authorization code interception is prevented via code_challenge/code_verifier\\n5. **Audit Logging** \u2014 Security-sensitive changes are logged immutably\\n6. **Password Hashing** \u2014 User passwords are hashed via bcrypt (in `auth` package)\\n7. **Permission Checks** \u2014 Sensitive endpoints (portal roles, AI usage thresholds) are gated by `auth.HasPermission`\\n\\n## Testing Endpoints\\n\\n- **RMM** \u2014 `handleTestRMM` performs mTLS handshake with the configured RMM server\\n- **MCP** \u2014 `handleTestMCPServer` calls tools/list for OAuth servers or HEAD for static auth\\n- **Email** \u2014 `handleTestEmail` sends a test email to the current user's address\\n\\nAll three provide user-friendly success/failure messages.\",\"internal-timer\":\"# internal \u2014 timer\\n\\n# Timer Module Documentation\\n\\n## Overview\\n\\nThe `internal/timer` module provides a multi-timer API for field technicians and support staff. Users can run multiple timers simultaneously\u2014one per ticket or project task\u2014with only one \\\"focused\\\" timer visible in the header at any time. When a timer stops, a work time entry is created for billing and time tracking purposes.\\n\\n**Key capabilities:**\\n- Start, pause, resume, stop, and discard timers\\n- Focus/unfocus timers to manage which one is prominent\\n- Escalate tickets directly from a timer\\n- Retrieve billing context (contracts, rates, work types) for the stop modal\\n- Automatic contract resolution and consumption tracking\\n- SLA escalation policy integration\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    A[\\\"API Handlers(apiStart, apiStop, etc.)\\\"]\\n    B[\\\"Timer State Builders(buildTimerState, enrichTicketContext)\\\"]\\n    C[\\\"Billing Helpers(resolveTicketContract, loadContractBillingDetails)\\\"]\\n    D[\\\"Stop Helpers(resolveStopBilling, recordStopTimeEntries)\\\"]\\n    E[\\\"Escalation Helpers(resolveEscalationTicket, loadEscalationTiers)\\\"]\\n    F[\\\"Database(work_timers, tickets, contracts)\\\"]\\n    \\n    A --&gt; B\\n    A --&gt; C\\n    A --&gt; D\\n    A --&gt; E\\n    A --&gt; F\\n    B --&gt; F\\n    C --&gt; F\\n    D --&gt; F\\n    E --&gt; F\\n```\\n\\n## Core Concepts\\n\\n### Timer States\\n\\nA timer has three operational states:\\n\\n- **running**: actively accumulating time\\n- **paused**: stopped but not finalized; can be resumed\\n- **stopped**: finalized; a work time entry has been created\\n\\nOnly one non-stopped timer per user can be \\\"focused\\\" at a time. The focused timer is displayed prominently in the UI; others run in the background.\\n\\n### Timer Lifecycle\\n\\n1. **Start** (`apiStart`): Creates a new timer or resumes an existing one for a ticket. Unfocuses all other timers and pauses any running ones.\\n2. **Pause/Resume** (`apiPause`, `apiResume`): Toggles between running and paused states without finalizing.\\n3. **Stop** (`apiStop`): Finalizes the timer, creates a work time entry with billing details, and promotes the next timer to focused.\\n4. **Discard** (`apiDiscard`): Silently removes a timer without creating a time entry.\\n5. **Focus** (`apiFocus`): Switches which timer is focused.\\n\\n### Billing Context\\n\\nWhen stopping a timer, the system resolves:\\n- **Contract**: ticket override \u2192 company contract \u2192 none\\n- **Work Type**: determines coverage (covered, billable_extra, excluded) and rate multiplier\\n- **Billing Role**: determines hourly rate\\n- **Product**: invoice line item category\\n- **Cost Rate**: employee cost for internal accounting\\n\\nAll of these can have contract-specific overrides that take precedence over defaults.\\n\\n### Escalation\\n\\nEscalation policies define tiers (1, 2, 3, ...) with:\\n- Time threshold (after N minutes)\\n- Assignment target (user or team)\\n- Notification channels (email, SMS, push, Slack)\\n\\nPolicies are resolved: ticket override \u2192 company \u2192 tenant default.\\n\\n## API Endpoints\\n\\n### `GET /api/timer`\\n\\nReturns all active timers for the authenticated user.\\n\\n**Response:**\\n```json\\n{\\n  \\\"focused\\\": { /* TimerState */ },\\n  \\\"others\\\": [ /* TimerState[] */ ],\\n  \\\"count\\\": 3,\\n  \\\"warn_min\\\": 20,\\n  \\\"danger_min\\\": 25,\\n  \\\"crit_min\\\": 30\\n}\\n```\\n\\nEach `TimerState` includes elapsed seconds, ticket/task context, SLA thresholds, and escalation tiers.\\n\\n### `POST /api/timer/start`\\n\\nStarts a new timer or resumes an existing one.\\n\\n**Request:**\\n```json\\n{\\n  \\\"ticket_id\\\": \\\"...\\\",\\n  \\\"project_task_id\\\": \\\"...\\\",\\n  \\\"survey_id\\\": \\\"...\\\",\\n  \\\"note\\\": \\\"optional work note\\\"\\n}\\n```\\n\\nAt least one of `ticket_id`, `project_task_id`, or `survey_id` is required.\\n\\n**Behavior:**\\n- If a timer already exists for the ticket, it is resumed instead of creating a duplicate.\\n- All other timers are unfocused and paused.\\n- The new/resumed timer becomes focused and running.\\n\\n### `POST /api/timer/pause` / `POST /api/timer/resume`\\n\\nToggles a timer between running and paused states.\\n\\n**Request:**\\n```json\\n{\\n  \\\"timer_id\\\": \\\"...\\\" // optional; if empty, targets the focused timer\\n}\\n```\\n\\n### `POST /api/timer/stop`\\n\\nFinalizes a timer and creates a work time entry.\\n\\n**Request:**\\n```json\\n{\\n  \\\"timer_id\\\": \\\"...\\\",\\n  \\\"work_type_id\\\": \\\"...\\\",\\n  \\\"billing_role_id\\\": \\\"...\\\",\\n  \\\"product_id\\\": \\\"...\\\",\\n  \\\"contract_id\\\": \\\"...\\\",\\n  \\\"description\\\": \\\"work performed\\\",\\n  \\\"is_billable\\\": true,\\n  \\\"ticket_status\\\": \\\"resolved\\\",\\n  \\\"override_minutes\\\": 45\\n}\\n```\\n\\n**Behavior:**\\n- Computes total elapsed time (or uses `override_minutes` if provided).\\n- Resolves billing details (contract, rates, multipliers, coverage).\\n- Creates `ticket_time_entries` (if linked to a ticket) and `work_time_entries` (unified log).\\n- Applies contract consumption (deducts from block hours or retainer pools).\\n- Updates ticket status if requested.\\n- Promotes the next timer to focused.\\n\\n**Response:**\\n```json\\n{\\n  \\\"status\\\": \\\"stopped\\\",\\n  \\\"total_seconds\\\": 2700,\\n  \\\"hours\\\": 0.75,\\n  \\\"time_entry_created\\\": true,\\n  \\\"redirect_url\\\": \\\"/helpdesk/tickets/...\\\"\\n}\\n```\\n\\n### `POST /api/timer/discard`\\n\\nRemoves a timer without creating a time entry.\\n\\n**Request:**\\n```json\\n{\\n  \\\"timer_id\\\": \\\"...\\\" // optional; if empty, discards the focused timer\\n}\\n```\\n\\n### `POST /api/timer/focus`\\n\\nMakes a specific timer the focused one.\\n\\n**Request:**\\n```json\\n{\\n  \\\"timer_id\\\": \\\"...\\\"\\n}\\n```\\n\\n### `POST /api/timer/escalate`\\n\\nEscalates a timer's ticket to a specified tier.\\n\\n**Request:**\\n```json\\n{\\n  \\\"timer_id\\\": \\\"...\\\",\\n  \\\"tier\\\": 2,\\n  \\\"note\\\": \\\"customer escalation request\\\"\\n}\\n```\\n\\n**Behavior:**\\n- Resolves the ticket from the timer (or uses the focused timer if `timer_id` is empty).\\n- Loads the escalation policy (ticket \u2192 company \u2192 tenant default).\\n- Picks the requested tier (or the highest configured tier if the requested one doesn't exist).\\n- Reassigns the ticket to the tier's user or team.\\n- Sets ticket status to \\\"escalated\\\".\\n- Records a history entry and internal note.\\n- Increments the company's escalation count.\\n\\n**Response:**\\n```json\\n{\\n  \\\"status\\\": \\\"escalated\\\",\\n  \\\"tier\\\": 2,\\n  \\\"assigned_to\\\": \\\"John Doe\\\",\\n  \\\"ticket_id\\\": \\\"...\\\",\\n  \\\"redirect_url\\\": \\\"/helpdesk/tickets/...\\\"\\n}\\n```\\n\\n### `GET /api/timer/{id}/billing`\\n\\nReturns billing context for the stop modal.\\n\\n**Response:**\\n```json\\n{\\n  \\\"timer_id\\\": \\\"...\\\",\\n  \\\"ticket_id\\\": \\\"...\\\",\\n  \\\"ticket_title\\\": \\\"...\\\",\\n  \\\"elapsed_seconds\\\": 2700,\\n  \\\"hours\\\": \\\"0.75\\\",\\n  \\\"contract_id\\\": \\\"...\\\",\\n  \\\"contract_name\\\": \\\"...\\\",\\n  \\\"contract_category\\\": \\\"block_hours\\\",\\n  \\\"company_name\\\": \\\"...\\\",\\n  \\\"block_hours_remaining\\\": 10.5,\\n  \\\"block_hours_purchased\\\": 20.0,\\n  \\\"work_types\\\": [\\n    {\\n      \\\"id\\\": \\\"...\\\",\\n      \\\"name\\\": \\\"Installation\\\",\\n      \\\"multiplier\\\": 1.0,\\n      \\\"coverage\\\": \\\"covered\\\"\\n    }\\n  ],\\n  \\\"billing_roles\\\": [\\n    {\\n      \\\"id\\\": \\\"...\\\",\\n      \\\"name\\\": \\\"Technician\\\",\\n      \\\"hourly_rate\\\": 85.0,\\n      \\\"is_user_role\\\": true,\\n      \\\"cost_rate\\\": 45.0\\n    }\\n  ],\\n  \\\"user_billing_role_id\\\": \\\"...\\\",\\n  \\\"products\\\": [ /* ProductBilling[] */ ],\\n  \\\"available_contracts\\\": [ /* ContractOption[] */ ]\\n}\\n```\\n\\n## Key Helper Functions\\n\\n### Timer State Building (`list_timers_helpers.go`)\\n\\n**`buildTimerState()`**\\nConstructs a `TimerState` from a database row, computing elapsed seconds (accumulated + current segment for running timers).\\n\\n**`enrichTicketContext()`**\\nLoads ticket title, company name, SLA policy, and escalation policy. Parses escalation tiers JSON and resolves assignee names. Derives warn/danger/crit thresholds from the tiers.\\n\\n**`deriveTimerThresholds()`**\\nMaps escalation tiers to SLA warning levels:\\n- Tier 1 \u2192 warn (orange)\\n- Tier 2 \u2192 danger (red)\\n- Tier 3 \u2192 crit (pulse + escalate)\\n\\nWhen fewer than 3 tiers exist, synthesizes missing thresholds by adding 5\u201310 minutes to the highest tier.\\n\\n**`enrichTaskTitle()`**\\nFills `TimerState.Title` from `project_tasks` when not already set by ticket lookup.\\n\\n**`isOrphanTimer()`**\\nFlags timers with no ticket/task linkage or that failed to resolve a title and have no accumulated time.\\n\\n**`autoStopOrphan()`**\\nMarks orphaned timers as stopped so they don't reappear in subsequent list calls.\\n\\n**`loadTenantTimerDefaults()`**\\nReads warn/danger/crit thresholds from tenant settings JSON, falling back to 20/25/30 minutes.\\n\\n**`autoPromoteFocused()`**\\nEnsures the user always has a focused timer when any non-stopped timers exist. Promotes the first background timer to focused if none is currently focused.\\n\\n### Billing Context (`billing_context_helpers.go`)\\n\\n**`loadBillingTimerInfo()`**\\nFetches timer row and derives elapsed seconds + hours for the stop modal. Hours floor at 0.01 only when elapsed &gt; 0 (unlike `apiStop`, where hours always floor at 0.01).\\n\\n**`resolveTicketContract()`**\\nLoads ticket title, company, and contract. If the ticket has no contract but the company has an active one, resolves and persists the company's most recent active contract onto the ticket.\\n\\n**`loadContractBillingDetails()`**\\nReturns contract name, category, and block-hours totals (when applicable).\\n\\n**`loadWorkTypesWithContract()` / `loadWorkTypesNoContract()`**\\nReturns work types with contract-specific coverage and rate multipliers (override \u2192 default). Without a contract, coverage is forced to \\\"billable_extra\\\".\\n\\n**`loadBillingRolesWithContract()` / `loadBillingRolesNoContract()`**\\nReturns billing roles with contract rate overrides and the user's cost rate. Sorts to put the user's own roles first.\\n\\n**`loadAvailableContractsForCompany()`**\\nReturns active service contracts for a company, deduped by name to avoid superseded/legal-doc duplicates.\\n\\n**`loadUserBillingRoleID()`**\\nReturns the user's default billing role, or \\\"\\\" if unset.\\n\\n**`loadInvoiceProducts()`**\\nReturns products eligible for time-entry invoice lines (labor/service categories only).\\n\\n### Stop Processing (`stop_helpers.go`)\\n\\n**`computeStopDuration()`**\\nCalculates total elapsed time and its hour/minute derivatives, honoring an optional manual minute override. Hours always floor at 0.01 minimum.\\n\\n**`resolveStopContractID()`**\\nPicks the contract for this stop: tech override first, then the contract stored on the ticket, else empty.\\n\\n**`resolveStopBilling()`**\\nComputes work-type/role/contract billing fields. Precedence:\\n1. Base defaults (isBillable=true, coverage=billable_extra)\\n2. Work-type \u00d7 role \u00d7 contract path computes rate/multiplier/amount/coverage\\n3. Survey timers inherit billable from their work type\\n4. Explicit `req.IsBillable` wins over everything\\n\\n**`resolveStopCostRate()`**\\nLooks up the user's employee cost rate, preferring an exact role+trade match, then role+any-trade.\\n\\n**`recordStopTimeEntries()`**\\nInserts `ticket_time_entries` (when linked to a ticket) and the unified `work_time_entries` row.\\n\\n**`applyContractConsumption()`**\\nDeducts billed hours from block_hours or retainer pools when the entry is covered by the contract.\\n\\n**`updateTicketStatusOnStop()`**\\nApplies an optional status change at stop time, writes a history row, and stamps `closed_at` for closed/resolved tickets.\\n\\n### Escalation (`escalate_helpers.go`)\\n\\n**`resolveEscalationTicket()`**\\nFinds the ticket attached to the requested timer (or the user's focused timer if no ID was supplied).\\n\\n**`loadEscalationTiers()`**\\nResolves the escalation policy for a ticket (ticket override \u2192 company \u2192 tenant default) and parses its tiers JSON.\\n\\n**`pickEscalationTier()`**\\nReturns the tier matching the requested number, falling back to the highest configured tier. Requested \u2264 0 defaults to tier 1.\\n\\n**`applyEscalationAssignment()`**\\nReassigns the ticket per the tier's user/team target and flips status to 'escalated'. Returns the human-readable assignee.\\n\\n**`recordEscalationAudit()`**\\nWrites the `ticket_history` row + an internal `ticket_notes` entry documenting the escalation.\\n\\n**`incrementCompanyEscalationCount()`**\\nBumps `escalation_count` on the ticket's company.\\n\\n## Shared Helpers (`handler.go`)\\n\\n**`unfocusAllTimers()`**\\nClears the `is_focused` flag on all active timers for a user.\\n\\n**`pauseRunningTimers()`**\\nPauses all running timers for a user and accumulates elapsed time.\\n\\n**`promoteNextTimer()`**\\nMakes the most recently updated non-stopped timer the focused one. Called after a timer is stopped or discarded so the user always has a focused timer.\\n\\n**`resumeExistingTimer()`**\\nFinds an active timer for a ticket, focuses and resumes it. Returns the timer ID if found, or \\\"\\\" if no existing timer exists.\\n\\n**`findTimerByIDOrFocused()`**\\nLocates a timer\u2014by explicit ID or the user's focused timer. Returns all fields needed by stop/discard handlers.\\n\\n## Data Structures\\n\\n### `TimerState`\\n\\nRepresents a single timer for JSON responses. Includes:\\n- Timer metadata (ID, status, focus state)\\n- Linked resource (ticket, task, survey)\\n- Elapsed time (total and accumulated)\\n- Title and note\\n- SLA thresholds (warn/danger/crit minutes)\\n- Escalation tiers and policy names\\n- Company context\\n\\n### `escalationTier`\\n\\nA single tier from an escalation policy:\\n- `Tier`: tier number (1, 2, 3, ...)\\n- `AfterMinutes`: time threshold\\n- `AssignTo`: user UUID (alternative to team)\\n- `AssignToName`: resolved user name\\n- `AssignTeam`: team name\\n- `Notify`: notification channels\\n\\n### `billingContext`\\n\\nEverything the stop modal needs:\\n- Timer and ticket info\\n- Contract details (name, category, block hours)\\n- Work types with coverage and multipliers\\n- Billing roles with rates\\n- User's default role\\n- Products for invoice lines\\n- Available contracts for override\\n\\n### `stopBilling` / `stopCost`\\n\\nInternal structures carrying resolved billing and cost fields used by time-entry inserts.\\n\\n## Database Schema (Key Tables)\\n\\n- **`work_timers`**: Timer rows (status, ticket_id, project_task_id, survey_id, accumulated_seconds, started_at, is_focused, note)\\n- **`tickets`**: Ticket rows (title, company_id, legal_contract_id, escalation_policy_id, status, type)\\n- **`companies`**: Company rows (name, sla_policy_id, escalation_policy_id, escalation_count)\\n- **`legal_contracts`**: Contract rows (name, contract_category, company_id, status)\\n- **`contract_block_hours`**: Block hours pools (hours_purchased, hours_consumed)\\n- **`contract_retainer_config`**: Retainer pools (current_month_hours_used)\\n- **`billing_work_types`**: Work type definitions (name, default_rate_multiplier, billable, is_active)\\n- **`contract_work_type_config`**: Work type overrides per contract (coverage, rate_multiplier_override)\\n- **`billing_roles`**: Role definitions (name, default_hourly_rate, is_active)\\n- **`contract_role_rate_overrides`**: Role rate overrides per contract (hourly_rate_override)\\n- **`employee_role_rates`**: User cost rates (user_id, billing_role_id, cost_rate, trade, end_date)\\n- **`escalation_policies`**: Policy definitions (name, tiers JSON, is_active)\\n- **`ticket_time_entries`**: Per-ticket time entries (billing details, rates, amounts)\\n- **`work_time_entries`**: Unified time entry log (all timers, all sources)\\n- **`ticket_history`**: Audit trail (field changes, escalations)\\n- **`ticket_notes`**: Internal and external notes (escalation notes marked as internal)\\n- **`project_tasks`**: Task rows (title, actual_hours)\\n- **`site_surveys`**: Survey rows (work_type_id, billable)\\n- **`products`**: Product/service definitions (name, category, price_monthly, is_active)\\n- **`users`**: User rows (full_name, billing_role_id)\\n- **`tenants`**: Tenant rows (settings JSON with timer thresholds)\\n\\n## Integration Points\\n\\n### Authentication\\n\\nAll endpoints extract `tenantID` and `userID` from the request context via `auth.TenantIDFromContext()` and `auth.UserIDFromContext()`.\\n\\n### Page Rendering\\n\\nThe `GetAllTimers()` method is called by page renderers to populate the timer list in the UI. It returns an `AllTimersResponse` with focused and background timers, plus tenant-level SLA thresholds.\\n\\n### Ticket &amp; Project Management\\n\\n- Stopping a timer can update ticket status and close_at timestamp.\\n- Stopping a timer increments `project_tasks.actual_hours`.\\n- Escalating a timer reassigns the ticket and updates its status.\\n\\n### Billing &amp; Accounting\\n\\n- Time entries are recorded in both `ticket_time_entries` (per-ticket) and `work_time_entries` (unified log).\\n- Contract consumption is applied (block hours deducted, retainer hours incremented).\\n- Cost rates are resolved for internal accounting.\\n\\n## Error Handling\\n\\nThe module uses HTTP status codes to signal errors:\\n- **400 Bad Request**: Invalid request payload, missing required fields, invalid tier\\n- **404 Not Found**: Timer not found, no focused timer, no escalation policy\\n- **500 Internal Server Error**: Database errors\\n\\nEscalation errors are mapped to specific sentinel strings (`escErrNoFocusedTimer`, `escErrNoTicket`, `escErrNoPolicy`, `escErrNoTiers`) that the orchestrator converts to HTTP statuses.\\n\\n## Concurrency &amp; Race Conditions\\n\\nThe `apiStart` handler includes a double-click guard: after pausing running timers, it checks again for an existing timer before creating a new one. This prevents duplicate timers when the client sends multiple start requests in quick succession.\\n\\n## Logging\\n\\nThe module logs significant events:\\n- Timer promotion to focused\\n- Timer discards\\n- Escalations\\n- Manual time overrides\\n- Database errors\\n\\nLogs use the standard `log` package and are prefixed with `\\\"timer:\\\"`.\",\"internal-timesheet\":\"# internal \u2014 timesheet\\n\\n# Timesheet Module\\n\\nThe timesheet module aggregates weekly work entries, manages approval workflows, and pushes approved timesheets to QuickBooks Online. It sits between the work-time-entry system and external accounting integrations.\\n\\n## Overview\\n\\nTimesheets are weekly rollups of a user's `work_time_entries`. The lifecycle is:\\n\\n1. **Draft** \u2014 entries accumulate automatically as users log time\\n2. **Submitted** \u2014 user submits the week for review\\n3. **Approved** \u2014 manager approves; triggers async QBO push\\n4. **Pushed** \u2014 entries synced to QuickBooks (or push failed)\\n5. **Rejected** \u2014 manager sends back with reason; user can resubmit\\n\\nThe module handles both employee and manager workflows, plus HR/Payroll reporting and CSV export.\\n\\n## Architecture\\n\\n```mermaid\\ngraph LR\\n    User[\\\"User(logs time)\\\"]\\n    WTE[\\\"work_time_entries(DB)\\\"]\\n    TS[\\\"Timesheet(weekly rollup)\\\"]\\n    Handler[\\\"Handler(HTTP)\\\"]\\n    Store[\\\"Store(persistence)\\\"]\\n    QBO[\\\"QuickBooks(async push)\\\"]\\n    \\n    User --&gt;|creates| WTE\\n    WTE --&gt;|aggregated into| TS\\n    Handler --&gt;|queries| Store\\n    Store --&gt;|reads/writes| TS\\n    Handler --&gt;|triggers| QBO\\n    QBO --&gt;|updates| TS\\n```\\n\\n## Core Types\\n\\n### Timesheet\\n\\nA `Timesheet` represents one user's work for a calendar week (Monday\u2013Sunday). Key fields:\\n\\n- **ID, TenantID, UserID** \u2014 identity\\n- **WeekStart, WeekEnd** \u2014 ISO week boundaries (stored as date strings)\\n- **Status** \u2014 one of `draft`, `submitted`, `approved`, `rejected`, `pushed`\\n- **SubmittedAt, ApprovedAt, RejectedAt, PushedAt** \u2014 workflow timestamps\\n- **ApproverID, ApproverName** \u2014 who approved/rejected\\n- **RejectionReason** \u2014 why it was sent back\\n- **Notes** \u2014 user's submission notes\\n- **PushAttempted, PushError** \u2014 QBO sync state\\n- **Entries** \u2014 loaded on-demand; array of `Entry` structs\\n- **TotalHours, BillableHrs, EntryCount, PushedCount** \u2014 derived metrics\\n\\n### Entry\\n\\nAn `Entry` mirrors a `work_time_entries` row with joined context:\\n\\n- **ID, WorkDate, Hours, Description** \u2014 core time entry\\n- **IsBillable, BillableAmount, EffectiveRate, Coverage** \u2014 billing metadata\\n- **TicketID, TicketTitle, TicketNumber** \u2014 linked ticket (if any)\\n- **ProjectTaskID, ProjectTaskName** \u2014 linked project task (if any)\\n- **CompanyID, CompanyName** \u2014 company context\\n- **WorkTypeName, BillingRoleName** \u2014 classification\\n- **QBTimeActivityID** \u2014 QuickBooks sync marker (non-empty = pushed)\\n- **CreatedAt** \u2014 when the entry was logged\\n\\n## Handler\\n\\nThe `Handler` serves HTTP endpoints and coordinates the workflow. It depends on:\\n\\n- **Store** \u2014 persistence layer\\n- **Renderer** \u2014 HTML template rendering\\n- **QBPusher** \u2014 optional interface for QuickBooks integration (wired at startup)\\n\\n### Routes\\n\\n| Method | Path | Handler | Purpose |\\n|--------|------|---------|---------|\\n| GET | `/timesheets` | `handleMyTimesheet` | Current user's timesheet (this week or specified week) |\\n| GET | `/timesheets/week/{weekStart}` | `handleMyTimesheet` | Current user's timesheet for a specific week |\\n| GET | `/timesheets/approvals` | `handleApproverQueue` | Manager's approval queue (submitted timesheets) |\\n| GET | `/timesheets/all` | `handleAllTimesheets` | HR/Payroll view of all timesheets (filtered) |\\n| GET | `/timesheets/all/export.csv` | `handleExportCSV` | CSV export of filtered timesheets |\\n| GET | `/timesheets/{id}` | `handleDetail` | Single timesheet detail (used by approvers) |\\n| POST | `/api/timesheets/{id}/submit` | `handleSubmit` | User submits timesheet |\\n| POST | `/api/timesheets/{id}/approve` | `handleApprove` | Manager approves; triggers QBO push |\\n| POST | `/api/timesheets/{id}/reject` | `handleReject` | Manager rejects with reason |\\n\\n### Key Handler Behaviors\\n\\n**handleMyTimesheet**\\n- Defaults to the current week; accepts optional `weekStart` path param (YYYY-MM-DD)\\n- Calls `GetOrCreateForWeek` to ensure a draft timesheet exists\\n- Loads entries and builds a Mon\u2013Sun grid for template rendering\\n- Includes navigation links to previous/next weeks\\n- Fetches user's recent timesheet history (8 most recent)\\n\\n**handleApproverQueue**\\n- Lists all `submitted` timesheets for the tenant\\n- Ordered by submission time (oldest first)\\n- Includes entry counts and total hours for quick review\\n\\n**handleAllTimesheets**\\n- Requires `timesheets:view_all` permission (HR/Payroll only)\\n- Supports filters: status, user, date range, name/email search\\n- Computes aggregate totals (total hours, billable hours)\\n- Populates user dropdown for filtering\\n\\n**handleExportCSV**\\n- Same permission gate as `handleAllTimesheets`\\n- Streams CSV with columns: user name/email, week dates, status, hours, entry counts, push state, approver, error\\n- Timestamps formatted as `YYYY-MM-DD HH:MM`\\n\\n**handleSubmit**\\n- Transitions `draft` or `rejected` \u2192 `submitted`\\n- Accepts optional notes (form or JSON)\\n- Links all unlinked entries in the week to the timesheet (via `Submit`)\\n- Idempotent: already-submitted sheets are left alone\\n\\n**handleApprove**\\n- Transitions `submitted` \u2192 `approved`\\n- Records approver ID and timestamp\\n- **Spawns async goroutine** to push to QBO (if `QBPusher` is wired)\\n  - 2-minute timeout\\n  - Failures call `MarkPushed` with error message but don't block approval\\n  - Success calls `MarkPushed` with empty error, sets status to `pushed`\\n\\n**handleReject**\\n- Transitions `submitted` \u2192 `rejected`\\n- Requires rejection reason (form or JSON)\\n- Records approver ID and reason\\n- User can resubmit after rejection\\n\\n## Store\\n\\nThe `Store` owns all database operations. It uses `pgxpool.Pool` for connection management.\\n\\n### Key Methods\\n\\n**GetOrCreateForWeek(ctx, tenantID, userID, weekStart)**\\n- Returns existing timesheet or creates a new draft\\n- Uses `ON CONFLICT` to handle race conditions\\n- Calls `GetByID` to return the full row\\n\\n**GetByWeek(ctx, tenantID, userID, weekStart)**\\n- Fetches a single timesheet by (tenant, user, week)\\n- Joins with `users` table for name/email\\n- Joins with approver's user record for approver name\\n- Returns `pgx.ErrNoRows` if not found\\n\\n**GetByID(ctx, tenantID, id)**\\n- Fetches by timesheet ID (with tenant check)\\n- Same joins as `GetByWeek`\\n\\n**LoadEntries(ctx, ts)**\\n- Populates `ts.Entries` with all `work_time_entries` in the week\\n- Joins with tickets, projects, companies, billing work types, and billing roles\\n- Accumulates `TotalHours`, `BillableHrs`, `EntryCount`, `PushedCount`\\n- Entries ordered by work date, then creation time\\n\\n**Submit(ctx, tenantID, timesheetID, notes)**\\n- Wraps in transaction with row-level lock (`FOR UPDATE`)\\n- Validates status is `draft` or `rejected`\\n- Links all unlinked entries in the week to the timesheet\\n- Updates status to `submitted`, clears rejection reason\\n- Returns count of linked entries\\n\\n**Approve(ctx, tenantID, timesheetID, approverID)**\\n- Updates status to `approved`, records approver and timestamp\\n- Fails if not in `submitted` state\\n\\n**Reject(ctx, tenantID, timesheetID, approverID, reason)**\\n- Updates status to `rejected`, records approver, timestamp, and reason\\n- Fails if not in `submitted` state\\n\\n**MarkPushed(ctx, tenantID, timesheetID, pushErr)**\\n- Called after QBO push attempt (success or failure)\\n- If `pushErr` is empty: sets status to `pushed`, clears error\\n- If `pushErr` is non-empty: keeps status unchanged, records error\\n- Always sets `push_attempted = true`\\n\\n**ListSubmitted(ctx, tenantID)**\\n- Returns all `submitted` timesheets for the tenant\\n- Includes computed totals (hours, entry count)\\n- Ordered by submission time (oldest first)\\n\\n**ListAll(ctx, tenantID, filters)**\\n- Returns all timesheets matching filters\\n- Supports: status, user ID, date range (week_start bounds), name/email substring\\n- Default limit 500; max 10,000 for CSV export\\n- Ordered by week_start DESC, then user name\\n- Includes computed totals and approver name\\n\\n**ListUsersWithTimesheets(ctx, tenantID)**\\n- Returns distinct users who have any timesheet\\n- Used to populate filter dropdowns\\n\\n**ListMyHistory(ctx, tenantID, userID, limit)**\\n- Returns user's recent timesheets (most recent first)\\n- Default limit 12\\n\\n### Utility Functions\\n\\n**MondayOf(t)**\\n- Returns the Monday that begins the ISO week containing `t`\\n- Treats Sunday as the end of the previous week\\n- Truncates time to midnight\\n\\n**itoa(n)**\\n- Simple int-to-string helper for dynamic SQL construction\\n\\n## QuickBooks Integration\\n\\nThe module uses a narrow `QBPusher` interface to avoid circular imports:\\n\\n```go\\ntype QBPusher interface {\\n    PushTimesheet(ctx context.Context, tenantID, timesheetID string) error\\n}\\n```\\n\\nThe actual implementation lives in the `quickbooks` package. Integration flow:\\n\\n1. **Startup** \u2014 `main.go` constructs both `Handler` and the QBO service, then calls `SetQBPusher` to wire them\\n2. **Approval** \u2014 `handleApprove` spawns a goroutine that calls `h.qb.PushTimesheet`\\n3. **Async** \u2014 push runs with a 2-minute timeout; failures are logged and recorded but never block approval\\n4. **Completion** \u2014 `MarkPushed` updates the timesheet with success/error state\\n\\nIf `QBPusher` is nil (not wired), approval still succeeds but no push occurs.\\n\\n## Permission Model\\n\\n- **Employee** \u2014 can view/submit own timesheet\\n- **Manager** \u2014 can view approval queue and approve/reject\\n- **HR/Payroll** \u2014 requires `timesheets:view_all` permission to access all-timesheets and export views\\n\\nPermission checks use `auth.HasPermission(ctx, \\\"timesheets:view_all\\\")`, which also grants access to admins (permission `\\\"*\\\"`).\\n\\n## Data Flow Example\\n\\n```\\nUser logs 8 hours on Monday\\n  \u2193\\nwork_time_entries row created (timesheet_id = NULL)\\n  \u2193\\nUser navigates to /timesheets\\n  \u2193\\nhandleMyTimesheet calls GetOrCreateForWeek\\n  \u2193\\nDraft timesheet created (if needed)\\n  \u2193\\nLoadEntries fetches all entries for the week\\n  \u2193\\nTemplate renders Mon\u2013Sun grid with entries\\n  \u2193\\nUser clicks \\\"Submit\\\" \u2192 handleSubmit\\n  \u2193\\nSubmit() links entries to timesheet, sets status=submitted\\n  \u2193\\nManager sees timesheet in /timesheets/approvals\\n  \u2193\\nManager clicks \\\"Approve\\\" \u2192 handleApprove\\n  \u2193\\nApprove() sets status=approved\\n  \u2193\\nAsync goroutine calls QBPusher.PushTimesheet\\n  \u2193\\nMarkPushed updates status=pushed (or records error)\\n```\\n\\n## Testing Considerations\\n\\n- **Week boundaries** \u2014 `MondayOf` is critical; test with dates across month/year boundaries\\n- **Idempotency** \u2014 `GetOrCreateForWeek` and `Submit` should be safe to call multiple times\\n- **Transactions** \u2014 `Submit` uses `FOR UPDATE` to prevent race conditions; verify lock behavior\\n- **Async push** \u2014 approval must succeed even if QBO is down; test timeout and error handling\\n- **Permissions** \u2014 verify `timesheets:view_all` gate on HR views\\n- **Filters** \u2014 `ListAll` builds dynamic SQL; test all filter combinations and edge cases\\n\\n## Common Patterns\\n\\n**Tenant isolation** \u2014 all queries include `tenant_id` in WHERE clause; no cross-tenant leakage\\n\\n**Date handling** \u2014 week boundaries stored as dates (not timestamps); formatted as `YYYY-MM-DD` strings in JSON/templates\\n\\n**Derived fields** \u2014 `TotalHours`, `BillableHrs`, `EntryCount`, `PushedCount` computed on-demand via subqueries in `ListAll` and `LoadEntries`\\n\\n**Async operations** \u2014 QBO push runs in background goroutine with timeout; failures recorded but non-blocking\\n\\n**HTML/HTMX** \u2014 handlers detect `HX-Request` header and pass `isHTMX` flag to renderer for partial vs. full page rendering\",\"internal-ui\":\"# internal \u2014 ui\\n\\n# internal/ui Module Documentation\\n\\n## Overview\\n\\nThe `internal/ui` module provides the template rendering engine and static file serving for the NexusOS PSA web interface. It manages Go HTML templates with HTMX support, per-user appearance preferences, and a comprehensive set of template functions for formatting, sanitization, and data transformation.\\n\\nThe module is responsible for:\\n- Parsing and caching all application templates from embedded filesystems\\n- Rendering pages with automatic user context injection\\n- Loading and applying per-user UI preferences (theme, accent color, density, etc.)\\n- Serving static assets (CSS, JS, images) with appropriate cache headers\\n- Sanitizing user-generated HTML content per audit requirements\\n- Providing 50+ template helper functions for common formatting tasks\\n\\n## Architecture\\n\\n### Template Organization\\n\\nTemplates are organized hierarchically:\\n\\n```\\ntemplates/\\n\u251c\u2500\u2500 layouts/\\n\u2502   \u251c\u2500\u2500 base.html          # Main desktop layout (sidebar, nav, content area)\\n\u2502   \u2514\u2500\u2500 mobile.html        # Mobile layout\\n\u251c\u2500\u2500 partials/\\n\u2502   \u251c\u2500\u2500 nav.html           # Navigation components\\n\u2502   \u251c\u2500\u2500 header.html        # Page headers\\n\u2502   \u2514\u2500\u2500 *.html             # Reusable UI components\\n\u2514\u2500\u2500 {module}/\\n    \u251c\u2500\u2500 *.html             # Page-specific templates\\n    \u251c\u2500\u2500 _*.html            # Private partials (prefixed with _)\\n    \u2514\u2500\u2500 tabs/              # Nested subdirectories\\n        \u2514\u2500\u2500 *.html\\n```\\n\\nEach page template defines a `content` block that renders inside the base layout. When HTMX makes a partial request (with `HX-Request: true` and a non-body `HX-Target`), only the `content` block is returned, enabling SPA-like navigation without full page reloads.\\n\\n### Rendering Flow\\n\\n```mermaid\\ngraph TD\\n    A[\\\"HTTP Handler\\\"] --&gt;|data map| B[\\\"RenderWithRequest\\\"]\\n    B --&gt;|extract user context| C[\\\"userInfoFromContext\\\"]\\n    B --&gt;|load preferences| D[\\\"loadUserPrefs\\\"]\\n    D --&gt;|query DB| E[\\\"users.ui_preferences\\\"]\\n    E --&gt;|fallback| F[\\\"DefaultUIPreferences\\\"]\\n    B --&gt;|check HTMX header| G[\\\"IsHTMXContent\\\"]\\n    G --&gt;|true| H[\\\"Execute 'content' block\\\"]\\n    G --&gt;|false| I[\\\"Execute 'base' layout\\\"]\\n    H --&gt;|inject UIPrefs| J[\\\"Render output\\\"]\\n    I --&gt;|inject UIPrefs| J\\n```\\n\\n## Key Components\\n\\n### Renderer\\n\\nThe `Renderer` struct manages parsed templates and renders them to HTTP responses.\\n\\n```go\\ntype Renderer struct {\\n    templates map[string]*template.Template  // Cached parsed templates\\n    mu        sync.RWMutex                   // Thread-safe access\\n    pool      any                            // DB pool for loading preferences\\n}\\n```\\n\\n**Constructor:**\\n```go\\nr := NewRenderer(dbPool)  // With DB pool for per-user prefs\\nr := NewRenderer()        // Without DB pool (uses defaults)\\n```\\n\\n**Key Methods:**\\n\\n- `Render(w io.Writer, name string, data interface{}, isHTMX bool)` \u2014 Core rendering method. Auto-injects `UIPrefs` and `AppVersion` into map data.\\n- `RenderWithRequest(w http.ResponseWriter, req *http.Request, name string, data map[string]interface{}, isHTMX bool)` \u2014 Preferred method for full pages. Extracts user info from request context and CSRF token from cookies.\\n- `RenderWithContext(w io.Writer, ctx context.Context, name string, data map[string]interface{}, isHTMX bool)` \u2014 Renders with context-stored user info.\\n- `RenderBlock(w io.Writer, name, block string, data interface{})` \u2014 Executes a specific named block (for standalone pages like login).\\n\\n### UIPreferences\\n\\nPer-user appearance settings persisted as JSONB in the `users.ui_preferences` column.\\n\\n```go\\ntype UIPreferences struct {\\n    Theme    string      // \\\"dark\\\" or \\\"light\\\"\\n    Surface  string      // \\\"glass\\\", \\\"matte\\\", etc.\\n    Corner   string      // \\\"rounded\\\", \\\"sharp\\\", etc.\\n    Button   string      // \\\"solid\\\", \\\"outline\\\", etc.\\n    Badge    string      // \\\"hex\\\", \\\"pill\\\", etc.\\n    Progress string      // \\\"hex\\\", \\\"bar\\\", etc.\\n    Density  string      // \\\"comfortable\\\", \\\"compact\\\", etc.\\n    Gradient string      // \\\"flat\\\", \\\"subtle\\\", etc.\\n    Accent   AccentColor // Custom accent color\\n}\\n\\ntype AccentColor struct {\\n    Hex  string  // e.g., \\\"#6c8cff\\\"\\n    RGB  string  // e.g., \\\"108,140,255\\\"\\n    Name string  // e.g., \\\"Nexus Blue\\\"\\n}\\n```\\n\\n**Default values** (from `DefaultUIPreferences()`):\\n- Theme: `\\\"dark\\\"`\\n- Surface: `\\\"glass\\\"`\\n- Corner: `\\\"rounded\\\"`\\n- Button: `\\\"solid\\\"`\\n- Badge: `\\\"hex\\\"`\\n- Progress: `\\\"hex\\\"`\\n- Density: `\\\"comfortable\\\"`\\n- Gradient: `\\\"flat\\\"`\\n- Accent: Nexus Blue (#6c8cff)\\n\\nPreferences are loaded from the database on each request and merged with defaults. Missing fields fall back to defaults; invalid theme values coerce to `\\\"dark\\\"`.\\n\\n### Context Injection\\n\\nUser info and preferences are stored in request context for template access:\\n\\n```go\\n// Store user display info\\nctx = ContextWithUserInfo(ctx, &amp;UserInfo{\\n    Name:     \\\"Alice Smith\\\",\\n    Email:    \\\"alice@example.com\\\",\\n    Initials: \\\"AS\\\",\\n    Role:     \\\"admin\\\",\\n})\\n\\n// Store UI preferences\\nctx = ContextWithUIPrefs(ctx, &amp;prefs)\\n```\\n\\nThe `InjectPrefsMiddleware` wraps the response writer with a `ContextWriter` so `Render()` can access the request context without changing the method signature.\\n\\n### HTMX Partial Detection\\n\\n```go\\nfunc IsHTMXContent(r *http.Request) bool {\\n    // Returns true only when BOTH conditions are met:\\n    // 1. HX-Request: true\\n    // 2. HX-Target: non-empty and not \\\"body\\\"\\n    \\n    if r.Header.Get(\\\"HX-Request\\\") != \\\"true\\\" {\\n        return false\\n    }\\n    target := r.Header.Get(\\\"HX-Target\\\")\\n    if target == \\\"\\\" || target == \\\"body\\\" {\\n        return false\\n    }\\n    return true\\n}\\n```\\n\\nA boosted plain `` with no explicit `hx-target` defaults to swapping the entire body. Returning content-only would wipe out the sidebar and nav, so the safer default is to return the full layout unless the caller explicitly specifies a target.\\n\\n## Template Functions\\n\\nThe module provides 50+ template helper functions registered in `parseTemplates()`. Key categories:\\n\\n### HTML Sanitization\\n\\n- `safeHTML(s string)` \u2014 Marks a string as trusted HTML (server-produced content only).\\n- `sanitizedHTML(s string)` \u2014 Runs user content through bluemonday's UGC policy before marking safe. Use for KB articles, ticket resolutions, etc.\\n- `mdToHTML(s string)` \u2014 Minimal markdown converter for AI-produced narrative content. Supports `##`/`###` headings, bullets, `**bold**`, simple `|tables|`, and paragraphs. Output is sanitized before rendering.\\n\\n**Audit compliance:** The `kbSanitizer` (bluemonday UGC policy) is built once at package init and enforces the sanitize-on-read policy for user-writable HTML (audit finding H3, 2026-04-29). It permits formatting tags but strips ``, inline event handlers, and `javascript:`/`data:` URLs.\\n\\n### Formatting\\n\\n- `fmtSize(bytes int64)` \u2014 Formats bytes as \\\"1.2 MB\\\", \\\"3.4 KB\\\", or \\\"567 B\\\".\\n- `fmtMoney(v float64)` \u2014 Formats as \\\"$1,234.56\\\" with comma grouping.\\n- `fmtNum(v float64)` \u2014 Formats integers with commas, no decimals.\\n- `money(v float64)` \u2014 Alternative money formatter.\\n- `printf(format string, args...)` \u2014 `fmt.Sprintf` wrapper.\\n\\n### Math\\n\\n- `divf(a, b float64)` \u2014 Safe division (returns 0 if b == 0).\\n- `mul(a, b float64)` \u2014 Multiplication.\\n- `add(a, b int)` \u2014 Integer addition.\\n- `addf(a, b float64)` \u2014 Float addition.\\n- `sub(a, b float64)` \u2014 Subtraction.\\n- `mod(a, b int)` \u2014 Modulo (safe).\\n\\n### Collections &amp; Slicing\\n\\n- `list(args ...any)` \u2014 Builds a slice from variadic args.\\n- `mkslice(args ...interface{})` \u2014 Alias for `list`.\\n- `seq(n int)` \u2014 Returns `[1, 2, ..., n]`.\\n- `seq0(n int)` \u2014 Returns `[0, 1, ..., n-1]`.\\n- `slice(s string, indices ...int)` \u2014 String slicing with bounds checking.\\n- `dict(args ...any)` \u2014 Builds `map[string]any` from alternating key/value pairs. Used by templates passing named params to sub-templates.\\n\\n### JSON &amp; Data\\n\\n- `json(v interface{})` \u2014 Marshals to JSON, returns `\\\"[]\\\"` on error.\\n- `jsonArray(v any)` \u2014 Emits JSON array literal, returns `\\\"[]\\\"` for nil/empty.\\n- `safeJS(s string)` \u2014 Marks a string as trusted JavaScript.\\n\\n### Status &amp; Color Mapping\\n\\n- `statusBadge(status string)` \u2014 Returns Tailwind classes for status badges (active, paused, completed, etc.).\\n- `phaseLabel(phase int)` \u2014 Maps phase number to label (e.g., 0 \u2192 \\\"MSA &amp; Setup\\\").\\n- `phaseColor(phase int)` \u2014 Maps phase to color name (purple, blue, cyan, etc.).\\n- `maturityColor(v interface{})` \u2014 Maps 1-7 NCSR/CIS maturity level to color (red &lt;3, amber 3, light-green 4, green \u22655).\\n- `ncsrPhaseColor(phase string)` \u2014 Maps NCSR phase (\\\"do_now\\\", \\\"do_next\\\", \\\"do_later\\\") to hex color.\\n- `ncsrPhaseLabel(phase string)` \u2014 Maps NCSR phase to display label.\\n- `appCategoryColor(category interface{})` \u2014 Maps app category (productive, meeting, distracted, neutral, idle) to hex color.\\n\\n### EPA Module Helpers\\n\\n- `contactInitials(name string)` \u2014 Generates 2-letter initials from a name.\\n- `secondsToHours(secs int)` \u2014 Converts seconds to hours (float).\\n- `heatmapRows(blocks interface{})` \u2014 Reshapes heatmap blocks into 5 rows \u00d7 7 slots.\\n- `heatmapColor(level int)` \u2014 Maps heatmap intensity level to color.\\n\\n### Utility\\n\\n- `upper(s string)` \u2014 `strings.ToUpper`.\\n- `le(a, b int)` \u2014 Less-than-or-equal comparison.\\n- `now()` \u2014 Returns `time.Now()`.\\n- `deref(p *float64)` \u2014 Dereferences pointer, returns 0 if nil.\\n- `toFloat(v interface{})` \u2014 Type-safe float conversion.\\n- `moduleTitle(m string)` \u2014 Maps module code to display title (ncsr \u2192 \\\"NCSR\\\", etc.).\\n\\n## Static File Serving\\n\\n```go\\nfunc StaticHandler() http.Handler\\n```\\n\\nReturns an HTTP handler that serves embedded static files from `/static/` with appropriate cache headers:\\n- **JS/CSS:** `Cache-Control: public, max-age=60` (1 minute) \u2014 deploys propagate quickly\\n- **Images:** `Cache-Control: public, max-age=3600` (1 hour)\\n\\n## Error Handling\\n\\n```go\\ntype TemplateError struct {\\n    Name string\\n}\\n\\nfunc (e *TemplateError) Error() string {\\n    return \\\"template not found: \\\" + e.Name\\n}\\n```\\n\\nReturned by `Render()` and `RenderBlock()` when a template is not found. Handlers should check for this error and return a 404 or 500 as appropriate.\\n\\n## Markdown Rendering\\n\\nThe `renderSimpleMarkdown()` function is a tight, dependency-free converter for AI-produced narrative content. It supports:\\n\\n- **Headings:** `# `, `## `, `### ` (maps to `\n`, `\n`, `\n`)\\n- **Lists:** `- ` or `* ` (unordered lists)\\n- **Bold:** `**text**`\\n- **Tables:** Pipe-delimited with header separator row\\n- **Paragraphs:** Blank lines separate blocks\\n\\nAll output is HTML-escaped at the leaf level before tag-wrapping, then run through bluemonday at the caller. This ensures safe rendering of AI-generated content.\\n\\n## Integration Points\\n\\n### With Authentication (`internal/auth`)\\n\\n- `UserIDFromContext()` and `TenantIDFromContext()` extract user/tenant IDs for preference loading\\n- `ClaimsFromContext()` retrieves JWT claims (used indirectly via UserID extraction)\\n\\n### With Version (`internal/version`)\\n\\n- `version.Short()` and `version.String()` are auto-injected into all map data as `AppVersion` and `AppVersionFull`\\n\\n### With Database (`pgxpool`)\\n\\n- Preferences are queried from `users.ui_preferences` JSONB column\\n- Pool is optional; if not provided, defaults are used\\n\\n### With Handlers\\n\\nHandlers call `RenderWithRequest()` or `RenderWithContext()` with a data map:\\n\\n```go\\nfunc handleDashboard(w http.ResponseWriter, r *http.Request) {\\n    data := map[string]interface{}{\\n        \\\"Title\\\":    \\\"Dashboard\\\",\\n        \\\"Tickets\\\":  tickets,\\n        \\\"Metrics\\\":  metrics,\\n    }\\n    renderer.RenderWithRequest(w, r, \\\"dashboard/index.html\\\", data, IsHTMXContent(r))\\n}\\n```\\n\\nUser info, CSRF token, and UI preferences are auto-injected.\\n\\n## CSS Architecture\\n\\nThe module includes three main CSS files:\\n\\n### nexus.css\\nThe authoritative design system with named component classes (hex-card, btn, badge, etc.) and `--nx-*` design tokens. Self-contained, zero dependencies, no build step required. Tailwind was retired 2026-04-23.\\n\\n### helpdesk-glass.css\\nModule-specific styling for the helpdesk ticket interface. Uses scoped `--hd-glass-*` variables to avoid overriding base theme tokens. Includes:\\n- Light mode overrides via `[data-theme=\\\"light\\\"]` selector\\n- Cosmic depth field (decorative orbs and hex grid)\\n- Hex-glass primitives (cards, badges, progress bars)\\n- Neural metrics strip, ticket cards, detail panel, command palette\\n- Responsive breakpoints and `prefers-reduced-motion` support\\n\\n### helpdesk-peek.css &amp; epa.css\\nModule-specific styles for peek slide-in panels and EPA module, using token cascades for theme consistency.\\n\\n## Testing\\n\\nThe module includes regression tests in `render_v2_test.go` for the V2 ticket detail page. These tests verify that template rendering doesn't break on shape changes to data maps (e.g., the PR 7 bug where a literal `\\\"` inside an `x-data` attribute took prod down for ~10 minutes).\\n\\nExample:\\n```go\\nfunc TestV2DetailRender_PR8_MultipleCandidates(t *testing.T) {\\n    r := NewRenderer()\\n    data := map[string]any{\\n        \\\"Ticket\\\": pr8ticket{...},\\n        \\\"AssetCandidates\\\": []pr8candidate{...},\\n    }\\n    var buf bytes.Buffer\\n    if err := r.Render(&amp;buf, \\\"helpdesk/ticket_detail_v2.html\\\", data, true); err != nil {\\n        t.Fatalf(\\\"render error: %v\\\", err)\\n    }\\n    // Verify output contains expected elements\\n}\\n```\\n\\n## Usage Examples\\n\\n### Basic Page Render\\n\\n```go\\nfunc handleTickets(w http.ResponseWriter, r *http.Request) {\\n    data := map[string]interface{}{\\n        \\\"Tickets\\\": fetchTickets(),\\n    }\\n    renderer.RenderWithRequest(w, r, \\\"helpdesk/tickets.html\\\", data, IsHTMXContent(r))\\n}\\n```\\n\\n### HTMX Partial Update\\n\\n```go\\n// Template sends: hx-get=\\\"/api/tickets/123/details\\\" hx-target=\\\"detail-panel\\\"\\n// Handler:\\nfunc handleTicketDetail(w http.ResponseWriter, r *http.Request) {\\n    data := map[string]interface{}{\\n        \\\"Ticket\\\": fetchTicket(r.PathValue(\\\"id\\\")),\\n    }\\n    // IsHTMXContent returns true (HX-Request + HX-Target set)\\n    renderer.RenderWithRequest(w, r, \\\"helpdesk/ticket_detail.html\\\", data, IsHTMXContent(r))\\n    // Returns only the \\\"content\\\" block, not the full layout\\n}\\n```\\n\\n### Custom Block Rendering\\n\\n```go\\nfunc handleLogin(w http.ResponseWriter, r *http.Request) {\\n    data := map[string]interface{}{\\n        \\\"Error\\\": \\\"Invalid credentials\\\",\\n    }\\n    renderer.RenderBlock(w, \\\"auth/login.html\\\", \\\"login_form\\\", data)\\n}\\n```\\n\\n### Template with Sanitized User Content\\n\\n```html\\n\\n{{ .Article.Body | sanitizedHTML }}\\n\\n\\n```\\n\\n### Markdown from AI\\n\\n```html\\n\\n{{ .AIAnalysis | mdToHTML }}\\n\\n\\n```\",\"internal-version\":\"# internal \u2014 version\\n\\n# internal/version\\n\\nThe `version` package exposes the NexusOS PSA application's version information in multiple formats. It provides both human-readable and UI-optimized version strings, with support for build-time injection of commit SHA and timestamp.\\n\\n## Overview\\n\\nThis package serves as the single source of truth for application versioning. The semantic version is defined as a constant in source code, while build metadata (git commit and build time) is optionally injected at compile time via `-ldflags`. When build metadata is unavailable\u2014such as during `go run`\u2014the package falls back to Go's runtime build information.\\n\\nThe package is consumed by:\\n- **CLI startup** (`cmd/psa/main.go`) \u2014 logs full version details on launch\\n- **UI rendering** (`internal/ui/render.go`) \u2014 displays version badges in dashboard and report views\\n\\n## Constants and Variables\\n\\n### Version\\n```go\\nconst Version = \\\"1.7.11\\\"\\n```\\nThe semantic version (MAJOR.MINOR.PATCH) of the application. Bump this constant on each release.\\n\\n### Commit\\n```go\\nvar Commit = \\\"\\\"\\n```\\nThe short git SHA (7 characters), injected at build time. Set via:\\n```bash\\n-ldflags \\\"-X nexusOS/internal/version.Commit=$(git rev-parse --short HEAD)\\\"\\n```\\nIf empty at runtime, `String()` and `Short()` will attempt to extract it from Go's build info.\\n\\n### BuildTime\\n```go\\nvar BuildTime = \\\"\\\"\\n```\\nThe build timestamp in RFC3339 format, injected at build time. Set via:\\n```bash\\n-ldflags \\\"-X nexusOS/internal/version.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\"\\n```\\nIf empty at runtime, both `String()` and `Short()` will attempt to extract it from Go's build info.\\n\\n## Functions\\n\\n### String() \u2192 string\\n\\nReturns the full version string in the format:\\n```\\nv1.7.11 (abc1234, 2026-04-29T15:00:00Z)\\n```\\n\\n**Behavior:**\\n- Always includes the semantic version prefixed with `v`\\n- Appends commit SHA and build time in parentheses if either is available\\n- Falls back to `runtime/debug.ReadBuildInfo()` when `Commit` or `BuildTime` are empty\\n- Gracefully handles missing metadata\u2014omits empty fields\\n\\n**Usage:**\\n- Logged at application startup\\n- Rendered in detailed version displays (e.g., help text, error reports)\\n- Called by `handleDetail`, `handleDashboard`, and `handleReport` handlers\\n\\n### Short() \u2192 string\\n\\nReturns a compact version string optimized for UI badges:\\n```\\nv1.7.11 \u00b7 Apr 29\\n```\\n\\n**Behavior:**\\n- Includes only the semantic version and short date (month and day)\\n- Omits commit SHA for brevity\\n- Parses `BuildTime` (RFC3339) and formats as `Jan 2`\\n- Falls back to `runtime/debug.ReadBuildInfo()` when `BuildTime` is empty\\n- Returns just the version if no timestamp is available\\n\\n**Usage:**\\n- Rendered in dashboard and report UI badges\\n- Called by `Render()` in the UI layer during request handling\\n\\n## Build Integration\\n\\n### Development (go run)\\n\\nWhen running with `go run`, neither `Commit` nor `BuildTime` are set. The package automatically extracts build metadata from Go's embedded build info:\\n\\n```bash\\n$ go run ./cmd/psa\\n# Logs: v1.7.11 (abc1234, 2026-04-29T15:00:00Z)\\n```\\n\\n### Production (go build with ldflags)\\n\\nInject metadata at build time:\\n\\n```bash\\ngo build \\\\\\n  -ldflags \\\"-X nexusOS/internal/version.Commit=$(git rev-parse --short HEAD) \\\\\\n            -X nexusOS/internal/version.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)\\\" \\\\\\n  ./cmd/psa\\n```\\n\\nThis ensures `Commit` and `BuildTime` are populated immediately, avoiding runtime fallback overhead.\\n\\n## Fallback Mechanism\\n\\nThe package implements a two-tier approach to version metadata:\\n\\n1. **Primary:** Use injected `Commit` and `BuildTime` variables (fastest, no runtime overhead)\\n2. **Fallback:** Query `runtime/debug.ReadBuildInfo()` for `vcs.revision` and `vcs.time` settings\\n\\nThis design ensures version information is always available, whether the binary was built with explicit ldflags or via standard `go build`.\\n\\n## Integration Points\\n\\n```mermaid\\ngraph LR\\n    A[\\\"cmd/psa/main.go\\\"] --&gt;|String| B[\\\"version.String\\\"]\\n    C[\\\"internal/ui/render.go\\\"] --&gt;|String| B\\n    C --&gt;|Short| D[\\\"version.Short\\\"]\\n    E[\\\"handleDashboard\\\"] --&gt; C\\n    F[\\\"handleReport\\\"] --&gt; C\\n    G[\\\"handleDetail\\\"] --&gt; C\\n```\\n\\nThe version package is lightweight and dependency-free, making it safe to import across the codebase without circular dependencies.\",\"internal-voip\":\"# internal \u2014 voip\\n\\n# VoIP Module\\n\\nThe VoIP module manages PBX integration, call logging, and AI-powered call handling for multi-tenant systems. It provides HTTP handlers for configuration management, knowledge base administration, call history retrieval, and secure webhook ingestion from PBX systems.\\n\\n## Overview\\n\\nThis module bridges your application with external PBX (Private Branch Exchange) systems, enabling:\\n\\n- **Configuration management**: Store and update PBX connection details, API credentials, and AI settings per tenant\\n- **Knowledge base**: Maintain FAQ/context entries that AI uses during call handling\\n- **Call logging**: Track inbound/outbound calls with transcripts, intent detection, and AI analysis\\n- **Webhook ingestion**: Securely receive call events from PBX systems with HMAC-SHA256 signature verification\\n\\nAll operations are tenant-isolated and require valid authentication context.\\n\\n## Core Types\\n\\n### VoIPConfig\\n\\nHolds PBX connection and AI settings for a single tenant:\\n\\n```go\\ntype VoIPConfig struct {\\n    ID              string  // Config record ID\\n    TenantID        string  // Tenant UUID\\n    PBXType         string  // PBX system type (e.g., \\\"asterisk\\\", \\\"freepbx\\\")\\n    PBXApiURL       string  // PBX API endpoint\\n    PBXApiKey       string  // API authentication key\\n    PBXApiSecret    string  // API authentication secret\\n    SIPDomain       string  // SIP domain for this tenant\\n    WebhookSecret   string  // HMAC secret for webhook signature verification\\n    WebhookEnabled  bool    // Whether to accept webhooks\\n    AIAnswerEnabled bool    // Whether AI should answer calls\\n    AIGreeting      string  // Custom greeting for AI\\n    AITransferTimeout int   // Seconds before transferring to human\\n    AIModel         string  // LLM model identifier\\n    RedisURL        string  // Redis connection for caching\\n    CacheTTL        int     // Cache time-to-live in seconds\\n    IsActive        bool    // Whether this config is enabled\\n}\\n```\\n\\n### KnowledgeBaseEntry\\n\\nA single FAQ or context item for AI call handling:\\n\\n```go\\ntype KnowledgeBaseEntry struct {\\n    ID          string  // Entry ID\\n    TenantID    string  // Tenant UUID\\n    CompanyID   string  // Optional: company-specific entry\\n    Category    string  // Topic category\\n    Title       string  // Short title\\n    Content     string  // Full content/answer\\n    Priority    int     // Sort order (lower = higher priority)\\n    IsActive    bool    // Whether entry is used\\n    CompanyName string  // Joined from companies table\\n}\\n```\\n\\n### CallSession\\n\\nRepresents a single call with metadata and AI analysis:\\n\\n```go\\ntype CallSession struct {\\n    ID             string     // Call session ID\\n    TenantID       string     // Tenant UUID\\n    CallerNumber   string     // Caller's phone number\\n    CallerName     string     // Caller's name (if available)\\n    ContactID      string     // Linked contact record\\n    CompanyID      string     // Linked company record\\n    Direction      string     // \\\"inbound\\\" or \\\"outbound\\\"\\n    Status         string     // \\\"ringing\\\", \\\"answered\\\", \\\"completed\\\", etc.\\n    AnsweredBy     string     // \\\"ai\\\", \\\"human\\\", \\\"voicemail\\\"\\n    TransferredTo  string     // Extension/person if transferred\\n    Transcript     string     // Full call transcript\\n    DetectedIntent string     // AI-detected caller intent\\n    Sentiment      string     // \\\"positive\\\", \\\"negative\\\", \\\"neutral\\\"\\n    Resolution     string     // How the call was resolved\\n    AISummary      string     // AI-generated summary\\n    AICost         float64    // Cost of AI processing\\n    TicketID       string     // Linked support ticket\\n    StartedAt      time.Time  // Call start time\\n    AnsweredAt     *time.Time // When call was answered\\n    EndedAt        *time.Time // When call ended\\n    DurationSeconds int       // Total call duration\\n    WaitSeconds    int        // Time in queue\\n    PBXCallID      string     // PBX system's call ID\\n}\\n```\\n\\n## Handler\\n\\nThe `Handler` struct serves all VoIP endpoints:\\n\\n```go\\ntype Handler struct {\\n    pool     *pgxpool.Pool  // PostgreSQL connection pool\\n    renderer *ui.Renderer   // Template renderer for HTMX responses\\n}\\n```\\n\\nCreate a handler with `NewHandler(pool, renderer)` and register routes via `RegisterRoutes(mux)`.\\n\\n## Endpoints\\n\\n### Settings Page\\n\\n**`GET /settings/voip`**\\n\\nRenders the VoIP settings page with current configuration, knowledge base entries, and recent calls. Supports both full-page and HTMX partial rendering based on the `HX-Request` header.\\n\\nFlow:\\n1. Ensures a default config row exists for the tenant (upsert)\\n2. Loads `VoIPConfig` from database\\n3. Loads all `KnowledgeBaseEntry` records for the tenant\\n4. Loads 50 most recent `CallSession` records\\n5. Renders template with all data\\n\\n### Configuration API\\n\\n**`PUT /api/settings/voip/config`**\\n\\nUpdates PBX and AI settings. Accepts JSON body with all `VoIPConfig` fields. Uses PostgreSQL `ON CONFLICT` to insert or update atomically.\\n\\nAfter successful update, re-renders the settings page.\\n\\n### Knowledge Base API\\n\\n**`POST /api/settings/voip/knowledge`**\\n\\nCreates a new knowledge base entry. Request body:\\n\\n```json\\n{\\n  \\\"company_id\\\": \\\"optional-uuid\\\",\\n  \\\"category\\\": \\\"billing\\\",\\n  \\\"title\\\": \\\"How do I update my payment method?\\\",\\n  \\\"content\\\": \\\"...\\\",\\n  \\\"priority\\\": 1\\n}\\n```\\n\\n**`PUT /api/settings/voip/knowledge/{id}`**\\n\\nUpdates an existing entry. Includes `is_active` field to enable/disable without deletion.\\n\\n**`DELETE /api/settings/voip/knowledge/{id}`**\\n\\nRemoves an entry permanently.\\n\\nAll three endpoints re-render the settings page after modification.\\n\\n### Call Log API\\n\\n**`GET /api/voip/calls`**\\n\\nReturns JSON array of up to 50 most recent calls for the tenant, ordered by `started_at DESC`. Includes joined company names and full call metadata.\\n\\n### PBX Webhook\\n\\n**`POST /api/voip/webhook`**\\n\\nIngests call events from PBX systems with cryptographic verification. This is the only unauthenticated endpoint in the module \u2014 security relies entirely on HMAC-SHA256 signature verification.\\n\\n#### Request Headers\\n\\n| Header | Purpose |\\n|--------|---------|\\n| `X-Tenant-Id` | Tenant UUID (identifies which webhook_secret to use) |\\n| `X-Voip-Timestamp` | Unix seconds; must be within \u00b15 minutes of server time |\\n| `X-Voip-Signature` | Hex-encoded HMAC-SHA256(webhook_secret, timestamp + \\\".\\\" + body) |\\n\\n#### Verification Process\\n\\n1. **Header validation**: All three headers must be present\\n2. **Timestamp check**: Parsed as Unix seconds; rejected if outside \u00b15 minute window (prevents replay attacks)\\n3. **Body size limit**: Capped at 1 MiB (real PBX webhooks carry metadata only, not media)\\n4. **Database lookup**: Retrieves tenant's `webhook_secret` and `webhook_enabled` flag\\n5. **Signature verification**: Computes HMAC-SHA256 over `\\\".\\\"` and compares with constant-time comparison\\n6. **Processing**: On success, logs acceptance and processes the event (TODO: parse PBX payload, create/update call_session, trigger HUD push)\\n\\n#### Security Notes\\n\\n- **No tenant enumeration**: All verification failures return 401 with no body, regardless of whether the tenant exists, webhook is disabled, or signature is wrong\\n- **No plaintext logging**: The request body is never logged, preventing PII (caller IDs, recording URLs) from appearing in journalctl\\n- **Constant-time comparison**: Uses `subtle.ConstantTimeCompare` to prevent timing attacks on the signature\\n- **Timestamp in signature**: The timestamp is part of the signed material, so an attacker cannot replay an old signature with a fresh timestamp\\n\\n## Tenant Isolation\\n\\nAll handlers extract the tenant ID from the request context via `auth.TenantIDFromContext()`. Database queries filter by `tenant_id` to ensure strict isolation \u2014 a tenant can only access their own configuration, knowledge base, and call logs.\\n\\n## Database Schema (Inferred)\\n\\nThe module assumes these tables:\\n\\n- **voip_config**: One row per tenant, stores PBX and AI settings\\n- **knowledge_base**: FAQ/context entries, optionally scoped to a company\\n- **call_sessions**: Call records with transcripts and AI analysis\\n- **companies**: Company records (joined for display names)\\n- **contacts**: Contact records (joined for call attribution)\\n\\n## Integration Points\\n\\n- **Authentication**: Depends on `auth.TenantIDFromContext()` to extract tenant from request context\\n- **Rendering**: Uses `ui.Renderer.Render()` for HTMX-aware template rendering\\n- **Database**: All operations use `pgxpool.Pool` for connection management\\n\\n## Error Handling\\n\\n- Database errors are logged and return 500 with a generic message\\n- JSON decode errors return 400\\n- Webhook verification failures return 401 with no body\\n- Missing required fields in API requests return 400\\n\\n## Future Work\\n\\nThe webhook handler includes a TODO comment: parse PBX-specific webhook payload, create/update `call_session`, and trigger HUD push. This is the integration point where incoming call events from the PBX system will be processed and made visible to users.\",\"internal-workorder\":\"# internal \u2014 workorder\\n\\n# Work Order Module\\n\\nThe work order module manages the complete lifecycle of field service jobs in NexusOS. Work orders are the construction/trades equivalent of IT tickets\u2014they track execution from mobilization through closeout, including crew assignments, daily field logs, punch lists, and client sign-off.\\n\\n## Overview\\n\\nA work order represents a discrete unit of work at a site. It captures:\\n- **Scope &amp; scheduling**: trade, type, phases, estimated vs. actual hours\\n- **Location &amp; logistics**: site address, building/floor, crew assignments\\n- **Financials**: contract amount, billing type, actual costs\\n- **Field execution**: daily logs, punch list items, crew check-in/out\\n- **Approval**: client sign-off before closeout\\n\\nThe module provides both UI pages (list, detail, form) and REST APIs for CRUD operations, crew management, phase advancement, and field reporting.\\n\\n## Architecture\\n\\n```mermaid\\ngraph TD\\n    Handler[\\\"Handler(HTTP routes)\\\"]\\n    DB[\\\"PostgreSQL(work_orders, crew, logs, punch)\\\"]\\n    Auth[\\\"Auth Context(tenant, user)\\\"]\\n    Renderer[\\\"UI Renderer(templates)\\\"]\\n    Orchestrator[\\\"Orchestrator Callbacks(OnWorkOrderCreated, etc.)\\\"]\\n    \\n    Handler --&gt;|Query/Exec| DB\\n    Handler --&gt;|Extract| Auth\\n    Handler --&gt;|Render| Renderer\\n    Handler --&gt;|Emit| Orchestrator\\n```\\n\\n### Handler\\n\\n`Handler` is the central HTTP request dispatcher. It:\\n- Registers all work order routes (pages and APIs)\\n- Extracts tenant and user context from requests\\n- Loads data from the database\\n- Renders templates or returns JSON\\n- Emits orchestrator callbacks for state changes\\n\\n```go\\ntype Handler struct {\\n    pool     *pgxpool.Pool\\n    renderer *ui.Renderer\\n    \\n    // Callbacks for external systems\\n    OnWorkOrderCreated      func(ctx context.Context, tenantID, woID, companyID, projectID string)\\n    OnWorkOrderPhaseChanged func(ctx context.Context, tenantID, woID, phase string)\\n    OnWorkOrderCompleted    func(ctx context.Context, tenantID, woID, companyID, projectID string)\\n}\\n```\\n\\nCallbacks are optional and fire asynchronously when work orders are created, phase-advanced, or completed. This allows other modules (billing, scheduling, notifications) to react without tight coupling.\\n\\n## Routes &amp; Endpoints\\n\\n### Pages (HTML)\\n\\n| Route | Handler | Purpose |\\n|-------|---------|---------|\\n| `GET /work-orders` | `handleList` | List all WOs with filters (trade, status) |\\n| `GET /work-orders/new` | `handleNewForm` | Form to create a new WO |\\n| `GET /work-orders/{id}` | `handleDetail` | Full WO detail with crew, logs, punch items |\\n| `GET /work-orders/{id}/edit` | `handleEditForm` | Form to edit WO fields |\\n\\n### CRUD APIs\\n\\n| Route | Handler | Purpose |\\n|-------|---------|---------|\\n| `POST /api/work-orders` | `handleCreate` | Create WO; auto-generates WO number |\\n| `PUT /api/work-orders/{id}` | `handleUpdate` | Update WO fields |\\n| `DELETE /api/work-orders/{id}` | `handleDelete` | Delete WO |\\n\\n### Phase &amp; Status\\n\\n| Route | Handler | Purpose |\\n|-------|---------|---------|\\n| `POST /api/work-orders/{id}/phase` | `handleAdvancePhase` | Move WO to next phase; updates status |\\n\\n### Crew Management\\n\\n| Route | Handler | Purpose |\\n|-------|---------|---------|\\n| `POST /api/work-orders/{id}/crew` | `handleAddCrew` | Assign tech to WO |\\n| `DELETE /api/work-orders/{id}/crew/{userId}` | `handleRemoveCrew` | Remove tech from WO |\\n| `POST /api/work-orders/{id}/crew/{userId}/checkin` | `handleCrewCheckin` | Tech checks in; records time |\\n| `POST /api/work-orders/{id}/crew/{userId}/checkout` | `handleCrewCheckout` | Tech checks out; calculates hours &amp; costs |\\n\\n### Daily Logs\\n\\n| Route | Handler | Purpose |\\n|-------|---------|---------|\\n| `POST /api/work-orders/{id}/daily-log` | `handleSubmitDailyLog` | Submit/update daily field report |\\n| `GET /api/work-orders/{id}/daily-logs` | `handleListDailyLogs` | Fetch all daily logs for WO |\\n\\n### Punch List\\n\\n| Route | Handler | Purpose |\\n|-------|---------|---------|\\n| `POST /api/work-orders/{id}/punch` | `handleAddPunch` | Add deficiency item |\\n| `POST /api/work-orders/{id}/punch/{punchId}/resolve` | `handleResolvePunch` | Mark punch item resolved |\\n\\n## Key Workflows\\n\\n### Creating a Work Order\\n\\n1. User navigates to `/work-orders/new` \u2192 `handleNewForm` renders form with company, project, user dropdowns\\n2. User submits form \u2192 `handleCreate` is called\\n3. Handler generates next WO number from `wo_sequences` table (e.g., \\\"WO-0042\\\")\\n4. Inserts row into `work_orders` table with all fields\\n5. Fires `OnWorkOrderCreated` callback asynchronously\\n6. Redirects to detail page\\n\\n**WO Number Generation:**\\n```go\\n// Lock the sequence row, read next_number, increment it\\nh.pool.QueryRow(r.Context(),\\n    \\\"SELECT next_number FROM wo_sequences WHERE tenant_id = $1 FOR UPDATE\\\", tenantID)\\n// Increment and use\\nwoNumber := fmt.Sprintf(\\\"%s%04d\\\", prefix, nextNum)\\n```\\n\\n### Crew Check-In / Check-Out\\n\\n**Check-In** (`handleCrewCheckin`):\\n- Sets `checked_in = true` and `checked_in_at = now()`\\n- Returns HTML badge \\\"Checked In\\\"\\n\\n**Check-Out** (`handleCrewCheckout`):\\n- Calculates `hours_actual` from `checked_in_at` to `now()`\\n- Looks up `bill_rate` and `cost_rate` from `employee_role_rates` table\\n- Creates a `work_time_entry` for unified time tracking and job costing\\n- Updates WO's `actual_hours` and `actual_cost` running totals\\n- Returns HTML badge \\\"Checked Out\\\"\\n\\nThis integrates crew time with the broader time-tracking and costing system.\\n\\n### Phase Advancement\\n\\n`handleAdvancePhase`:\\n- Updates `phase` field to new value\\n- Auto-updates `status` based on phase:\\n  - `phase = 'complete'` \u2192 `status = 'complete'`\\n  - `phase = 'cancelled'` \u2192 `status = 'cancelled'`\\n  - Otherwise \u2192 `status = 'in_progress'`\\n- Fires `OnWorkOrderPhaseChanged` callback\\n\\nPhases follow the construction lifecycle: mobilize \u2192 rough-in \u2192 trim-out \u2192 termination \u2192 testing \u2192 punch-list \u2192 closeout \u2192 complete.\\n\\n### Daily Logging\\n\\n`handleSubmitDailyLog`:\\n- Inserts or updates `wo_daily_logs` row (upsert on work_order_id + log_date)\\n- Captures weather, site conditions, work performed, crew count, total hours, delays, safety incidents, change orders, notes, tomorrow's plan\\n- Recalculates WO's `actual_hours` by summing all daily logs\\n\\nThis provides a field-level audit trail and supports project analytics.\\n\\n### Punch List Management\\n\\n`handleAddPunch`:\\n- Creates `wo_punch_items` row with description, location, severity, assigned-to user\\n- Severity levels: critical, major, minor, cosmetic\\n\\n`handleResolvePunch`:\\n- Marks item as resolved with timestamp and resolver ID\\n- Punch items remain visible for closeout review\\n\\n## Data Types\\n\\n### WorkOrder\\n\\nThe main entity with ~40 fields covering:\\n- **Identity**: id, tenant_id, wo_number\\n- **Scope**: title, description, trade, wo_type, priority\\n- **Scheduling**: scheduled_start, scheduled_end, estimated_hours, actual_hours\\n- **Location**: site_address, site_city, site_state, site_zip, building, floor\\n- **Financials**: billing_type, contract_amount, actual_cost\\n- **Relationships**: company_id, project_id, bid_id, survey_id, lead_tech_id\\n- **Approval**: client_signed, client_signed_by, client_signed_at\\n- **Metadata**: created_by, created_at, updated_at\\n\\nHelper methods provide labels and colors for UI rendering:\\n```go\\nfunc (wo WorkOrder) TradeLabel() string      // \\\"Structured Cabling\\\"\\nfunc (wo WorkOrder) PhaseLabel() string      // \\\"Trim-Out\\\"\\nfunc (wo WorkOrder) StatusColor() string     // Bootstrap color class\\n```\\n\\n### CrewAssignment\\n\\nRepresents a tech assigned to a work order:\\n- user_id, role (lead, installer, apprentice, helper, inspector, pm)\\n- hours_scheduled, hours_actual\\n- checked_in, checked_in_at, checked_out_at\\n\\n### DailyLog\\n\\nField report submitted once per day per work order:\\n- weather, site_conditions, work_performed, materials_used, equipment_used\\n- crew_count, total_hours, delays, safety_incidents, change_orders\\n- notes, tomorrow_plan\\n\\n### PunchItem\\n\\nDeficiency to fix before closeout:\\n- description, location, severity (critical, major, minor, cosmetic)\\n- status (open, in_progress, resolved, accepted, deferred)\\n- photo_path, assigned_to, resolved_at\\n\\n## Enumerations\\n\\n### Trades\\n\\n10 trade types with color codes for UI:\\n- General, Structured Cabling, Access Control, Fire Alarm, CCTV, Network, Audio/Visual, Electrical, HVAC, Low Voltage\\n\\n### WO Types\\n\\n8 work order types:\\n- Install, Service Call, Warranty, Inspection, Maintenance, Emergency, Retrofit, Upgrade\\n\\n### Phases\\n\\n9 construction phases with colors:\\n- Mobilize, Rough-In, Trim-Out, Termination, Testing, Punch List, Closeout, Complete, Cancelled\\n\\n### Statuses\\n\\n9 statuses:\\n- Draft, Scheduled, In Progress, On Hold, Pending Inspection, Punch List, Complete, Cancelled, Invoiced\\n\\n## Data Loading\\n\\nThe handler provides several internal data loaders used by page handlers:\\n\\n```go\\nloadWorkOrder(ctx, tenantID, id) *WorkOrder\\nloadCrew(ctx, tenantID, woID) []CrewAssignment\\nloadDailyLogs(ctx, tenantID, woID) []DailyLog\\nloadPunchItems(ctx, tenantID, woID) []PunchItem\\nloadCompanies(ctx, tenantID) []optItem\\nloadProjects(ctx, tenantID) []optItem\\nloadUsers(ctx, tenantID) []optItem\\n```\\n\\nThese are called by page handlers to populate template data. They execute SQL queries and return slices of domain objects.\\n\\n## Integration Points\\n\\n### Authentication\\n\\nAll handlers extract tenant and user context via `auth.TenantIDFromContext()` and `auth.UserIDFromContext()`. This ensures multi-tenancy isolation and audit trails.\\n\\n### UI Rendering\\n\\nThe handler uses `ui.Renderer` to render HTML templates. It passes HTMX request headers to determine whether to return full pages or fragments.\\n\\n### Orchestrator Callbacks\\n\\nThree optional callbacks allow external modules to react to work order events:\\n\\n- **OnWorkOrderCreated**: Fired after WO is inserted. Used by billing/scheduling to initialize related records.\\n- **OnWorkOrderPhaseChanged**: Fired when phase advances. Used by notifications or workflow engines.\\n- **OnWorkOrderCompleted**: Fired when WO reaches complete status. Used by invoicing or project closure.\\n\\nCallbacks are invoked asynchronously (via `go`) to avoid blocking the HTTP response.\\n\\n### Time Tracking &amp; Costing\\n\\nWhen crew checks out, the handler:\\n1. Calculates hours worked\\n2. Looks up bill and cost rates from `employee_role_rates`\\n3. Creates a `work_time_entry` for unified time tracking\\n4. Updates WO's `actual_hours` and `actual_cost` totals\\n\\nThis integrates with the broader time-tracking and job-costing system.\\n\\n## Error Handling\\n\\nThe module logs errors to stdout and returns HTTP error responses:\\n- `500 Internal Server Error` for database failures\\n- `404 Not Found` for missing work orders\\n- `400 Bad Request` for invalid input (implicit via form parsing)\\n\\nErrors are logged with context (e.g., \\\"wo list error: %v\\\") for debugging.\\n\\n## Multi-Tenancy\\n\\nAll queries filter by `tenant_id` extracted from the request context. The `wo_sequences` table is also tenant-scoped, ensuring WO numbers don't collide across tenants.\\n\\n## Contributing\\n\\nWhen adding new work order features:\\n\\n1. **Add routes** in `RegisterRoutes()` following the pattern `METHOD /path`\\n2. **Implement handler** that extracts context, validates input, executes SQL, and responds\\n3. **Add data types** in `types.go` if needed\\n4. **Emit callbacks** for state changes that other modules should know about\\n5. **Test multi-tenancy** by verifying tenant_id filters in queries\\n\\nKeep handlers focused on HTTP concerns (parsing, rendering, redirecting). Move business logic to helper methods or separate packages if it grows complex.\",\"mockups-mockups\":\"# mockups \u2014 mockups\\n\\n# Mockups Module\\n\\n## Overview\\n\\nThe **mockups** module is a collection of HTML wireframes and interactive prototypes that visualize key user-facing surfaces across the NexusOS platform. These are **static HTML files** designed for design review, stakeholder communication, and developer reference\u2014not production code.\\n\\nEach mockup represents a distinct feature area or workflow, rendered with a consistent dark-theme design system and annotated with implementation notes.\\n\\n## Purpose\\n\\n- **Design communication**: Show stakeholders and team members what features will look like before engineering begins\\n- **Developer reference**: Provide visual specs and interaction patterns for frontend implementation\\n- **Feature documentation**: Capture the intended UX for compliance, distributor, and assessment workflows\\n- **Accessibility baseline**: Establish color, spacing, and typography standards across the platform\\n\\n## Design System\\n\\nAll mockups use a shared CSS variable palette and component library:\\n\\n```css\\n--nx-bg: #0b1220              /* Page background */\\n--nx-surface-1: #111a2c       /* Card backgrounds */\\n--nx-surface-2: #16223a       /* Secondary surfaces */\\n--nx-border: #233148          /* Border color */\\n--nx-text-1: #e6edf7          /* Primary text */\\n--nx-text-2: #b6c2d4          /* Secondary text */\\n--nx-text-muted: #7a8aa3      /* Muted text */\\n--nx-accent: #63b3ed          /* Interactive elements */\\n--nx-success: #48bb78         /* Success states */\\n--nx-warning: #ed8936         /* Warning states */\\n--nx-danger: #e53e3e          /* Error states */\\n```\\n\\nCommon components include:\\n- **Cards** (`.nx-card`): Rounded containers with subtle borders\\n- **Pills** (`.pill`): Status badges with semantic colors\\n- **Tables**: Collapsible rows, group headers, hover states\\n- **Buttons**: Primary, ghost, and danger variants\\n- **Stats blocks**: Large numbers with labels and context\\n\\n## Mockup Inventory\\n\\n### 1. **bid_drawing_page_selection.html**\\n**Purpose**: Vision Takeoff page selection interface for construction bid drawings\\n\\n**Key sections**:\\n- Header with bid metadata (BID-19, 133 pages, source PDF)\\n- Stats row: total pages, selected for Vision, pruned by trade, estimated cost\\n- Discipline filter pills (CCTV, Access Control, etc.) with allowed/blocked tags\\n- Grouped table of drawing pages by discipline (A, E, T, LV, FA, M, P, S)\\n- Checkbox selection with status indicators (complete, processing, pending, skipped)\\n- Footer with selection summary and \\\"Run Vision on Selected\\\" action\\n\\n**Design notes**: Auto-prune warning banner explains that pages with no detectable sheet number stay included by default. Disabled rows (pruned trades) use reduced opacity. Status badges use semantic colors.\\n\\n---\\n\\n### 2. **crm_client_compliance_tab.html**\\n**Purpose**: CRM client detail page showing compliance frameworks, reviews, and risk register\\n\\n**Key sections**:\\n- Client header with active MSA pill\\n- Tab strip (Overview, Contacts, Deals, Tickets, Projects, Billing, Documents, Compliance)\\n- Quick stat strip: active frameworks, reviews on file, accepted risks, open tickets\\n- **Active Frameworks** grid: NIST CSF 2.0, HIPAA, CMMC L2 with maturity scores and action buttons\\n- **Completed Reviews** table: date, framework, participant, decisions, status, PDF binder download\\n- **Accepted Risk Register** table: control ref, decision type (Accept/Transfer/Defer), risk description, client reasoning, accepted by, source review\\n\\n**Design notes**: Risk register rows use color-coded pills (Accept=green, Transfer=blue, Defer=gray). Expiring acceptances flag with amber warning. Each review produces a downloadable PDF binder for audit defense.\\n\\n---\\n\\n### 3. **distributor_s1_vendor_settings.html**\\n**Purpose**: MCP server configuration for distributor integrations (PAX8, TD Synnex, Ingram, D&amp;H, Microsoft NCE)\\n\\n**Key sections**:\\n- MCP-Vendors card with OAuth status and health indicator\\n- Distributor list with connection status, product count, subscription count, last sync time\\n- Per-distributor actions: Refresh, Catalog, Subscriptions, View Log\\n- Pending distributor (Microsoft NCE) with \\\"Connect\\\" button to initiate OAuth\\n- Footer actions: Reauthorize, Edit, Test Connection, Remove\\n\\n**Design notes**: Distributor rows auto-detected from MCP `tools/list` response. Sync history tracked for audit. Vendor-side issues (e.g., Ingram CorrelationID warnings) flagged inline.\\n\\n---\\n\\n### 4. **distributor_s2_catalog_browser.html**\\n**Purpose**: Cross-distributor product search and price comparison\\n\\n**Key sections**:\\n- Search bar + vendor/distributor filters\\n- Active filter chips (e.g., \\\"Vendor: Microsoft \u2715\\\")\\n- Stats row: matched products, vendors, last sync, best price spread\\n- Expandable product rows showing:\\n  - Product name, MFG code, vendor, distributor count\\n  - Sell price (QBO canonical)\\n  - Distributor sources grid (PAX8, TDS, Ingram, D&amp;H) with best-price highlight\\n  - Actions: Add to Quote, Refresh All Sources, Link to Existing Product\\n  - AI match badge + token cost\\n\\n**Design notes**: Best-price source highlighted with green border. Stale pricing (&gt;7 days) flagged with amber warning. Expandable rows keep the list scannable.\\n\\n---\\n\\n### 5. **distributor_s3_client_subscriptions.html**\\n**Purpose**: Client subscriptions tab showing distributor-backed recurring charges\\n\\n**Key sections**:\\n- Client header with active status\\n- Tab strip (Overview, Contacts, Contracts, Subscriptions, Tickets, Projects, Compliance, Billing)\\n- Stats: active subs, linked to contract, pending (locked), monthly distributor cost\\n- **Lock banner**: Contract locked state explanation + \\\"Generate CCR\\\" button\\n- **Linked Subscriptions** table: product, distributor, qty, unit cost, renewal date, contract line, sync status\\n- **Pending \u2014 Locked Contract** table: subscriptions queued until CCR signed\\n- **Manually Excluded** table: test subs or non-billable items\\n\\n**Design notes**: Locked contract prevents new line additions until a Contract Change Request (CCR) is signed. Qty drift (e.g., \\\"\u2191 from 28\\\") auto-applies to existing lines. Pending rows use left border accent.\\n\\n---\\n\\n### 6. **distributor_s4_contract_distributor_lines.html**\\n**Purpose**: Contract detail showing distributor-backed line items and change audit trail\\n\\n**Key sections**:\\n- Contract header (MSA-2026-001, client, term, MRR, lock status)\\n- Lock banner explaining auto-apply behavior\\n- Tab strip (Overview, Lines, Distributor Lines, Change History, CCRs, Documents, Renewals)\\n- **Distributor-Backed Lines** table: sheet #, product, distributor, qty, unit cost, sell price, margin %, last sync\\n- **Pending CCR (Draft)** card: shows what will be added/removed, net monthly impact, action buttons\\n- **Change History** timeline: sync events, CCR applications, manual exclusions with timestamps and source tags\\n\\n**Design notes**: Timeline uses left-border color coding (green=sync, amber=CCR, red=manual). Each event shows before/after values. Contract briefly unlocks when CCR is applied, then re-locks.\\n\\n---\\n\\n### 7. **distributor_s5_product_catalog.html**\\n**Purpose**: QBO product catalog enhanced with distributor cost and margin visibility\\n\\n**Key sections**:\\n- Toolbar: product count, QBO sync time, re-sync buttons\\n- Info banner: QBO is canonical; distributor columns are read-only\\n- Product table with columns:\\n  - Product name + MFG code\\n  - Source of Truth (QBO badge)\\n  - Sell price (QBO)\\n  - Distributor sources (stacked: PAX8, TDS, Ingram, D&amp;H with prices and sync age)\\n  - Best cost (highlighted)\\n  - Margin %\\n  - Status\\n\\n**Design notes**: Best-price source highlighted with green border. Stale pricing (&gt;7 days) flagged. Internal labor and service items show \\\"\u2014\\\" for distributor columns. Margin color-coded (green=good, amber=mid, red=low).\\n\\n---\\n\\n### 8. **distributor_s6_invoice_distributor_cost.html**\\n**Purpose**: Invoice detail with read-only distributor cost and margin columns\\n\\n**Key sections**:\\n- Invoice header (INV-2026-04-0148, client, MSA, QBO sync status, total due)\\n- Stats row: invoice total, distributor cost, gross margin, QBO status\\n- Line items table with new columns (highlighted in teal):\\n  - Distributor (PAX8, TDS, D&amp;H)\\n  - Unit cost (snapshot at invoice creation)\\n  - Line cost\\n  - Margin %\\n- Footer: subtotal, distributor cost total, margin %\\n\\n**Design notes**: Distributor cost is a snapshot (not live). Margin computed client-side. QBO sees only existing line items unchanged. New columns are read-only for audit visibility.\\n\\n---\\n\\n### 9. **ncsr_action_plan_post_review.html**\\n**Purpose**: NIST CSF 2.0 (NCSR) action plan after live review with client decisions\\n\\n**Key sections**:\\n- Review progress card: 74% complete (32 of 43 items decided)\\n- Filter chips: All, Accepted, Modified, Declined, Pending, Do Now/Next/Later\\n- **Do Now** section: 12 actions with:\\n  - Control reference (GV.OC-01, PR.AA-05, etc.)\\n  - Action title and effort estimate\\n  - Decision pill (\u2713 Accepted, \u270e Modified, \u2717 Declined, \u23f8 Pending)\\n  - Maturity level change (ML 2 \u2192 4)\\n  - Client notes (amber box for modifications)\\n- **Do Next** section: 18 actions (same structure)\\n\\n**Design notes**: Left border color encodes decision state (green=accepted, amber=modified, red=declined, gray=pending). Modified actions inline-show client notes so PMs know what changed. Filter chips switch the list view.\\n\\n---\\n\\n### 10. **ncsr_assessment_full_page.html**\\n**Purpose**: Full NIST CSF 2.0 assessment dashboard with maturity trajectory and function scores\\n\\n**Key sections**:\\n- Sidebar nav (sketch): Composer, Frameworks, Live Review, Evidence Vault, Nexie AI\\n- Breadcrumbs + title + import metadata\\n- **Stepper**: Survey uploaded \u2192 Recommendations parsed \u2192 Action plan generated \u2192 View Action Plan\\n- **Trajectory card** (redesigned):\\n  - Curved rail showing current (2.8) vs. target (4.0) maturity\\n  - 7-phase scale (ML 1\u20137) with current and target highlighted\\n  - Stats: current overall, board target, maturity gap, below-target count\\n- **Function score row**: Overall + 6 functions (GV, ID, PR, DE, RS, RC) with maturity scores and gap counts\\n- **Heatmap**: Subcategory maturity by function (color-coded 1\u20137 scale)\\n- **Improvement areas** (3-column grid): Do Now, Do Next, Do Later with action items\\n- **Documents** section: downloadable assessment PDF, action plan, evidence binder\\n\\n**Design notes**: Trajectory card is the visual centerpiece\u2014curved rail with gradient shows the path from current to target. Function cards use semantic colors. Heatmap uses 4-tier color scale (red/orange/yellow/green). \\\"NEW \u00b7 redesigned\\\" annotation marks the trajectory card as a design update.\\n\\n---\\n\\n## Key Design Patterns\\n\\n### Status Indicators\\n- **Pills**: Semantic colors for state (\u2713 complete, \u27f3 processing, pending, skipped)\\n- **Left borders**: Timeline and action rows use colored left borders (green=sync, amber=warning, red=error)\\n- **Badges**: Inline status tags (\u25cf Active, \u25cf Connected, \u26a0 Vendor-side issue)\\n\\n### Tables\\n- **Group headers**: Uppercase, muted text, full-width background\\n- **Hover states**: Subtle background tint on row hover\\n- **Disabled rows**: Reduced opacity for pruned/excluded items\\n- **Sortable columns**: Uppercase headers with letter-spacing\\n\\n### Cards &amp; Containers\\n- **Gradient backgrounds**: Subtle radial gradients for depth (e.g., trajectory card)\\n- **Border colors**: Semantic (success=green, warning=amber, danger=red, neutral=gray)\\n- **Rounded corners**: 10\u201320px radius depending on component size\\n\\n### Data Visualization\\n- **Progress bars**: Gradient fill (green \u2192 blue) with glow effect\\n- **Heatmaps**: 4-tier color scale (red \u2192 orange \u2192 yellow \u2192 green)\\n- **Curved rails**: SVG paths for maturity trajectories\\n- **Stacked lists**: Distributor pricing shown as compact stacked rows\\n\\n---\\n\\n## Implementation Notes for Developers\\n\\n### HTML Structure\\n- All mockups use semantic HTML5 (`\n`, ``, ``, ``)\\n- No JavaScript\u2014purely static HTML + CSS\\n- Inline `` blocks for self-contained styling\\n\\n### CSS Approach\\n- **CSS variables** for theming (`:root` block at top of each file)\\n- **CSS Grid** for layouts (toolbar, stats row, function cards)\\n- **Flexbox** for alignment and spacing\\n- **No external dependencies**\u2014all styling is vanilla CSS\\n\\n### Responsive Design\\n- Media query at bottom of `ncsr_assessment_full_page.html` hides sidebar on mobile\\n- Most layouts use `grid-template-columns: repeat(auto-fit, minmax(...))` for flexibility\\n- Tables may need horizontal scroll on small screens (not implemented in mockups)\\n\\n### Color Semantics\\n- **Green** (#22c55e, #86efac): Success, accepted, complete\\n- **Amber** (#f59e0b, #fcd34d): Warning, modified, in-progress\\n- **Red** (#ef4444, #fca5a5): Error, declined, danger\\n- **Blue** (#6366f1, #a5b4fc): Primary, info, accent\\n- **Gray** (#64748b, #94a3b8): Muted, secondary, disabled\\n\\n### Typography\\n- **Headings**: 700\u2013800 weight, letter-spacing for uppercase\\n- **Body**: 13\u201314px, line-height 1.45\u20131.55\\n- **Monospace**: `ui-monospace, Menlo, Consolas` for SKUs, IDs, timestamps\\n- **Uppercase labels**: 10\u201311px, 0.16\u20130.18em letter-spacing\\n\\n---\\n\\n## Relationship to Codebase\\n\\nThese mockups are **design artifacts**, not executable code. They serve as:\\n\\n1. **Frontend specs**: Developers building React/Vue components reference these for layout, spacing, and interaction patterns\\n2. **Design system reference**: Color palette, typography, and component styles are extracted into actual CSS/design tokens\\n3. **Feature documentation**: Product managers and stakeholders use these to understand workflows before implementation\\n4. **Accessibility baseline**: Color contrast, focus states, and semantic HTML are modeled here\\n\\n**No direct code dependency**: The mockups are not imported or executed by the application. They live in version control as historical design records and reference material.\\n\\n---\\n\\n## File Organization\\n\\n```\\nmockups/\\n\u251c\u2500\u2500 bid_drawing_page_selection.html          (Vision Takeoff)\\n\u251c\u2500\u2500 crm_client_compliance_tab.html           (CRM Compliance)\\n\u251c\u2500\u2500 distributor_s1_vendor_settings.html      (MCP Config)\\n\u251c\u2500\u2500 distributor_s2_catalog_browser.html      (Product Search)\\n\u251c\u2500\u2500 distributor_s3_client_subscriptions.html (Client Subs)\\n\u251c\u2500\u2500 distributor_s4_contract_distributor_lines.html (Contract Lines)\\n\u251c\u2500\u2500 distributor_s5_product_catalog.html      (QBO Catalog)\\n\u251c\u2500\u2500 distributor_s6_invoice_distributor_cost.html (Invoice Margin)\\n\u251c\u2500\u2500 ncsr_action_plan_post_review.html        (Action Plan)\\n\u2514\u2500\u2500 ncsr_assessment_full_page.html           (Assessment Dashboard)\\n```\\n\\nEach file is **self-contained**: all CSS, HTML, and structure needed to render the mockup is in a single file. No external stylesheets or scripts.\\n\\n---\\n\\n## Design Highlights\\n\\n### Distributor Ingest Workflow (S1\u2013S6)\\nThe six distributor mockups form a coherent workflow:\\n1. **S1**: Configure MCP servers and authorize distributors\\n2. **S2**: Search and compare products across distributors\\n3. **S3**: View client subscriptions linked to contracts\\n4. **S4**: Manage contract lines and track changes via CCRs\\n5. **S5**: View product catalog with distributor cost visibility\\n6. **S6**: See margin on invoices (read-only, QBO-canonical)\\n\\n### Compliance Workflows\\n- **CRM Compliance Tab**: Single pane for frameworks, reviews, and risk register\\n- **NCSR Assessment**: Full dashboard with trajectory, function scores, and heatmap\\n- **Action Plan**: Post-review decisions with client notes and effort estimates\\n\\n### Visual Hierarchy\\n- **Large numbers** (maturity scores, page counts) use 22\u201342px font weight 800\\n- **Section headings** use uppercase with letter-spacing for scannability\\n- **Muted text** (#7a8aa3, #94a3b8) for secondary info and timestamps\\n- **Color accents** (green, amber, red) guide the eye to important states\",\"mockups-nexusiq\":\"# mockups \u2014 nexusiq\\n\\n# NexusIQ Module Documentation\\n\\n## Overview\\n\\nNexusIQ is a business intelligence and financial forecasting system designed for MSPs (Managed Service Providers). It provides role-based workspaces for strategic planning, sales management, and real-time team activity tracking. The module bridges the gap between high-level business goals (set by owners) and day-to-day execution (tracked by sales teams), with results flowing back into the broader system via Pulse SMART numbers.\\n\\n**Core purpose:** Enable owners to set cascading financial goals, model business scenarios, and monitor sales team performance against those goals in real time.\\n\\n---\\n\\n## Architecture &amp; Role-Based Access Control\\n\\nNexusIQ implements strict RBAC with four distinct workspaces:\\n\\n| Workspace | Audience | Permission | Purpose |\\n|-----------|----------|-----------|---------|\\n| **Plan Cascade** | Owner only | `nexusiq.plan.view` | Set vision \u2192 net worth \u2192 income \u2192 MRR targets; track SMART numbers |\\n| **Lever Lab** | Owner only | `nexusiq.lever.view` | What-if modeling; adjust 22 levers; see 24-month projections |\\n| **Sales Workspace** | Sales team + Owner | `nexusiq.workspace.use` | Daily deal management; timer; transcript import; quote generation |\\n| **Sales Cockpit** | Owner + Sales Manager | `nexusiq.cockpit.view` | Read-only roll-up of team activity; PBR kanban; Cookbook; YTD heatmap |\\n\\n**Default permission grants:**\\n- `workspace.use` \u2192 anyone with `crm.deals.write`\\n- `plan.view`, `lever.view`, `cockpit.view` \u2192 owner/manager tier only\\n\\n---\\n\\n## Core Data Model\\n\\n### Sales Goals (Per-User Targets)\\n\\n```sql\\nsales_goals (\\n  id              uuid pk,\\n  tenant_id       uuid fk,\\n  user_id         uuid fk users(id),     -- the sales rep\\n  period_start    date,\\n  period_end      date,\\n  mrr_target      numeric(14,2) default 0,\\n  orr_target      numeric(14,2) default 0,\\n  nrr_target      numeric(14,2) default 0,\\n  commission_target numeric(14,2) default 0,\\n  notes           text,\\n  assigned_by     uuid fk users(id),     -- owner who set it\\n  created_at, updated_at\\n)\\n```\\n\\nGoals are assigned in **Plan Cascade** (owner-only \\\"Team Goals\\\" section) and rendered in **Sales Workspace** as \\\"My Goals\\\" panel (matching `current_user + today between period_start/end`).\\n\\n### Commission Tracking\\n\\nExtends existing `bid_sheets` table:\\n\\n1. **New column:** `bid_sheets.sales_rep_id uuid REFERENCES users(id)`\\n   - Explicit commission recipient (distinct from `prepared_by` estimator)\\n   - Defaults to `prepared_by` on insert, but separately editable\\n\\n2. **Optional Phase 2:** `bid_commission_splits` table for multi-rep deals\\n   ```sql\\n   bid_commission_splits (\\n     bid_sheet_id uuid fk,\\n     user_id      uuid fk,\\n     pct          numeric(5,2),  -- must sum to 100 per bid\\n     PRIMARY KEY (bid_sheet_id, user_id)\\n   )\\n   ```\\n\\n3. **Materialized view:** `v_user_commissions`\\n   ```sql\\n   SELECT\\n     bs.sales_rep_id AS user_id,\\n     bs.tenant_id,\\n     bs.status,                    -- won / submitted / draft / lost\\n     bs.won_at,\\n     bs.submitted_at,\\n     bs.commission_amount,\\n     d.probability                 -- from deals(id) for forecast weighting\\n   FROM bid_sheets bs\\n   LEFT JOIN deals d ON d.id = bs.deal_id\\n   WHERE bs.sales_rep_id IS NOT NULL;\\n   ```\\n\\n   Rolls up:\\n   - **Earned** = sum(commission_amount) where status='won' AND won_at in period\\n   - **Forecast** = sum(commission_amount \u00d7 deal.probability) where status in ('draft','submitted','pending_review')\\n\\n---\\n\\n## Unified Time Tracking Integration\\n\\n**Hard requirement:** Sales Workspace timer writes to the existing unified `work_timers` / `work_time_entries` schema, NOT a parallel sales-only table.\\n\\n### Required Migration\\n\\n```sql\\nALTER TABLE work_timers       ADD COLUMN deal_id UUID REFERENCES deals(id) ON DELETE SET NULL;\\nALTER TABLE work_timers       ADD COLUMN sales_category TEXT;  \\n-- 'discovery', 'follow_up', 'proposal', 'cold_outreach', 'admin', etc.\\n\\nALTER TABLE work_time_entries ADD COLUMN deal_id UUID REFERENCES deals(id) ON DELETE SET NULL;\\nALTER TABLE work_time_entries ADD COLUMN sales_category TEXT;\\n\\nCREATE INDEX idx_work_time_deal ON work_time_entries(deal_id) WHERE deal_id IS NOT NULL;\\n```\\n\\n**Behavior:**\\n- Sales Workspace timer Start/Pause/Stop hits the same `/timer/*` endpoints as helpdesk + projects\\n- `is_billable` defaults to `false` for sales-category entries (sales time is internal overhead)\\n- \\\"Time Today\\\" panel on Sales Workspace queries the same timesheet data as existing employee timesheet\\n- Existing Timesheet UI gets a third source-type: **Deals** (with deal name + sales category)\\n- Pulse SMART numbers depending on hours-per-rep (utilization, AISP, leverage) automatically include sales hours\\n\\n---\\n\\n## Goal Cascade Data Flow\\n\\n```\\nOwner sets goals (Plan Cascade UI)\\n     \u2193\\nsales_goals rows written, assigned_by = owner\\n     \u2193\\nSales Workspace reads sales_goals + v_user_commissions for current_user\\n     \u2193\\nPulse picks up aggregated team attainment as SMART numbers (e.g. NXP.SL.GLATT)\\n     \u2193\\nSales Cockpit reads same view \u2014 owner roll-up against team goals\\n```\\n\\n---\\n\\n## Workspace Details\\n\\n### 1. Plan Cascade\\n\\n**Owner-only strategic planning interface.**\\n\\n**Sections:**\\n- **Vision** \u2014 free-text personal vision statement\\n- **Liquid Net Worth Goal** \u2014 target net worth + life event costs + time horizon \u2192 annual savings goal\\n- **Annual Earnings Goal** \u2014 living expenses + savings goal \u2192 pre-tax income required\\n- **3-Year Business Targets** \u2014 net profit %, revenue required, recurring % \u2192 MRR target\\n- **SMART Numbers** \u2014 live pull from Pulse (MRR, AISP, net profit %, recurring %, rev/employee, rev/tech)\\n- **Top 5 Initiatives** \u2014 editable list with owner, due date, status, progress bar; can link to Projects/Bids for auto-progress\\n- **Plan Health Gut Check** \u2014 14-question self-assessment (inline 5-question summary or full form)\\n\\n**Gut Check scoring:**\\n- 14 questions across Strategy, Cascade, Team, Process, Numbers\\n- Likert 1\u20135 or Yes/Partial/No responses\\n- Score 0\u201370; tiers: Reactive (1\u201320), Building (21\u201340), Competent (41\u201360), Elite (61\u201370)\\n- Free-text blocker question feeds into Nexie recommendations\\n- Snapshot logged to audit trail on submission\\n\\n### 2. Lever Lab\\n\\n**What-if modeling playground for owners.**\\n\\n**Structure:**\\n- **Left rail:** Scenario list (Baseline read-only, editable scenarios, save/compare/promote)\\n- **Center:** 22 levers across 3 banks (Income, Expense, Operations)\\n  - Each lever: label + input field + slider\\n  - Real-time recompute on change\\n- **Right rail:** \u0394 panel (vs baseline) + cross-module effects + Nexie suggestion\\n\\n**Levers:**\\n- **Income (7):** MRR target, AISP, avg MRR/client, recurring %, NRR, close ratio, avg deal size\\n- **Expense (8):** Support HC, support cost/emp, centralized HC, tools, TAM HC, vCIO HC, pro services HC, other OpEx\\n- **Operations (7):** Seats managed, seats/TAM, clients/TAM, clients/vCIO, MRR/support tech, target NP %, utilization %\\n\\n**Output:**\\n- 24-month projection chart (revenue, costs, net profit)\\n- \u0394 panel shows 12-month deltas (MRR, annual revenue, net profit %, gross margin/seat, hires required, Rule of 40, etc.)\\n- Cross-module effects (CRM pipeline target, HR requisitions, Pulse baseline, Sales Cockpit activity goals)\\n- Nexie AI suggestion (cost: $0.0041 per run)\\n\\n### 3. Sales Workspace\\n\\n**Daily deal management surface for sales reps.**\\n\\n**Layout:**\\n- **Sticky goal strip** (full width) \u2014 rep name, MRR/NRR/activity targets with progress bars, earned + forecast commission\\n- **Deal summary** \u2014 company name, stage, source, MRR, suspect/prospect/quoted badge\\n- **Left column:** Account info (industry, employees, location, contract type)\\n- **Center column (expands):** PBR command grid (8 cells: recurring value, win %, expected close, last touch, next step, decision maker, compete, bid sheet) + working notes + recent activity\\n- **Right rail (sticky):** Timer (on-deal, with category), transcript drop zone, time-this-week summary\\n\\n**Key interactions:**\\n- Timer Start/Pause/Stop \u2192 writes to `work_timers` + `work_time_entries` with `deal_id` + `sales_category`\\n- Transcript drop \u2192 Nexie auto-fills SRS scorecard fields\\n- Quote generation \u2192 creates bid sheet, links to deal\\n- Deal stage/MRR/close-date changes \u2192 emit SSE events to Sales Cockpit\\n\\n### 4. Sales Cockpit\\n\\n**Manager/owner read-only roll-up of team sales activity.**\\n\\n**Sections:**\\n- **PBR Kanban** (3 columns: Prospects / Suspects / Closed) \u2014 cards show company, rep, MRR, next step + date color-coded\\n- **Cookbook** (7-day activity table) \u2014 calls, meetings, emails, demos per rep; activity score (A+/A/B/C)\\n- **Sales Time** (week-to-date) \u2014 total hours, on-deal hours, admin hours per rep\\n- **YTD Commission Heatmap** \u2014 12-month grid, rep \u00d7 month, color-coded by commission earned\\n\\n**Real-time updates via SSE:**\\n- Server-Sent Events over authenticated `/nexusiq/cockpit/stream` endpoint\\n- Event types: `deal.created`, `deal.updated`, `deal.bucket_moved`, `deal.won`, `quote.generated`, `time.logged`, `cookbook.activity`, `review.completed`\\n- Client patches DOM (no full re-render); LIVE pill timestamp updates\\n- Auto-reconnect with exponential backoff on disconnect\\n\\n**Hard rule:** Cockpit is read-only. Managers deep-link to Sales Workspace to edit deals.\\n\\n---\\n\\n## Trademark Cleanup (Required)\\n\\n**PLAUD is a trademarked third-party brand and MUST NOT appear in shipped code.**\\n\\nCleanup checklist:\\n- `internal/billing/plaud_handler.go` \u2192 `transcript_handler.go`\\n- Route `/quotes/import-plaud` \u2192 `/quotes/import-transcript`\\n- All `plaud*` identifiers (struct fields, function names, variables) \u2192 `transcript*`\\n- Audit: `grep -ri plaud .` must return zero hits before release\\n\\nSame rule applies to all third-party trademarks (Hudu/Autotask/Zomentum OK only as truthful integration labels, never as default feature naming).\\n\\n---\\n\\n## Integration Points\\n\\n### With Pulse (SMART Numbers)\\n\\nPlan Cascade reads live SMART numbers from Pulse:\\n- MRR, AISP, net profit %, recurring %, rev/employee, rev/tech\\n- Targets are editable in Plan Cascade; baseline updates on save\\n\\nSales Cockpit activity (calls, meetings, time logged) feeds back into Pulse utilization/AISP/leverage calculations.\\n\\n### With CRM (Deals &amp; Opportunities)\\n\\n- Sales Workspace is the primary deal editor (extends existing CRM Opportunities)\\n- Deal stage, MRR, close date, next step changes emit SSE events\\n- `sales_rep_id` field added to deals (mirrors `bid_sheets.sales_rep_id`)\\n- Cockpit PBR kanban reads from deals table, filtered by close-date buckets (30/60/won)\\n\\n### With Billing (Bid Sheets)\\n\\n- `bid_sheets.sales_rep_id` \u2192 commission recipient\\n- `v_user_commissions` materializes earned + forecast commission per rep\\n- Sales Workspace displays bid sheet status (draft/approved/won)\\n\\n### With HR (Time Tracking)\\n\\n- Sales Workspace timer writes to unified `work_timers` / `work_time_entries`\\n- Existing Timesheet UI displays sales time alongside tickets/projects\\n- Utilization calculations include sales hours\\n\\n### With Projects (Initiatives)\\n\\n- Plan Cascade initiatives can link to Projects/Bids\\n- Progress auto-updates from milestone completion\\n\\n---\\n\\n## Mockup Files Reference\\n\\n| File | Purpose |\\n|------|---------|\\n| `ARCHITECTURE.md` | This document; schema, RBAC, data flow |\\n| `index.html` | Mockup index; links to all screens |\\n| `plan_cascade.html` | Vision \u2192 MRR cascade, SMART numbers, initiatives, gut check |\\n| `gut_check.html` | 14-question self-assessment form (standalone) |\\n| `lever_lab.html` | What-if modeling; 22 levers, 24-month projection, \u0394 panel |\\n| `sales_workspace.html` | Deal management; timer, transcript, PBR grid, time tracking |\\n| `sales_cockpit.html` | Team roll-up; PBR kanban, Cookbook, heatmap, SSE live updates |\\n| `proposal_v2_sales_cockpit.html` | Layout proposal: full-bleed, kanban instead of scroll, side-by-side Cookbook/Time |\\n| `proposal_v2_sales_workspace.html` | Layout proposal: full-bleed, sticky goal strip, expanded PBR grid, sticky right rail |\\n| `proposal_v3_pbr_dense_grid.html` | PBR as dense table (no kanban, no scroll); rep column sticky; stage cells show count + $ |\\n\\n---\\n\\n## Implementation Notes\\n\\n### Backend Handlers (Placeholder Locations)\\n\\n- **Timer endpoints** \u2192 extend `internal/timer/handler.go` to accept `deal_id` + `sales_category`\\n- **Sales goals CRUD** \u2192 new `internal/nexusiq/goals_handler.go`\\n- **Commission view** \u2192 new `internal/nexusiq/commission_handler.go` (queries `v_user_commissions`)\\n- **Cockpit SSE stream** \u2192 new `internal/nexusiq/cockpit_handler.go` (GET `/nexusiq/cockpit/stream`)\\n- **Gut check submission** \u2192 new `internal/nexusiq/gutcheck_handler.go` (POST, logs snapshot)\\n\\n### Frontend Routes\\n\\n- `/nexusiq/plan-cascade` \u2192 Plan Cascade page\\n- `/nexusiq/lever-lab` \u2192 Lever Lab page\\n- `/nexusiq/gut-check` \u2192 Gut Check form (standalone)\\n- `/nexusiq/sales-workspace` \u2192 Sales Workspace (deal view)\\n- `/nexusiq/sales-cockpit` \u2192 Sales Cockpit (manager roll-up)\\n- `/nexusiq/cockpit/stream` \u2192 SSE endpoint (authenticated, tenant-scoped)\\n\\n### Database Migrations\\n\\n1. **Add sales_goals table** (new)\\n2. **Extend work_timers / work_time_entries** (deal_id, sales_category columns + index)\\n3. **Extend bid_sheets** (sales_rep_id column)\\n4. **Create v_user_commissions view** (materialized or on-demand)\\n5. **Optional Phase 2:** bid_commission_splits table\\n\\n---\\n\\n## What Stays Untouched\\n\\n- Helpdesk / RMM (Support Techs)\\n- Operations (Ops folks)\\n- All other modules \u2014 they consume goals/attainment via Pulse SMART numbers, not directly\\n\\n---\\n\\n## Key Design Decisions\\n\\n1. **One unified time log, not parallel tables.** Sales hours flow into the same `work_time_entries` that powers employee timesheet and utilization calculations.\\n\\n2. **SSE for Cockpit, not WebSocket.** One-way server\u2192client, survives proxies, browser auto-reconnect built in. Cockpit is read-only; no bidirectional complexity needed.\\n\\n3. **Cockpit is read-only.** All writes happen in Sales Workspace. Managers deep-link to rep's workspace to edit.\\n\\n4. **Gut Check is a snapshot, not a live metric.** 14-question annual self-assessment; score logged to audit trail; feeds into Plan Health pills on Plan Cascade.\\n\\n5. **Levers are business-level, not rep-level.** Lever Lab is owner-only; reps see their individual goals in Sales Workspace, not the levers that drive them.\\n\\n6. **Commission splits are Phase 2.** MVP uses single `sales_rep_id` per bid; multi-rep deals handled in Phase 2 with `bid_commission_splits` table.\",\"mockups\":\"# mockups\\n\\n# Mockups Module\\n\\n## Overview\\n\\nThe **mockups** module is a collection of static HTML wireframes and interactive prototypes that visualize key user-facing surfaces across the NexusOS platform. These mockups serve as design references and communication artifacts for stakeholders and developers\u2014not production code.\\n\\n## Sub-Modules\\n\\n### [NexusIQ](nexusiq.md)\\n\\nThe primary mockup set for NexusIQ, a business intelligence and financial forecasting system for MSPs. It includes role-based workspace visualizations for:\\n\\n- **Owners**: Strategic planning, goal-setting, and scenario modeling\\n- **Sales managers**: Team performance tracking and pipeline management  \\n- **Sales reps**: Activity logging and personal quota progress\\n- **Admins**: System configuration and user management\\n\\nThe NexusIQ mockups demonstrate how cascading financial goals flow from strategic planning through to real-time sales execution, with results feeding back into Pulse SMART numbers.\\n\\n## Purpose\\n\\n- **Design communication**: Visualize features before engineering begins\\n- **Developer reference**: Provide visual specifications and interaction patterns for frontend implementation\\n- **Feature documentation**: Capture intended UX for compliance and distribution\\n\\n## Key Characteristics\\n\\nAll mockups in this module:\\n- Use a consistent dark-theme design system\\n- Are rendered as static HTML files\\n- Include implementation notes and annotations\\n- Represent distinct feature areas or workflows\\n- Do not contain cross-module dependencies or shared execution flows\\n\\nFor detailed specifications of individual mockup sets, see the [NexusIQ](nexusiq.md) documentation.\",\"overview\":\"# nexusos-psa \u2014 Wiki\\n\\n# NexusOS PSA\\n\\nWelcome to **NexusOS PSA** \u2014 a comprehensive Professional Services Automation platform built in Go. This is the central hub for understanding the system's architecture, modules, and development workflow.\\n\\n## What is NexusOS PSA?\\n\\nNexusOS PSA is an integrated business operations platform designed for managed service providers (MSPs) and professional services firms. It unifies project management, billing, CRM, compliance, security assessments, and business intelligence into a single multi-tenant application.\\n\\nThe platform handles:\\n- **Project &amp; Service Delivery** \u2014 work orders, timesheets, resource allocation\\n- **Financial Operations** \u2014 billing, invoicing, QuickBooks integration, procurement\\n- **Client Relationships** \u2014 CRM, helpdesk, portal, customer communications\\n- **Compliance &amp; Risk** \u2014 CMMC assessments, security reviews, regulatory evidence, audit trails\\n- **Business Intelligence** \u2014 forecasting, performance analytics, strategic planning via NexusIQ\\n- **Security &amp; Monitoring** \u2014 SIEM integration, vulnerability assessments, email security, RMM integration\\n\\n## Architecture Overview\\n\\nNexusOS is built as a modular monolith with clear separation of concerns. The [cmd](cmd.md) module bootstraps the PSA server, which initializes all internal services, database connections, and external integrations. Authentication flows through a centralized [auth](internal-auth.md) module that every other service depends on.\\n\\nHere's the high-level system topology:\\n\\n```mermaid\\ngraph TB\\n    Client[\\\"Client Applications(Web, Mobile, Extension)\\\"]\\n    \\n    CMD[\\\"cmd/psa(Server Entry Point)\\\"]\\n    Auth[\\\"internal/auth(JWT &amp; Session)\\\"]\\n    \\n    Core[\\\"Core Services(helpdesk, crm, project,billing, workorder)\\\"]\\n    Compliance[\\\"Compliance &amp; Risk(cmmc, assessment,review, legal)\\\"]\\n    Intelligence[\\\"Intelligence &amp; Analytics(nexiq, performance,bidengine, ai)\\\"]\\n    Integration[\\\"External Integrations(quickbooks, rmm,metasploit, siem)\\\"]\\n    \\n    DB[\\\"PostgreSQL(Multi-tenant)\\\"]\\n    \\n    Client --&gt;|HTTP/WebSocket| CMD\\n    CMD --&gt; Auth\\n    CMD --&gt; Core\\n    CMD --&gt; Compliance\\n    CMD --&gt; Intelligence\\n    CMD --&gt; Integration\\n    \\n    Core --&gt; Auth\\n    Compliance --&gt; Auth\\n    Intelligence --&gt; Auth\\n    Integration --&gt; Auth\\n    \\n    Core --&gt; DB\\n    Compliance --&gt; DB\\n    Intelligence --&gt; DB\\n    Integration --&gt; DB\\n```\\n\\n## Key Modules\\n\\n### Foundation &amp; Entry Points\\n- **[cmd](cmd.md)** \u2014 Server bootstrap, configuration loading, service initialization\\n- **[internal/auth](internal-auth.md)** \u2014 JWT token generation, session management, permission enforcement (depended on by 40+ modules)\\n\\n### Core Business Logic\\n- **[internal/helpdesk](internal-helpdesk.md)** \u2014 Ticket management, queue routing, SLA tracking\\n- **[internal/crm](internal-crm.md)** \u2014 Client accounts, contacts, opportunity pipeline\\n- **[internal/project](internal-project.md)** \u2014 Project planning, resource scheduling, delivery tracking\\n- **[internal/billing](internal-billing.md)** \u2014 Invoicing, time-based billing, expense tracking\\n- **[internal/workorder](internal-workorder.md)** \u2014 Work order creation, assignment, completion workflows\\n\\n### Compliance &amp; Legal\\n- **[internal/cmmc](internal-cmmc.md)** \u2014 CMMC assessment framework, practice mapping, evidence collection\\n- **[internal/assessment](internal-assessment.md)** \u2014 Security assessments, vulnerability scanning, risk scoring\\n- **[internal/review](internal-review.md)** \u2014 Document review workflows, signature capture, approval chains\\n- **[internal/legal](internal-legal.md)** \u2014 Contract generation, order documents, signature stamping\\n\\n### Intelligence &amp; Planning\\n- **[internal/nexiq](internal-nexiq.md)** \u2014 Business intelligence dashboards, financial forecasting, scenario modeling\\n- **[internal/performance](internal-performance.md)** \u2014 KPI tracking, team analytics, utilization reporting\\n- **[internal/bidengine](internal-bidengine.md)** \u2014 Proposal generation, pricing models, win probability\\n\\n### External Integrations\\n- **[internal/quickbooks](internal-quickbooks.md)** \u2014 QuickBooks Online sync, invoice export, expense import\\n- **[internal/rmm](internal-rmm.md)** \u2014 RMM platform integration, device monitoring, alert ingestion\\n- **[internal/metasploit](internal-metasploit.md)** \u2014 Penetration testing framework integration, vulnerability data\\n- **[internal/siem](internal-siem.md)** \u2014 Security event aggregation, alert correlation, incident response\\n\\n### UI &amp; Presentation\\n- **[internal/ui](internal-ui.md)** \u2014 HTTP handlers, template rendering, API endpoints for web and mobile clients\\n- **[extensions](extensions.md)** \u2014 Chrome extension for one-click product data capture and enrichment\\n\\n### Supporting Systems\\n- **[internal/tenant](internal-tenant.md)** \u2014 Multi-tenancy isolation, workspace management\\n- **[internal/database](internal-database.md)** \u2014 PostgreSQL connection pooling, migrations, query builders\\n- **[internal/ai](internal-ai.md)** \u2014 LLM integration (Nexie), classification, content generation\\n- **[internal/notify](internal-notify.md)** \u2014 Email, push notifications, alert delivery\\n- **[internal/sentinel](internal-sentinel.md)** \u2014 Real-time event streaming, WebSocket coordination\\n\\n### Documentation &amp; Tooling\\n- **[docs](docs.md)** \u2014 Architecture specs, compliance evidence, integration roadmaps, design artifacts\\n- **[scripts](scripts.md)** \u2014 Build utilities, changelog generation, security evidence compilation\\n- **[branding](branding.md)** \u2014 Brand guidelines, design system, visual assets\\n- **[mockups](mockups.md)** \u2014 UI wireframes and interactive prototypes\\n\\n## Execution Flow: Contract Signing\\n\\nOne representative end-to-end flow shows how the system coordinates across modules:\\n\\n1. **User initiates contract signature** via the [internal/review](internal-review.md) module\\n2. **Review handler** validates the session and retrieves the contract document\\n3. **Legal module** generates the order document with current data (pricing, coverage, terms)\\n4. **Signature stamper** injects the digital signature into the DOCX file, managing XML namespaces\\n5. **Database** persists the signed contract and audit trail\\n6. **Notification system** sends confirmation to client and internal stakeholders\\n\\nThis flow demonstrates the layered architecture: UI handlers \u2192 business logic \u2192 document generation \u2192 persistence \u2192 notifications.\\n\\n## Getting Started\\n\\n### Prerequisites\\n- **Go 1.25.0** or later\\n- **PostgreSQL 14+** (for multi-tenant data)\\n- **Docker** (for local development with testcontainers)\\n\\n### Build &amp; Run\\n\\n```bash\\n# Clone the repository\\ngit clone \\ncd nexusOS\\n\\n# Install dependencies\\ngo mod download\\n\\n# Build the PSA server\\ngo build -o psa ./cmd/psa\\n\\n# Run with environment configuration\\nexport DATABASE_URL=\\\"postgres://user:pass@localhost/nexusdb\\\"\\nexport JWT_SECRET=\\\"your-secret-key\\\"\\n./psa\\n```\\n\\nThe server starts on port 8080 by default and initializes all internal services, database migrations, and external integrations.\\n\\n### Development Workflow\\n\\n- **Code organization**: Each module in `internal/` is self-contained with handlers, repositories, types, and business logic\\n- **Testing**: Use [testcontainers](https://testcontainers.com/) for integration tests with real PostgreSQL instances\\n- **Documentation**: Update [docs](docs.md) alongside code changes; architecture decisions are captured there\\n- **Branching**: Follow the discipline outlined in the [Root](root.md) module\\n\\n## Key Dependencies\\n\\n- **github.com/jackc/pgx/v5** \u2014 PostgreSQL driver with connection pooling\\n- **github.com/golang-jwt/jwt/v5** \u2014 JWT token signing and validation\\n- **github.com/vmihailenco/msgpack/v5** \u2014 Efficient message serialization\\n- **github.com/xuri/excelize/v2** \u2014 Excel file generation for reports\\n- **github.com/ledongthuc/pdf** \u2014 PDF parsing for document processing\\n- **github.com/SherClockHolmes/webpush-go** \u2014 Web push notifications\\n- **github.com/pquerna/otp** \u2014 Two-factor authentication (TOTP)\\n\\n## Next Steps\\n\\n- **New to the codebase?** Start with [internal/auth](internal-auth.md) to understand how requests are authenticated, then explore a core module like [internal/helpdesk](internal-helpdesk.md)\\n- **Building a feature?** Check [docs/architecture.md](docs.md) for system design patterns and multi-tenancy guidelines\\n- **Integrating an external system?** Review [internal/quickbooks](internal-quickbooks.md) or [internal/rmm](internal-rmm.md) as reference implementations\\n- **Deploying?** See [Root](root.md) for CI/CD and deployment pipeline configuration\\n\\n---\\n\\n**Questions?** Refer to the [docs](docs.md) module for detailed specifications, or check individual module pages for implementation details.\",\"root\":\"# Root\\n\\n# Root Module\\n\\nThe Root module is the foundational configuration and entry point for NexusOS PSA. It establishes the project structure, build system, deployment pipeline, and development workflow that all other modules depend on.\\n\\n## Purpose\\n\\nThe Root module serves three critical functions:\\n\\n1. **Project Configuration** \u2014 defines how the application is built, tested, deployed, and versioned\\n2. **Development Workflow** \u2014 establishes branch discipline, code review, documentation, and code-intelligence procedures\\n3. **Operational Deployment** \u2014 specifies the production environment, systemd integration, and release process\\n\\nEvery developer and every CI/CD pipeline operates within the constraints and conventions defined here.\\n\\n## Key Components\\n\\n### Build System (`Makefile`)\\n\\nThe Makefile is the single source of truth for all build operations. It embeds version information (commit hash and build timestamp) via ldflags and provides these targets:\\n\\n| Target | Purpose |\\n|--------|---------|\\n| `make build` | Compile the Go binary with version metadata |\\n| `make run` | Build and execute locally |\\n| `make dev` | Live reload via `air` (if installed), falls back to `make run` |\\n| `make migrate` | Run database migrations |\\n| `make seed` | Populate demo data |\\n| `make test` | Run full test suite |\\n| `make docker` | Build Docker image |\\n| `make up` / `make down` | Docker Compose stack control |\\n| `make changelog` | Preview rendered `[Unreleased]` section |\\n| `make changelog-check` | Verify changelog fragment exists (CI gate) |\\n| `make changelog-release` | Inject fragments into CHANGELOG.md at release time |\\n| `make schema` | Regenerate `internal/database/schema.sql` from production |\\n| `make setup-hooks` | Activate `.githooks/` for this repo |\\n\\nThe build system is **locked** \u2014 no build-step tooling (webpack, Tailwind, Node) is permitted for the application itself. All CSS is native and embedded; all templates are server-rendered.\\n\\n### Docker &amp; Containerization\\n\\n**Dockerfile** \u2014 multi-stage build:\\n- **Builder stage:** compiles the Go binary with static linking (`CGO_ENABLED=0`) for `linux/amd64`\\n- **Runtime stage:** minimal Alpine image with only the binary, CA certificates, and timezone data\\n\\nAll templates, static assets, and migrations are embedded via Go's `embed.FS`, so no additional files are copied into the runtime image.\\n\\n**docker-compose.yml** \u2014 production-like stack:\\n- PostgreSQL 16 on Alpine (no exposed port; internal Docker network only)\\n- PSA service with health checks, volume mounts for keys and certs\\n- Named volume `pgdata` for persistence\\n\\n**docker-compose.dev.yml** \u2014 development overrides:\\n- Mounts source code as read-only\\n- Exposes PostgreSQL on port 5432 for local tooling\\n- Enables live reload\\n\\n### Deployment Pipeline\\n\\nProduction deployment is **manual and disciplined**:\\n\\n```bash\\n# 1. Merge PR via GitHub API (not gh pr merge, which can be blocked by stale rule-eval cache)\\ngh api -X PUT repos/Horizon-Managed/nexusos-psa/pulls//merge -f merge_method=squash\\n\\n# 2. Verify on main at tip\\ngit checkout main &amp;&amp; git pull --ff-only\\ngit rev-parse HEAD  # must equal origin/main\\n\\n# 3. Build and deploy to 96.76.137.179\\nGOOS=linux GOARCH=amd64 go build -o nexusos-psa ./cmd/psa/\\nscp nexusos-psa hzadmin@96.76.137.179:/tmp/nexusos-psa\\nssh hzadmin@96.76.137.179 'sudo cp /tmp/nexusos-psa /opt/nexusos/psa &amp;&amp; sudo chmod +x /opt/nexusos/psa &amp;&amp; sudo systemctl restart nexusos-psa'\\n```\\n\\n**Critical rule:** Never build from a feature branch. The next build from `main` silently drops the changes. Always merge to `main` first, verify you're at tip, then build.\\n\\n### Release Management\\n\\nReleases follow semantic versioning (patch for fixes, minor for features, major for breaking changes):\\n\\n```bash\\n# 1. Create release branch and promote [Unreleased]\\ngit checkout -b chore/release-vX.Y.Z origin/main\\ngo run ./scripts/changelog -clean\\n# Edit CHANGELOG.md: insert new ## [vX.Y.Z] - YYYY-MM-DD heading\\n\\n# 2. Commit, push, open PR (changelog-fragment CI check will be red; it's non-required)\\ngit commit -am \\\"chore(release): cut vX.Y.Z\\\"\\ngit push -u origin chore/release-vX.Y.Z\\n\\n# 3. After merge, tag and publish\\ngit checkout main &amp;&amp; git pull --ff-only\\ngit tag -a vX.Y.Z -m \\\"Release vX.Y.Z (YYYY-MM-DD)\\\" $(git rev-parse HEAD)\\ngit push origin vX.Y.Z\\ngh release create vX.Y.Z --title \\\"vX.Y.Z \u2014 \\\" --notes \\\"...\\\"\\n```\\n\\nVersion is sourced from `internal/version/version.go` (single source of truth).\\n\\n## Development Workflow\\n\\n### Daily Startup (Mandatory)\\n\\nBefore any work:\\n\\n```bash\\ngit fetch origin --prune\\ngit checkout main &amp;&amp; git pull --ff-only\\nnpx gitnexus analyze\\n```\\n\\nThis ensures you're synced with overnight merges and the code-intelligence index is fresh.\\n\\n### Branch &amp; PR Discipline\\n\\n- **Never commit directly to `main`.** Always branch + PR.\\n- **Branch naming:** `feat/`, `fix/`, `refactor/`, `chore/`, `docs/`, `extract/`, `split/`, `audit/`, `test/` + kebab-case\\n- **One concern per PR.** Don't bundle extraction + feature, refactor + file-split, etc.\\n- **Parallel PRs on the same file:** use sequential (merge first, then branch off new `origin/main`) or stacked (branch PR B off PR A's branch, rebase after each upstream merge). Never open 3+ PRs all branched off `main` against the same file \u2014 that guarantees a merge-conflict storm.\\n- **No `Co-Authored-By: Claude` footers** in commits or PRs.\\n\\n### Documentation (Mandatory on Every PR)\\n\\nEvery code change requires:\\n\\n1. **Changelog fragment** \u2014 one file under `changelog.d/..md` (categories: `added`, `changed`, `deprecated`, `removed`, `fixed`, `security`). Do NOT edit `CHANGELOG.md` directly. Verify locally with `make changelog`.\\n\\n2. **`docs/INVENTORY.md`** \u2014 update if a module under `internal/` was added, removed, or substantively changed its purpose. Mark **N/A** if nothing changed at module level.\\n\\n3. **`docs/DEPENDENCIES.md`** \u2014 update if internal import edges changed or third-party libraries were added/upgraded/removed. Record the reason.\\n\\nAll three land in the **same PR** as the code change. Fragment files have unique names, so git auto-merges parallel PRs without conflict.\\n\\n### Code-Intelligence Procedure (Mandatory on Every Edit)\\n\\nBefore editing any function, class, or method:\\n\\n```bash\\ngitnexus_impact({target: \\\"symbolName\\\", direction: \\\"upstream\\\"})\\n```\\n\\nReport the blast radius (direct callers, affected processes, risk level). **STOP** if the result is HIGH or CRITICAL risk.\\n\\nBefore committing:\\n\\n```bash\\ngitnexus_detect_changes()      # verify affected scope matches intent\\ngo test -short ./...            # fast suite, skips integration tests\\ngo vet ./...                     # catch common mistakes\\ngo run ./scripts/changelog -dry-run  # verify changelog fragment renders\\n```\\n\\nNever use find-and-replace for renames \u2014 use `gitnexus_rename`, which understands the call graph.\\n\\n## Configuration\\n\\nEnvironment variables (defaults work for local dev):\\n\\n| Variable | Default | Purpose |\\n|---|---|---|\\n| `DATABASE_URL` | `postgres://nexusos:nexusos-dev@localhost:5432/nexusos?sslmode=disable` | PostgreSQL connection |\\n| `LISTEN_ADDR` | `:3000` | HTTP server bind address |\\n| `JWT_PRIVATE_KEY_PATH` | `keys/jwt.pem` | RSA private key for JWT signing |\\n| `JWT_PUBLIC_KEY_PATH` | `keys/jwt.pub` | RSA public key for JWT verification |\\n| `RMM_LISTEN_ADDR` | `:8443` | mTLS agent listener |\\n| `RMM_CERT_PATH` / `RMM_KEY_PATH` / `RMM_CA_CERT_PATH` / `RMM_CA_KEY_PATH` | \u2014 | mTLS certs for RMM |\\n| `SIEM_TLS_CERT_PATH` / `SIEM_TLS_KEY_PATH` | \u2014 | TLS for syslog listener (port 6514) |\\n| `SIEM_ENCRYPTION_KEY` | \u2014 | 32-byte hex key for AES-256-GCM session export encryption |\\n| `DEV_MODE` | \u2014 | Set `true` to auto-auth as first user. **Never in production.** |\\n| `PUBLIC_URL` | \u2014 | Canonical public origin (e.g. `https://nexus.horizonmanaged.com`). Required behind reverse proxies that drop `X-Forwarded-Host`. |\\n\\n## Tech Stack (Locked)\\n\\n- **Language:** Go (standard `net/http`, `pgx/v5` for Postgres, single binary)\\n- **Frontend:** server-rendered HTML templates + Alpine.js + HTMX\\n- **Styling:** `nexus.css` \u2014 native CSS, zero third-party runtime deps. No build step. (Tailwind retired 2026-04-23.)\\n- **Database:** PostgreSQL with raw SQL migrations (no ORM)\\n\\n**Forbidden:** React, Vue, Vite, webpack, Tailwind, or any Node-based build tooling for the application itself. Node is allowed only for dev tools like the GitNexus CLI.\\n\\n## Project Structure\\n\\n```\\ncmd/psa/main.go                          \u2014 application entrypoint\\ninternal/                                \u2014 all application code\\n  auth/                                  \u2014 JWT, sessions, MFA, SSO\\n  config/                                \u2014 env-based config\\n  database/                              \u2014 pgx pool, migrations, query helpers\\n    migrations/                          \u2014 sequential SQL files\\n  middleware/                            \u2014 auth gate, module gate, RBAC\\n  ui/\\n    templates/                           \u2014 HTML templates by module\\n    static/css/nexus.css                 \u2014 design system (native CSS)\\n  /                              \u2014 each module (siem, devices, rmm, etc.)\\n    handler.go                           \u2014 HTTP handlers + route registration\\n    *.go                                 \u2014 domain logic, types, queries\\ndocs/\\n  ARCHITECTURE.md                        \u2014 30,000-foot system shape\\n  INVENTORY.md                           \u2014 module index\\n  DEPENDENCIES.md                        \u2014 internal imports + third-party history\\nCHANGELOG.md                             \u2014 release notes (at repo root)\\nchangelog.d/                             \u2014 fragment staging area\\nMakefile                                 \u2014 build, test, deploy targets\\nDockerfile                               \u2014 multi-stage build\\ndocker-compose.yml                       \u2014 production-like stack\\ndocker-compose.dev.yml                   \u2014 development overrides\\ngo.mod / go.sum                          \u2014 dependency manifest\\n```\\n\\n## Forbidden Actions\\n\\nWithout explicit user confirmation:\\n\\n- Running destructive database migrations or `DROP` statements\\n- Modifying files outside the repo\\n- Committing secrets, API keys, or credentials\\n- Pushing directly to `main` (branch protection blocks this anyway)\\n- `git push --force` to a shared branch (use `--force-with-lease` on your own feature branches only, after explicit approval)\\n- Editing AGENTS.md content between `gitnexus:start` / `gitnexus:end` markers (auto-managed)\\n- Deploying a binary built from a feature branch\\n\\n## Integration with Other Modules\\n\\nThe Root module does not contain business logic. Instead, it provides the **infrastructure** that all other modules depend on:\\n\\n- **`internal/auth`** uses the JWT keys configured in Root\\n- **`internal/database`** runs migrations defined in Root's `internal/database/migrations/`\\n- **`internal/middleware`** enforces entitlements defined in Root's config\\n- **All modules** are registered as HTTP handlers in `cmd/psa/main.go` and served from the single binary built by Root's Makefile\\n\\nThe Root module is the **contract** between developers and operations. Changes to build, deployment, or workflow procedures here affect every contributor and every production instance.\",\"scripts\":\"# scripts\\n\\n# Scripts Module Documentation\\n\\nThe `scripts/` directory contains standalone utility programs and shell scripts that support development, deployment, and documentation workflows. These tools are not part of the main application but are essential for maintaining code quality, security posture, and operational readiness.\\n\\n## Overview\\n\\n| Script | Purpose | Language |\\n|--------|---------|----------|\\n| `changelog/main.go` | Render changelog fragments into CHANGELOG.md | Go |\\n| `security_gen/main.go` | Generate security evidence HTML from markdown corpus | Go |\\n| `check-tenant-id.sh` | Pre-commit guard against unscoped database queries | Bash |\\n| `proxmox-deploy.sh` | Deploy NexusOS PSA to Proxmox LXC containers | Bash |\\n| `seed.go` | Populate development database with demo data | Go |\\n\\n---\\n\\n## Changelog Renderer (`changelog/main.go`)\\n\\n### Purpose\\n\\nAutomates changelog management by rendering small markdown fragments into a unified `CHANGELOG.md` file. This follows the [Keep a Changelog](https://keepachangelog.com/) format and enforces discipline: every code change must have a corresponding fragment file.\\n\\n### Fragment Format\\n\\nFragments live in `changelog.d/` and follow the naming pattern:\\n\\n```\\n..md\\n```\\n\\n**Example files:**\\n- `helpdesk-split.changed.md` \u2014 describes a changed feature\\n- `cve-2026-1234.security.md` \u2014 documents a security fix\\n\\n**Categories** (in display order):\\n- `added` \u2014 new features\\n- `changed` \u2014 behavior changes\\n- `deprecated` \u2014 upcoming removals\\n- `removed` \u2014 deleted features\\n- `fixed` \u2014 bug fixes\\n- `security` \u2014 security patches\\n\\nThe fragment body is plain text (no leading `- `) \u2014 the renderer adds bullet formatting.\\n\\n### Usage\\n\\n```bash\\n# Preview rendered [Unreleased] section\\ngo run ./scripts/changelog -dry-run\\n\\n# Merge fragments into CHANGELOG.md\\ngo run ./scripts/changelog\\n\\n# Merge and delete fragments after rendering\\ngo run ./scripts/changelog -clean\\n\\n# CI: fail if no fragments found (for mandatory changelog entries)\\ngo run ./scripts/changelog -check\\n```\\n\\n### How It Works\\n\\n1. **Load fragments** (`loadFragments`)\\n   - Scans `changelog.d/` for `*.md` files\\n   - Validates filename format and extracts category\\n   - Rejects empty files\\n   - Sorts by filename for deterministic output\\n\\n2. **Group by category** (`groupByCategory`)\\n   - Maps each fragment's body text to its category\\n   - Preserves order within each category\\n\\n3. **Render or merge**\\n   - **Dry-run**: outputs the `## [Unreleased]` section to stdout\\n   - **Merge**: inserts bullets into existing `## [Unreleased]` section in CHANGELOG.md\\n     - Creates missing `### Category` subsections in canonical order\\n     - Appends to existing subsections without disturbing released sections\\n     - Preserves file line endings (CRLF on Windows, LF on Unix)\\n\\n4. **Clean** (optional)\\n   - Deletes fragment files after successful merge\\n\\n### Key Functions\\n\\n| Function | Role |\\n|----------|------|\\n| `loadFragments(dir)` | Read and validate fragment files |\\n| `categoryFromFilename(name)` | Extract category from filename |\\n| `groupByCategory(fragments)` | Organize by category |\\n| `renderUnreleased(bullets)` | Format `## [Unreleased]` section |\\n| `formatBullet(body)` | Add `- ` prefix and indent continuation lines |\\n| `mergeIntoChangelog(path, bullets)` | Insert into existing CHANGELOG.md |\\n| `appendBulletsToUnreleased(body, bullets)` | Merge bullets into section body |\\n\\n---\\n\\n## Security Evidence Generator (`security_gen/main.go`)\\n\\n### Purpose\\n\\nGenerates `docs/security/index.html` from a markdown corpus documenting security controls, threat models, network surface, cryptography inventory, and audit trails. This creates a single-page security evidence dashboard for compliance audits (SOC 2, HIPAA, PCI-DSS, NIST, etc.).\\n\\n### Input Files\\n\\nAll files live under `docs/security/`:\\n\\n| File | Format | Content |\\n|------|--------|---------|\\n| `controls/*.md` | YAML frontmatter + sections | Framework controls (SOC 2, HIPAA, etc.) with implementation &amp; proof |\\n| `threats/*.md` | Markdown sections | STRIDE-lite threat models per module |\\n| `network-surface.md` | Markdown tables | Inbound listeners and outbound API clients |\\n| `crypto-inventory.md` | Markdown tables | TLS, symmetric encryption, signing, hashing |\\n| `audit-catalog.md` | Markdown table | Privileged actions and audit logging |\\n\\n### Control File Structure\\n\\n```yaml\\n---\\nframework: soc2\\ncontrol_id: CC6.1\\ntitle: Logical Access Controls\\nstatus: implemented\\nlast_verified: 2026-03-15\\n---\\n\\n## Control Text\\nDescription of what this control requires.\\n\\n## Implementation\\n- `auth/jwt.go` \u2014 JWT token validation\\n- `middleware/rbac.go` \u2014 Role-based access control\\n\\n## Proof\\n- `auth/jwt_test.go` \u2014 Token validation tests\\n- `middleware/rbac_test.go` \u2014 RBAC enforcement tests\\n\\n## Notes\\nAdditional context or exceptions.\\n```\\n\\n### Threat Model File Structure\\n\\n```markdown\\n---\\nmodule: API Gateway\\nupdated: 2026-03-10\\ngap_categories: [D, E]\\n---\\n\\n# Threat Model \u2014 API Gateway\\n\\n## Spoofing\\nMitigations for spoofing threats...\\n\\n## Tampering\\nMitigations for tampering threats...\\n\\n## Denial of Service\\nKnown gap: rate limiting not yet implemented.\\n```\\n\\n### Output\\n\\nGenerates a single-page HTML dashboard with:\\n- **Framework coverage cards** \u2014 % of controls implemented per framework\\n- **Network surface table** \u2014 listeners (port, TLS, auth, rate limit, audit log)\\n- **Outbound table** \u2014 external API clients (protocol, auth, cert pinning)\\n- **Threat tiles** \u2014 STRIDE-lite status per module (green/yellow/red hexagon)\\n- **Crypto inventory** \u2014 TLS, symmetric, signing, hashing algorithms\\n- **Audit catalog** \u2014 privileged actions and tenant-scoping\\n- **Featured control panel** \u2014 detailed view of a selected control\\n- **Alert section** \u2014 gaps and stale verifications (&gt;90 days)\\n\\n### Key Functions\\n\\n| Function | Role |\\n|----------|------|\\n| `loadControls(dir)` | Parse control files with frontmatter |\\n| `parseControl(path, raw)` | Extract framework, status, sections |\\n| `loadThreats(dir)` | Parse threat model files |\\n| `parseThreat(filename, raw)` | Extract STRIDE cells and gap markers |\\n| `sectionHasGap(body)` | Detect gap keywords in threat section |\\n| `loadNetworkSurface(path)` | Parse listener and outbound tables |\\n| `parseTLSBadge(s)` | Classify TLS version into badge style |\\n| `loadCryptoInventory(path)` | Parse crypto tables |\\n| `loadAuditCatalog(path)` | Parse audit action table |\\n| `buildPageData(...)` | Aggregate all data into PageData struct |\\n| `splitFrontmatter(raw)` | Extract YAML block from markdown |\\n| `splitSections(body)` | Map `## heading` \u2192 body text |\\n| `parseTable(body)` | Parse GitHub-flavored markdown tables |\\n| `renderInlineMarkdown(s)` | Convert `code` and `**bold**` to HTML |\\n\\n### Frameworks Tracked\\n\\nThe generator recognizes 14 canonical frameworks:\\n\\n```\\nSOC 2, HIPAA, PCI-DSS v4.0, CIS Controls v8, NIST 800-53, NIST CSF 2.0,\\nCMMC L2, ISO 27001:2022, GDPR, CCPA, FedRAMP Moderate, GLBA, SOX, FISMA\\n```\\n\\nEach control file specifies its `framework:` key (e.g., `soc2`, `hipaa`).\\n\\n### STRIDE-Lite Categories\\n\\nThreat models use six STRIDE categories:\\n\\n| Letter | Category | Marker |\\n|--------|----------|--------|\\n| S | Spoofing | `## Spoofing` |\\n| T | Tampering | `## Tampering` |\\n| R | Repudiation | `## Repudiation` |\\n| I | Information Disclosure | `## Information Disclosure` |\\n| D | Denial of Service | `## Denial of Service` |\\n| E | Elevation of Privilege | `## Elevation of Privilege` |\\n\\nA section is marked as a **gap** if:\\n- It contains keywords like \\\"known gap\\\", \\\"not implemented\\\", \\\"missing\\\", \\\"not yet wired\\\"\\n- The section is missing entirely (unless overridden in frontmatter)\\n- Frontmatter specifies `gap_categories: [D, E]`\\n\\nThreat tiles show a hexagon color:\\n- **Green** (success): 0 gaps\\n- **Yellow** (warning): 2+ gaps\\n- **Red** (danger): 3+ gaps\\n\\n---\\n\\n## Tenant-ID Guard (`check-tenant-id.sh`)\\n\\n### Purpose\\n\\nPre-commit and CI guard that prevents unscoped database queries on tenant-bound tables. Catches queries like:\\n\\n```sql\\nWHERE id = $1  -- \u274c Missing AND tenant_id = $N\\n```\\n\\nInstead of:\\n\\n```sql\\nWHERE id = $1 AND tenant_id = $2  -- \u2705 Correct\\n```\\n\\n### Tenant-Scoped Tables\\n\\nThe script maintains a list of tables that **must always** include `tenant_id` in WHERE clauses:\\n\\n```bash\\nusers, companies, tickets, projects, contacts, deals, invoices, quotes,\\nwork_timers, assessments, cmmc_poam, composer_poam, ...\\n```\\n\\n### Usage\\n\\n```bash\\n# As a pre-commit hook\\nln -s ../../scripts/check-tenant-id.sh .git/hooks/pre-commit\\n\\n# Manual audit (all .go files)\\nbash scripts/check-tenant-id.sh --all\\n\\n# CI mode (staged changes only)\\nbash scripts/check-tenant-id.sh\\n```\\n\\n### How It Works\\n\\n1. Determines file list:\\n   - **With `--all`**: all `*.go` files (excluding `_test.go`)\\n   - **Without**: only staged changes (`git diff --cached`)\\n\\n2. For each file, uses `awk` to scan for violations:\\n   - Pattern: `FROM  WHERE id = $N` (or `UPDATE ... WHERE id = $N`)\\n   - Checks if the same line or next line contains `tenant_id`\\n   - Ignores lines with `// nolint:tenantcheck` comment\\n\\n3. Exits with:\\n   - **0** if clean\\n   - **1** if violations found (lists each hit with line number)\\n\\n### Exceptions\\n\\nLegitimate exceptions (e.g., `WHERE id = $1` on the `tenants` table itself) require a trailing comment:\\n\\n```go\\n// nolint:tenantcheck \u2014 id IS the tenant identifier\\n```\\n\\n---\\n\\n## Proxmox Deployment (`proxmox-deploy.sh`)\\n\\n### Purpose\\n\\nAutomates deployment of NexusOS PSA to Proxmox LXC containers. Handles:\\n1. Docker image build and export\\n2. Container creation/update\\n3. Docker installation inside container\\n4. File transfer (image, compose, keys)\\n5. Service startup\\n\\n### Prerequisites\\n\\n- SSH access to Proxmox host as `root`\\n- Docker installed locally\\n- `pct` and `pveam` available on Proxmox host\\n\\n### Usage\\n\\n```bash\\n./scripts/proxmox-deploy.sh [options]\\n\\n# With defaults\\n./scripts/proxmox-deploy.sh\\n\\n# Custom configuration\\n./scripts/proxmox-deploy.sh \\\\\\n  --host proxmox.example.com \\\\\\n  --ctid 250 \\\\\\n  --memory 4096 \\\\\\n  --cores 4 \\\\\\n  --disk 50G\\n```\\n\\n### Options\\n\\n| Option | Default | Purpose |\\n|--------|---------|---------|\\n| `--host` | `proxmox.local` | Proxmox host address |\\n| `--ctid` | `200` | Container ID |\\n| `--storage` | `local-lvm` | Storage pool |\\n| `--memory` | `2048` | RAM in MB |\\n| `--cores` | `2` | CPU cores |\\n| `--disk` | `20` | Disk size in GB |\\n| `--bridge` | `vmbr0` | Network bridge |\\n\\n### Deployment Steps\\n\\n1. **Build Docker image** \u2014 `docker build -t nexusos-psa:latest .`\\n2. **Export to tarball** \u2014 `docker save | gzip`\\n3. **Check container** \u2014 Create if missing, start if stopped\\n4. **Install Docker** \u2014 Inside container via `pct exec`\\n5. **Transfer files** \u2014 Image, docker-compose.yml, JWT keys\\n6. **Start services** \u2014 `docker compose up -d`\\n\\n### Output\\n\\n```\\n=== NexusOS PSA \u2014 Proxmox Deployment ===\\nHost:    proxmox.local\\nCTID:    200\\nMemory:  2048MB / 2 cores / 20G disk\\n\\n[1/6] Building Docker image...\\n[2/6] Exporting Docker image...\\n...\\n[6/6] Starting NexusOS PSA services...\\n\\n=== Deployment Complete ===\\nContainer:  200 on proxmox.local\\nAccess:     http://192.168.1.100:3000\\n```\\n\\n---\\n\\n## Database Seeder (`seed.go`)\\n\\n### Purpose\\n\\nPopulates a development database with realistic demo data for testing and demos. Creates:\\n- Tenant (\\\"Horizon Managed Services\\\")\\n- Roles (admin, manager, technician, viewer)\\n- Admin user (admin@horizonmanaged.com / admin123)\\n- Sample companies, contacts, products, tickets\\n- ITIL entities (problems, changes, CMDB items, service catalog)\\n- Billing data (quotes, invoices, payments)\\n- CRM data (deals, activities, email campaigns)\\n\\n### Usage\\n\\n```bash\\n# With default DATABASE_URL\\ngo run ./scripts/seed.go\\n\\n# With custom database\\nDATABASE_URL=\\\"postgres://user:pass@host:5432/db\\\" go run ./scripts/seed.go\\n```\\n\\n### Data Created\\n\\n| Entity | Count | Purpose |\\n|--------|-------|---------|\\n| Tenant | 1 | \\\"Horizon Managed Services\\\" |\\n| Roles | 4 | admin, manager, technician, viewer |\\n| Users | 1 | admin@horizonmanaged.com |\\n| Companies | 5 | Acme Corp, TechStart Inc, MedSecure Health, LegalEagle LLP, RetailPro Stores |\\n| Products | 5 | M365, SentinelOne, Veeam, Cisco Meraki, Huntress |\\n| Tickets | 8 | Mix of incidents and service requests |\\n| KB Articles | 1 | Password reset guide |\\n| Problems | 3 | Known issues with workarounds |\\n| Changes | 4 | Pending, scheduled, and emergency changes |\\n| CMDB Items | 8 | Servers, network devices, software, cloud services |\\n| Quotes | 3 | Managed IT, security assessment, M365 migration |\\n| Invoices | 2 | One sent, one partial payment |\\n| Contacts | 15 | 3 per company (primary, billing, technical) |\\n| Deals | 4 | Various stages (discovery, proposal, negotiation) |\\n| CRM Tags | 5 | Enterprise, SMB, Healthcare, High Priority, New Client |\\n| Activities | 10 | Calls, emails, meetings, follow-ups |\\n| Email Campaign | 1 | Q1 security newsletter |\\n\\n### Key Patterns\\n\\n- **Tenant scoping**: All entities linked to tenant via `tenant_id`\\n- **Password hashing**: Admin password hashed with bcrypt\\n- **Conflict handling**: Uses `ON CONFLICT DO NOTHING` for idempotency\\n- **Realistic data**: Companies have websites, industries, employee counts; contacts have titles and departments\\n- **Relationships**: Contacts linked to companies; deals linked to contacts; activities linked to both\\n\\n---\\n\\n## Integration with Main Codebase\\n\\n```mermaid\\ngraph LR\\n    A[\\\"changelog.d/(fragment files)\\\"] --&gt;|load| B[\\\"changelog/main.go\\\"]\\n    B --&gt;|merge| C[\\\"CHANGELOG.md\\\"]\\n    \\n    D[\\\"docs/security/(markdown corpus)\\\"] --&gt;|load| E[\\\"security_gen/main.go\\\"]\\n    E --&gt;|render| F[\\\"docs/security/index.html\\\"]\\n    \\n    G[\\\"*.go files\\\"] --&gt;|scan| H[\\\"check-tenant-id.sh\\\"]\\n    H --&gt;|validate| I[\\\"CI/pre-commit\\\"]\\n    \\n    J[\\\"Dockerfiledocker-compose.yml\\\"] --&gt;|build &amp; deploy| K[\\\"proxmox-deploy.sh\\\"]\\n    K --&gt;|provision| L[\\\"Proxmox LXCContainer\\\"]\\n    \\n    M[\\\"Database schema\\\"] --&gt;|populate| N[\\\"seed.go\\\"]\\n    N --&gt;|insert demo data| O[\\\"Development DB\\\"]\\n```\\n\\n### Typical Workflows\\n\\n**Adding a feature:**\\n1. Write code in `cmd/` or `internal/`\\n2. Create `changelog.d/my-feature.added.md`\\n3. Run `go run ./scripts/changelog -dry-run` to preview\\n4. Commit both files\\n\\n**Updating security docs:**\\n1. Edit `docs/security/controls/*.md` or threat models\\n2. Run `go run ./scripts/security_gen/main.go`\\n3. Commit updated `docs/security/index.html`\\n\\n**Deploying to staging:**\\n1. Run `./scripts/proxmox-deploy.sh --host staging.proxmox.local --ctid 250`\\n2. Access at `http://:3000`\\n\\n**Setting up development environment:**\\n1. `docker-compose up` (local dev)\\n2. `go run ./scripts/seed.go` (populate demo data)\\n3. Login as `admin@horizonmanaged.com / admin123`\\n\\n---\\n\\n## Dependencies\\n\\n- **changelog/main.go**: stdlib only (bufio, flag, fmt, os, path/filepath, sort, strings)\\n- **security_gen/main.go**: stdlib only (bytes, fmt, html/template, os, path/filepath, sort, strings, time)\\n- **check-tenant-id.sh**: bash, git, awk, grep\\n- **proxmox-deploy.sh**: bash, ssh, scp, docker, pct, pveam\\n- **seed.go**: stdlib (context, fmt, log, os) + `github.com/jackc/pgx/v5/pgxpool`, `golang.org/x/crypto/bcrypt`\\n\\n---\\n\\n## Maintenance Notes\\n\\n- **Changelog categories**: Keep in sync with Keep a Changelog spec\\n- **Framework list**: Update `canonicalFrameworks` in security_gen when adding compliance frameworks\\n- **Tenant tables**: Extend `TENANT_TABLES` in check-tenant-id.sh as schema grows\\n- **Seed data**: Update company/product lists to reflect current offerings\\n- **Proxmox defaults**: Adjust memory/cores/disk based on deployment environment\"};\nvar TREE = [{\"name\":\"Root\",\"slug\":\"root\",\"files\":[\"AGENTS.md\",\"CLAUDE.md\",\"Dockerfile\",\"Makefile\",\"docker-compose.dev.yml\",\"docker-compose.yml\",\"go.mod\"]},{\"name\":\"branding\",\"slug\":\"branding\",\"files\":[\"branding/BRAND_GUIDELINES.md\",\"branding/COLOR_PALETTE.md\",\"branding/adobe_firefly_prompt.md\",\"branding/landing_page_sample.html\",\"branding/letterhead_sample_1.html\",\"branding/letterhead_sample_2.html\",\"branding/letterhead_sample_3.html\",\"branding/nexoscore_logo_v1.html\",\"branding/nexoscore_logo_v2.html\",\"branding/typography_sample_1.html\",\"branding/typography_sample_2.html\",\"branding/typography_sample_3.html\",\"branding/typography_sample_4a.html\",\"branding/typography_sample_4b.html\",\"branding/typography_sample_4c.html\",\"branding/typography_sample_4d.html\"]},{\"name\":\"cmd\",\"slug\":\"cmd\",\"files\":[\"cmd/auth-gate/main.go\",\"cmd/psa/main.go\",\"cmd/qb-push-items/main.go\"]},{\"name\":\"docs\",\"slug\":\"docs\",\"files\":[],\"children\":[{\"name\":\"docs \u2014 docs\",\"slug\":\"docs-docs\",\"files\":[\"docs/2026-04-07-Meeting-Email-Security-Flow.md\",\"docs/API_REFERENCE.md\",\"docs/ARCHITECTURE.md\",\"docs/AUDIT_CRM_TENANT_SCOPE.md\",\"docs/BASE_HTML_MIGRATION_PLAN.md\",\"docs/BID_ENGINE_TEST_GUIDE.md\",\"docs/CMMC_ONE_CLICK_COMPLIANCE_PLAN.md\",\"docs/CMMC_ONE_CLICK_TEST_GUIDE.md\",\"docs/CODEBASE_OBSERVATIONS.md\",\"docs/COMPLIANCE_COMPOSER_PLAN.md\",\"docs/DEPENDENCIES-EXTERNAL.md\",\"docs/DEPENDENCIES.md\",\"docs/DISTRIBUTOR_PHASE3_PROCUREMENT_PLAN.md\",\"docs/EMAIL_SECURITY_TRIAGE_PLAN.md\",\"docs/INFRASTRUCTURE.md\",\"docs/INVENTORY.md\",\"docs/LIVE_REVIEW_ENGINE_MANUAL.md\",\"docs/NCSR_INTEGRATION_PLAN.md\",\"docs/NexusOS_Orchestrator_Architecture.md\",\"docs/NexusOS_Sales_Lifecycle_Automation_Plan.md\",\"docs/NexusOS_Sales_Lifecycle_Test_Plan.md\",\"docs/NexusRMM-Status-Update-Jon.md\",\"docs/ONBOARDING.md\",\"docs/Orchestrator_Hex_Detail_UI_Artifact.md\",\"docs/PRODUCTS_SERVICES_CATALOG.md\",\"docs/PROJECT_MANAGEMENT.md\",\"docs/PR_A_Helpdesk-Peek.md\",\"docs/PR_B_Helpdesk-Hub.md\",\"docs/Phase9_Action_Configuration_Plan.md\",\"docs/RISK_ACCEPTANCE_DISPOSITION_GUIDE.md\",\"docs/ROADMAP.md\",\"docs/gen_ach_user_guide.py\",\"docs/gen_ach_whitepaper.py\",\"docs/gen_onboarding_guide.py\",\"docs/gen_onboarding_user_guide.py\",\"docs/gen_schema_poster.py\",\"docs/lifecycle-test-manual.md\",\"docs/orchestrator_ui_enhancement_artifact.md\",\"docs/teleport-deployment-guide.md\",\"docs/tickets-er-diagram.html\",\"docs/work-order-manual.md\"]},{\"name\":\"docs \u2014 architecture\",\"slug\":\"docs-architecture\",\"files\":[\"docs/architecture/ARCHITECTURE.md\",\"docs/architecture/PROJECT_STATUS.md\",\"docs/architecture/REMOTE_DESKTOP_SPEC.md\"]},{\"name\":\"docs \u2014 guide\",\"slug\":\"docs-guide\",\"files\":[\"docs/guide/MULTI_REVIEWER_TEST_GUIDE.md\",\"docs/guide/NCSR_USER_GUIDE.md\",\"docs/guide/PHASE3_PROCUREMENT_TESTING_GUIDE.md\",\"docs/guide/README.md\",\"docs/guide/RISK_ACCEPTANCE_USER_GUIDE.md\",\"docs/guide/billing-contracts.md\",\"docs/guide/dispatch.md\",\"docs/guide/employee-hr.md\",\"docs/guide/projects.md\"]},{\"name\":\"docs \u2014 integrations\",\"slug\":\"docs-integrations\",\"files\":[\"docs/integrations/BACKLOG.md\"]},{\"name\":\"docs \u2014 mockups\",\"slug\":\"docs-mockups\",\"files\":[\"docs/mockups/brief_mockup_v2.html\",\"docs/mockups/live_review_generalization.html\",\"docs/mockups/phase3/_shared.css\",\"docs/mockups/phase3/agreement-saas.html\",\"docs/mockups/phase3/contract-materials.html\",\"docs/mockups/phase3/index.html\",\"docs/mockups/phase3/procurement-detail.html\",\"docs/mockups/phase3/procurement-list.html\",\"docs/mockups/phase3/project-po-integration.html\",\"docs/mockups/phase3/remediation-queue.html\",\"docs/mockups/phase3/settings-distributors.html\",\"docs/mockups/phase3/ticket-po-integration.html\",\"docs/mockups/review_host_completion.html\",\"docs/mockups/review_ipad_completion.html\"]},{\"name\":\"docs \u2014 modules\",\"slug\":\"docs-modules\",\"files\":[\"docs/modules/auth/API.md\",\"docs/modules/auth/MANUAL.md\",\"docs/modules/auth/SOURCE_MAP.md\",\"docs/modules/auth/TODO.md\",\"docs/modules/auth/WHITEPAPER.md\",\"docs/modules/billing/API.md\",\"docs/modules/billing/MANUAL.md\",\"docs/modules/billing/SOURCE_MAP.md\",\"docs/modules/billing/TODO.md\",\"docs/modules/billing/WHITEPAPER.md\",\"docs/modules/certificates/API.md\",\"docs/modules/certificates/MANUAL.md\",\"docs/modules/certificates/SOURCE_MAP.md\",\"docs/modules/certificates/TODO.md\",\"docs/modules/certificates/WHITEPAPER.md\",\"docs/modules/compliance/API.md\",\"docs/modules/compliance/MANUAL.md\",\"docs/modules/compliance/SOURCE_MAP.md\",\"docs/modules/compliance/TODO.md\",\"docs/modules/compliance/WHITEPAPER.md\",\"docs/modules/crm/API.md\",\"docs/modules/crm/MANUAL.md\",\"docs/modules/crm/SOURCE_MAP.md\",\"docs/modules/crm/TODO.md\",\"docs/modules/crm/WHITEPAPER.md\",\"docs/modules/cybersec/API.md\",\"docs/modules/cybersec/MANUAL.md\",\"docs/modules/cybersec/SOURCE_MAP.md\",\"docs/modules/cybersec/TODO.md\",\"docs/modules/cybersec/WHITEPAPER.md\",\"docs/modules/devices/API.md\",\"docs/modules/devices/MANUAL.md\",\"docs/modules/devices/SOURCE_MAP.md\",\"docs/modules/devices/TODO.md\",\"docs/modules/devices/WHITEPAPER.md\",\"docs/modules/email-security/API.md\",\"docs/modules/email-security/MANUAL.md\",\"docs/modules/email-security/SOURCE_MAP.md\",\"docs/modules/email-security/TODO.md\",\"docs/modules/email-security/WHITEPAPER.md\",\"docs/modules/health/API.md\",\"docs/modules/health/MANUAL.md\",\"docs/modules/health/SOURCE_MAP.md\",\"docs/modules/health/TODO.md\",\"docs/modules/health/WHITEPAPER.md\",\"docs/modules/helpdesk/API.md\",\"docs/modules/helpdesk/MANUAL.md\",\"docs/modules/helpdesk/SOURCE_MAP.md\",\"docs/modules/helpdesk/TODO.md\",\"docs/modules/helpdesk/WHITEPAPER.md\",\"docs/modules/hudu/API.md\",\"docs/modules/hudu/MANUAL.md\",\"docs/modules/hudu/SOURCE_MAP.md\",\"docs/modules/hudu/TODO.md\",\"docs/modules/hudu/WHITEPAPER.md\",\"docs/modules/knowledge-base/API.md\",\"docs/modules/knowledge-base/MANUAL.md\",\"docs/modules/knowledge-base/SOURCE_MAP.md\",\"docs/modules/knowledge-base/TODO.md\",\"docs/modules/knowledge-base/WHITEPAPER.md\",\"docs/modules/legal/API.md\",\"docs/modules/legal/MANUAL.md\",\"docs/modules/legal/SOURCE_MAP.md\",\"docs/modules/legal/TODO.md\",\"docs/modules/legal/WHITEPAPER.md\",\"docs/modules/metasploit/API.md\",\"docs/modules/metasploit/MANUAL.md\",\"docs/modules/metasploit/SOURCE_MAP.md\",\"docs/modules/metasploit/TODO.md\",\"docs/modules/metasploit/WHITEPAPER.md\",\"docs/modules/mobile/API.md\",\"docs/modules/mobile/MANUAL.md\",\"docs/modules/mobile/SOURCE_MAP.md\",\"docs/modules/mobile/TODO.md\",\"docs/modules/mobile/WHITEPAPER.md\",\"docs/modules/nexie-ai/API.md\",\"docs/modules/nexie-ai/MANUAL.md\",\"docs/modules/nexie-ai/SOURCE_MAP.md\",\"docs/modules/nexie-ai/TODO.md\",\"docs/modules/nexie-ai/WHITEPAPER.md\",\"docs/modules/onboarding/API.md\",\"docs/modules/onboarding/MANUAL.md\",\"docs/modules/onboarding/SOURCE_MAP.md\",\"docs/modules/onboarding/TODO.md\",\"docs/modules/onboarding/WHITEPAPER.md\",\"docs/modules/products/API.md\",\"docs/modules/products/MANUAL.md\",\"docs/modules/products/SOURCE_MAP.md\",\"docs/modules/products/TODO.md\",\"docs/modules/products/WHITEPAPER.md\",\"docs/modules/projects/API.md\",\"docs/modules/projects/MANUAL.md\",\"docs/modules/projects/SOURCE_MAP.md\",\"docs/modules/projects/TODO.md\",\"docs/modules/projects/WHITEPAPER.md\",\"docs/modules/purchase-orders/API.md\",\"docs/modules/purchase-orders/MANUAL.md\",\"docs/modules/purchase-orders/SOURCE_MAP.md\",\"docs/modules/purchase-orders/TODO.md\",\"docs/modules/purchase-orders/WHITEPAPER.md\",\"docs/modules/push-notifications/API.md\",\"docs/modules/push-notifications/MANUAL.md\",\"docs/modules/push-notifications/SOURCE_MAP.md\",\"docs/modules/push-notifications/TODO.md\",\"docs/modules/push-notifications/WHITEPAPER.md\",\"docs/modules/quickbooks/API.md\",\"docs/modules/quickbooks/MANUAL.md\",\"docs/modules/quickbooks/SOURCE_MAP.md\",\"docs/modules/quickbooks/TODO.md\",\"docs/modules/quickbooks/WHITEPAPER.md\",\"docs/modules/rmm/AGENT_AUDIT.md\",\"docs/modules/rmm/AGENT_REQUIREMENTS.md\",\"docs/modules/rmm/API.md\",\"docs/modules/rmm/MANUAL.md\",\"docs/modules/rmm/SOURCE_MAP.md\",\"docs/modules/rmm/TODO.md\",\"docs/modules/rmm/WHITEPAPER.md\",\"docs/modules/settings/API.md\",\"docs/modules/settings/MANUAL.md\",\"docs/modules/settings/SOURCE_MAP.md\",\"docs/modules/settings/TODO.md\",\"docs/modules/settings/WHITEPAPER.md\",\"docs/modules/siem/API.md\",\"docs/modules/siem/MANUAL.md\",\"docs/modules/siem/SOURCE_MAP.md\",\"docs/modules/siem/TODO.md\",\"docs/modules/siem/WHITEPAPER.md\",\"docs/modules/timer/API.md\",\"docs/modules/timer/MANUAL.md\",\"docs/modules/timer/SOURCE_MAP.md\",\"docs/modules/timer/TODO.md\",\"docs/modules/timer/WHITEPAPER.md\",\"docs/modules/voip/API.md\",\"docs/modules/voip/MANUAL.md\",\"docs/modules/voip/SOURCE_MAP.md\",\"docs/modules/voip/TODO.md\",\"docs/modules/voip/WHITEPAPER.md\"]},{\"name\":\"docs \u2014 security\",\"slug\":\"docs-security\",\"files\":[\"docs/security/EVIDENCE.md\",\"docs/security/audit-catalog.md\",\"docs/security/controls/soc2-cc6.1-logical-access.md\",\"docs/security/crypto-inventory.md\",\"docs/security/index.html\",\"docs/security/network-surface.md\",\"docs/security/threats/rmm.md\"]},{\"name\":\"docs \u2014 ui-samples\",\"slug\":\"docs-ui-samples\",\"files\":[\"docs/ui-samples/action_config_panels_mockup.html\",\"docs/ui-samples/hex-frosted-matte-sample.html\",\"docs/ui-samples/hex-frosted-white-ceramic-sample.html\",\"docs/ui-samples/hex-glass-buttons-sample.html\",\"docs/ui-samples/hex-glass-light-mode-sample.html\",\"docs/ui-samples/hex-glass-theme-sample.html\",\"docs/ui-samples/orchestrator_module_detail_mockup.html\"]}]},{\"name\":\"extensions\",\"slug\":\"extensions\",\"files\":[\"extensions/chrome/background.js\",\"extensions/chrome/content.js\",\"extensions/chrome/manifest.json\",\"extensions/chrome/popup.html\",\"extensions/chrome/popup.js\"]},{\"name\":\"internal\",\"slug\":\"internal\",\"files\":[],\"children\":[{\"name\":\"internal \u2014 ai\",\"slug\":\"internal-ai\",\"files\":[\"internal/ai/claude.go\",\"internal/ai/redact.go\"]},{\"name\":\"internal \u2014 assessment\",\"slug\":\"internal-assessment\",\"files\":[\"internal/assessment/analyzer.go\",\"internal/assessment/approval_gate.go\",\"internal/assessment/blueprint_builder.go\",\"internal/assessment/document_processor.go\",\"internal/assessment/handler.go\",\"internal/assessment/quote_converter.go\",\"internal/assessment/scheduled_ops.go\",\"internal/assessment/types.go\"]},{\"name\":\"internal \u2014 auth\",\"slug\":\"internal-auth\",\"files\":[\"internal/auth/handler.go\",\"internal/auth/jwt.go\",\"internal/auth/mfa.go\",\"internal/auth/middleware.go\",\"internal/auth/password.go\",\"internal/auth/session.go\",\"internal/auth/sso.go\"]},{\"name\":\"internal \u2014 bidengine\",\"slug\":\"internal-bidengine\",\"files\":[\"internal/bidengine/addendum_apply.go\",\"internal/bidengine/addendum_split.go\",\"internal/bidengine/audit.go\",\"internal/bidengine/audit_handler.go\",\"internal/bidengine/audit_resolve.go\",\"internal/bidengine/audit_view.go\",\"internal/bidengine/drawing_discipline.go\",\"internal/bidengine/engine.go\",\"internal/bidengine/engine_test.go\",\"internal/bidengine/exclusion_extractor.go\",\"internal/bidengine/exclusion_extractor_test.go\",\"internal/bidengine/flow.go\",\"internal/bidengine/generate_from_drawings.go\",\"internal/bidengine/generate_from_spec_helpers.go\",\"internal/bidengine/handler.go\",\"internal/bidengine/outcomes.go\",\"internal/bidengine/page_selection.go\",\"internal/bidengine/processing_lock.go\",\"internal/bidengine/reconcile.go\",\"internal/bidengine/scope_summary.go\",\"internal/bidengine/trade_profile.go\",\"internal/bidengine/types.go\",\"internal/bidengine/vision_audit.go\"]},{\"name\":\"internal \u2014 billing\",\"slug\":\"internal-billing\",\"files\":[\"internal/billing/email.go\",\"internal/billing/invoice_attachments.go\",\"internal/billing/invoice_handler.go\",\"internal/billing/quote_attachments.go\",\"internal/billing/quote_handler.go\",\"internal/billing/transcript_analyzer.go\",\"internal/billing/transcript_handler.go\",\"internal/billing/types.go\"]},{\"name\":\"internal \u2014 certs\",\"slug\":\"internal-certs\",\"files\":[\"internal/certs/ca.go\",\"internal/certs/handler.go\",\"internal/certs/store.go\"]},{\"name\":\"internal \u2014 cmmc\",\"slug\":\"internal-cmmc\",\"files\":[\"internal/cmmc/binder_render.go\",\"internal/cmmc/connect.go\",\"internal/cmmc/disposition.go\",\"internal/cmmc/handler.go\",\"internal/cmmc/nexie.go\",\"internal/cmmc/policy_render.go\",\"internal/cmmc/risk_resolver.go\",\"internal/cmmc/ssp_render.go\",\"internal/cmmc/types.go\"]},{\"name\":\"internal \u2014 compliance\",\"slug\":\"internal-compliance\",\"files\":[\"internal/compliance/handler.go\",\"internal/compliance/types.go\"]},{\"name\":\"internal \u2014 composer\",\"slug\":\"internal-composer\",\"files\":[\"internal/composer/disposition.go\",\"internal/composer/framework.go\",\"internal/composer/handler.go\",\"internal/composer/ncsr/action_plan.go\",\"internal/composer/ncsr/crm_compliance.go\",\"internal/composer/ncsr/delete.go\",\"internal/composer/ncsr/handler.go\",\"internal/composer/ncsr/parse_pdf.go\",\"internal/composer/ncsr/parse_pdf_test.go\",\"internal/composer/ncsr/parse_xlsx.go\",\"internal/composer/ncsr/parse_xlsx_test.go\",\"internal/composer/ncsr/post_review.go\",\"internal/composer/ncsr/review_queue.go\",\"internal/composer/ncsr/review_writeback.go\",\"internal/composer/ncsr/reviews_page.go\",\"internal/composer/ncsr/risk_resolver.go\",\"internal/composer/ncsr/seeder.go\",\"internal/composer/ncsr/trajectory.go\",\"internal/composer/ncsr/types.go\",\"internal/composer/nexie.go\",\"internal/composer/reviews_history.go\",\"internal/composer/risk_resolver.go\",\"internal/composer/start_review.go\",\"internal/composer/types.go\"]},{\"name\":\"internal \u2014 config\",\"slug\":\"internal-config\",\"files\":[\"internal/config/config.go\"]},{\"name\":\"internal \u2014 crm\",\"slug\":\"internal-crm\",\"files\":[\"internal/crm/accounts_handler.go\",\"internal/crm/activities_handler.go\",\"internal/crm/campaigns_handler.go\",\"internal/crm/contacts_handler.go\",\"internal/crm/deals_handler.go\",\"internal/crm/email_security_handler.go\",\"internal/crm/form_handlers.go\",\"internal/crm/handler.go\",\"internal/crm/lead_analysis.go\",\"internal/crm/lead_analysis_test.go\",\"internal/crm/pipelines_handler.go\",\"internal/crm/tenant_isolation_test.go\",\"internal/crm/types.go\"]},{\"name\":\"internal \u2014 cybersec\",\"slug\":\"internal-cybersec\",\"files\":[\"internal/cybersec/handler.go\",\"internal/cybersec/types.go\"]},{\"name\":\"internal \u2014 database\",\"slug\":\"internal-database\",\"files\":[\"internal/database/database.go\",\"internal/database/migrate.go\",\"internal/database/migrations/001_extensions.sql\",\"internal/database/migrations/002_tenants.sql\",\"internal/database/migrations/003_roles_permissions.sql\",\"internal/database/migrations/004_users.sql\",\"internal/database/migrations/005_companies_agents.sql\",\"internal/database/migrations/006_products_compliance.sql\",\"internal/database/migrations/007_sso.sql\",\"internal/database/migrations/008_helpdesk.sql\",\"internal/database/migrations/009_compliance_cybersec_extras.sql\",\"internal/database/migrations/010_projects.sql\",\"internal/database/migrations/011_itil.sql\",\"internal/database/migrations/012_quoting_invoicing.sql\",\"internal/database/migrations/013_crm.sql\",\"internal/database/migrations/014_audit_revisions.sql\",\"internal/database/migrations/015_quickbooks_integration.sql\",\"internal/database/migrations/016_quote_require_po.sql\",\"internal/database/migrations/017_certs_mcp_email.sql\",\"internal/database/migrations/018_user_favorites.sql\",\"internal/database/migrations/019_compliance_engine.sql\",\"internal/database/migrations/020_compliance_full_controls.sql\",\"internal/database/migrations/021_compliance_doc_import.sql\",\"internal/database/migrations/022_hipaa_full_controls.sql\",\"internal/database/migrations/023_pci_dss_full_controls.sql\",\"internal/database/migrations/024_soc2_full_controls.sql\",\"internal/database/migrations/025_cmmc_full_controls.sql\",\"internal/database/migrations/026_nist_800_53_full_controls.sql\",\"internal/database/migrations/027_ai_usage_log.sql\",\"internal/database/migrations/027b_compliance_framework_source_cols.sql\",\"internal/database/migrations/028_iso27001.sql\",\"internal/database/migrations/029_nist_csf.sql\",\"internal/database/migrations/030_nist_800_171.sql\",\"internal/database/migrations/031_fedramp.sql\",\"internal/database/migrations/032_iso27701.sql\",\"internal/database/migrations/033_hitrust.sql\",\"internal/database/migrations/034_ffiec.sql\",\"internal/database/migrations/035_stateramp.sql\",\"internal/database/migrations/036_cjis.sql\",\"internal/database/migrations/037_crm_client_enhancements.sql\",\"internal/database/migrations/038_communication_channels.sql\",\"internal/database/migrations/039_country_field.sql\",\"internal/database/migrations/040_rmm_merge.sql\",\"internal/database/migrations/041_legal_documents_and_contracts.sql\",\"internal/database/migrations/042_contract_service_types.sql\",\"internal/database/migrations/043_contract_sync.sql\",\"internal/database/migrations/044_teleport_integration.sql\",\"internal/database/migrations/045_agent_polling_teleport_token.sql\",\"internal/database/migrations/046_metasploit_integration.sql\",\"internal/database/migrations/047_client_vulnerability_config.sql\",\"internal/database/migrations/048_documentation_provider.sql\",\"internal/database/migrations/049_ai_auto_resolve.sql\",\"internal/database/migrations/050_siem.sql\",\"internal/database/migrations/051_billing_work_types_and_roles.sql\",\"internal/database/migrations/052_contract_categories.sql\",\"internal/database/migrations/053_contract_rate_overrides.sql\",\"internal/database/migrations/054_esign_multi_signature.sql\",\"internal/database/migrations/055_product_legal_documents.sql\",\"internal/database/migrations/056_quote_to_contract.sql\",\"internal/database/migrations/057_quote_attachments.sql\",\"internal/database/migrations/058_siem_capture_sessions.sql\",\"internal/database/migrations/059_siem_log_retention.sql\",\"internal/database/migrations/060_certificate_store.sql\",\"internal/database/migrations/061_device_credentials.sql\",\"internal/database/migrations/062_siem_remediation.sql\",\"internal/database/migrations/063_login_audit.sql\",\"internal/database/migrations/064_invitations.sql\",\"internal/database/migrations/065_rmm_enroll_token.sql\",\"internal/database/migrations/066_persistent_timers.sql\",\"internal/database/migrations/067_security_audit_log.sql\",\"internal/database/migrations/068_multi_timer.sql\",\"internal/database/migrations/069_ticket_contract_link.sql\",\"internal/database/migrations/070_company_escalation_policy.sql\",\"internal/database/migrations/071_quote_sla_escalation_policies.sql\",\"internal/database/migrations/072_nexie_time_entries.sql\",\"internal/database/migrations/073_ticket_config.sql\",\"internal/database/migrations/074_voip_integration.sql\",\"internal/database/migrations/075_quote_sequencing.sql\",\"internal/database/migrations/076_quote_sections.sql\",\"internal/database/migrations/077_fix_contract_policy_columns.sql\",\"internal/database/migrations/078_project_management_v2.sql\",\"internal/database/migrations/079_hudu_sync_status.sql\",\"internal/database/migrations/080_health_components.sql\",\"internal/database/migrations/082_ticket_charges.sql\",\"internal/database/migrations/083_time_entry_product.sql\",\"internal/database/migrations/084_purchase_orders.sql\",\"internal/database/migrations/085_po_approvers.sql\",\"internal/database/migrations/086_vendors.sql\",\"internal/database/migrations/087_cr_esign.sql\",\"internal/database/migrations/088_po_approval_signature.sql\",\"internal/database/migrations/089_web_push.sql\",\"internal/database/migrations/090_project_site_location.sql\",\"internal/database/migrations/091_orchestrator.sql\",\"internal/database/migrations/092_workflow_engine_v2.sql\",\"internal/database/migrations/093_rmm_session_tokens.sql\",\"internal/database/migrations/094_rmm_enrollment_tokens.sql\",\"internal/database/migrations/095_email_send_log.sql\",\"internal/database/migrations/096_client_security_profile.sql\",\"internal/database/migrations/097_client_onboarding.sql\",\"internal/database/migrations/098_ach_payment_method.sql\",\"internal/database/migrations/098_device_cert_columns.sql\",\"internal/database/migrations/099_orchestrator_packages.sql\",\"internal/database/migrations/099_sla_pause_support.sql\",\"internal/database/migrations/100_note_edit_tracking.sql\",\"internal/database/migrations/101_multi_pipeline.sql\",\"internal/database/migrations/102_invoice_attachments.sql\",\"internal/database/migrations/103_recurring_invoice_schedule.sql\",\"internal/database/migrations/104_timesheets.sql\",\"internal/database/migrations/105_qb_employees.sql\",\"internal/database/migrations/106_timesheet_permissions.sql\",\"internal/database/migrations/107_portal.sql\",\"internal/database/migrations/108_portal_permissions.sql\",\"internal/database/migrations/109_contacts_country.sql\",\"internal/database/migrations/110_kb_visibility.sql\",\"internal/database/migrations/111_invoice_qb_link.sql\",\"internal/database/migrations/112_ai_usage_portal.sql\",\"internal/database/migrations/113_portal_roles.sql\",\"internal/database/migrations/114_portal_role_labels.sql\",\"internal/database/migrations/115_user_ui_theme.sql\",\"internal/database/migrations/116_user_ui_preferences.sql\",\"internal/database/migrations/117_industry_intelligence.sql\",\"internal/database/migrations/118_assessments.sql\",\"internal/database/migrations/119_orchestrator_events.sql\",\"internal/database/migrations/120_notification_actions.sql\",\"internal/database/migrations/121_scheduled_operations.sql\",\"internal/database/migrations/122_pipeline_presets.sql\",\"internal/database/migrations/123_industry_business_types.sql\",\"internal/database/migrations/124_compliance_frameworks_sync.sql\",\"internal/database/migrations/125_epa.sql\",\"internal/database/migrations/126_epa_client_contact_asset.sql\",\"internal/database/migrations/127_agent_releases.sql\",\"internal/database/migrations/128_agent_releases_v11_seed.sql\",\"internal/database/migrations/129_agent_releases_go_v110.sql\",\"internal/database/migrations/129_custom_actions.sql\",\"internal/database/migrations/131_agent_releases_go_v111.sql\",\"internal/database/migrations/132_contacts_nexie_epa_enabled.sql\",\"internal/database/migrations/133_nexuspulse.sql\",\"internal/database/migrations/134_nexuspulse_reports.sql\",\"internal/database/migrations/135_nexuspulse_rebrand_codes.sql\",\"internal/database/migrations/136_product_qbo_accounts.sql\",\"internal/database/migrations/137_product_cost.sql\",\"internal/database/migrations/138_product_subcontractor.sql\",\"internal/database/migrations/139_product_image.sql\",\"internal/database/migrations/140_product_sku.sql\",\"internal/database/migrations/141_product_supplier.sql\",\"internal/database/migrations/142_bid_engine.sql\",\"internal/database/migrations/143_bid_spec_compliance.sql\",\"internal/database/migrations/144_product_labor_hours.sql\",\"internal/database/migrations/145_bid_card_types.sql\",\"internal/database/migrations/146_bid_revisions.sql\",\"internal/database/migrations/147_bid_sections.sql\",\"internal/database/migrations/148_bid_trade_scope.sql\",\"internal/database/migrations/149_section_scope_link.sql\",\"internal/database/migrations/150_bid_address_fields.sql\",\"internal/database/migrations/151_spec_doc_ai_cost.sql\",\"internal/database/migrations/152_site_surveys.sql\",\"internal/database/migrations/153_crm_company_type.sql\",\"internal/database/migrations/154_survey_checklist_seeds.sql\",\"internal/database/migrations/155_orchestrator_survey_bid_presets.sql\",\"internal/database/migrations/156_timer_survey_support.sql\",\"internal/database/migrations/157_survey_photo_analysis.sql\",\"internal/database/migrations/158_orchestrator_photo_analysis_preset.sql\",\"internal/database/migrations/159_survey_poc_contact.sql\",\"internal/database/migrations/160_survey_audio.sql\",\"internal/database/migrations/161_drawings.sql\",\"internal/database/migrations/162_photo_thumbnails.sql\",\"internal/database/migrations/163_assessment_work_type.sql\",\"internal/database/migrations/164_work_orders.sql\",\"internal/database/migrations/165_full_lifecycle_presets.sql\",\"internal/database/migrations/166_dispatch.sql\",\"internal/database/migrations/167_employee.sql\",\"internal/database/migrations/168_seed_employees.sql\",\"internal/database/migrations/169_recorder_sessions_segments.sql\",\"internal/database/migrations/170_role_rates.sql\",\"internal/database/migrations/171_time_entry_costs.sql\",\"internal/database/migrations/172_role_rates_billing_link.sql\",\"internal/database/migrations/173_cmmc_l2_module.sql\",\"internal/database/migrations/174_cmmc_company_branding.sql\",\"internal/database/migrations/175_recorder_consent.sql\",\"internal/database/migrations/176_infra_devices.sql\",\"internal/database/migrations/177_agents_compliance_data.sql\",\"internal/database/migrations/178_cmmc_ingested_docs.sql\",\"internal/database/migrations/179_compliance_composer.sql\",\"internal/database/migrations/180_sentinel_devices.sql\",\"internal/database/migrations/181_sentinel_schema_isolation.sql\",\"internal/database/migrations/182_sentinel_audit.sql\",\"internal/database/migrations/183_agents_sentinel_hmac_key.sql\",\"internal/database/migrations/183_risk_disposition.sql\",\"internal/database/migrations/184_ncsr_assessments.sql\",\"internal/database/migrations/185_recorder_consent_cancelled.sql\",\"internal/database/migrations/185_risk_acceptances.sql\",\"internal/database/migrations/186_live_review_engine.sql\",\"internal/database/migrations/187_review_token_revocation.sql\",\"internal/database/migrations/188_ncsr_plan_jobs.sql\",\"internal/database/migrations/189_review_decision_writeback.sql\",\"internal/database/migrations/189_tickets_pinned.sql\",\"internal/database/migrations/190_backfill_action_decline_from_area.sql\",\"internal/database/migrations/190_tickets_source_enum.sql\",\"internal/database/migrations/191_backfill_action_decision_from_area.sql\",\"internal/database/migrations/192_brief_v2_fields.sql\",\"internal/database/migrations/193_ticket_followers.sql\",\"internal/database/migrations/194_review_multi_reviewer.sql\",\"internal/database/migrations/195_mcp_oauth_columns.sql\",\"internal/database/migrations/196_distributor_ingest.sql\",\"internal/database/migrations/196_helpdesk_config_tables.sql\",\"internal/database/migrations/197_tickets_fk_indexes.sql\",\"internal/database/migrations/200_contract_lock_state.sql\",\"internal/database/migrations/201_contract_line_exclusions.sql\",\"internal/database/migrations/202_contract_change_requests.sql\",\"internal/database/migrations/203_contract_line_audit.sql\",\"internal/database/migrations/204_nexiq_foundation.sql\",\"internal/database/migrations/205_pbr_command_control_fields.sql\",\"internal/database/migrations/206_bid_drawing_reconciliation.sql\",\"internal/database/migrations/207_bid_mode.sql\",\"internal/database/migrations/208_quote_totals_trigger.sql\",\"internal/database/migrations/209_invoice_totals_trigger.sql\",\"internal/database/migrations/210_lifecycle_events.sql\",\"internal/database/migrations/210_quote_line_client_selection.sql\",\"internal/database/migrations/211_quote_totals_effective.sql\",\"internal/database/migrations/212_phase3_purchase_orders_procurement.sql\",\"internal/database/migrations/213_po_remediation_queue.sql\",\"internal/database/migrations/214_vendor_distributor_auto_dispatch.sql\",\"internal/database/migrations/215_purchase_order_audit.sql\",\"internal/database/migrations/216_contract_line_procurement.sql\",\"internal/database/migrations/217_deprecate_project_purchase_orders.sql\",\"internal/database/migrations/218_bid_ai_scope_summary.sql\",\"internal/database/migrations/219_bid_nexie_last_error.sql\",\"internal/database/migrations/220_drawings_page_selection.sql\",\"internal/database/migrations/221_peek_summary.sql\",\"internal/database/migrations/222_bid_line_part_number.sql\",\"internal/database/migrations/223_bid_addendum_changes.sql\",\"internal/database/migrations/224_drawing_supersession.sql\",\"internal/database/migrations/225_addendum_diff_status.sql\",\"internal/database/migrations/226_addendum_audit_trail.sql\",\"internal/database/migrations/227_addendum_decision_audit.sql\",\"internal/database/migrations/228_addendum_scope_exclusion.sql\",\"internal/database/migrations/229_backfill_orphan_recon_line_items.sql\",\"internal/database/migrations/230_bid_line_scope_flags.sql\",\"internal/database/migrations/231_bid_trade_profiles.sql\",\"internal/database/migrations/232_bid_outcomes.sql\",\"internal/database/migrations/233_drawings_extracted_text.sql\",\"internal/database/migrations/234_bid_audit_acks.sql\",\"internal/database/migrations/235_tag_tables_tenant_id.sql\",\"internal/database/migrations/236_campaign_recipients_tenant_id.sql\"]},{\"name\":\"internal \u2014 devices\",\"slug\":\"internal-devices\",\"files\":[\"internal/devices/fortigate.go\",\"internal/devices/handler.go\"]},{\"name\":\"internal \u2014 dispatch\",\"slug\":\"internal-dispatch\",\"files\":[\"internal/dispatch/graph_calendar.go\",\"internal/dispatch/handler.go\",\"internal/dispatch/types.go\"]},{\"name\":\"internal \u2014 distributor\",\"slug\":\"internal-distributor\",\"files\":[\"internal/distributor/catalog_query.go\",\"internal/distributor/contract_query.go\",\"internal/distributor/handler.go\",\"internal/distributor/provisioner.go\",\"internal/distributor/scheduler.go\",\"internal/distributor/status.go\",\"internal/distributor/subs_query.go\",\"internal/distributor/sync.go\",\"internal/distributor/types.go\",\"internal/distributor/wrapper.go\"]},{\"name\":\"internal \u2014 drawing\",\"slug\":\"internal-drawing\",\"files\":[\"internal/drawing/handler.go\",\"internal/drawing/isometric.go\",\"internal/drawing/takeoff.go\",\"internal/drawing/types.go\",\"internal/drawing/vision.go\"]},{\"name\":\"internal \u2014 emailsec\",\"slug\":\"internal-emailsec\",\"files\":[\"internal/emailsec/graph.go\",\"internal/emailsec/handler.go\",\"internal/emailsec/headers.go\",\"internal/emailsec/llm_triage.go\",\"internal/emailsec/pipeline.go\",\"internal/emailsec/scoring.go\"]},{\"name\":\"internal \u2014 employee\",\"slug\":\"internal-employee\",\"files\":[\"internal/employee/handler.go\",\"internal/employee/types.go\"]},{\"name\":\"internal \u2014 epa\",\"slug\":\"internal-epa\",\"files\":[\"internal/epa/handler.go\",\"internal/epa/store.go\",\"internal/epa/types.go\"]},{\"name\":\"internal \u2014 financial\",\"slug\":\"internal-financial\",\"files\":[\"internal/financial/provider.go\",\"internal/financial/qbo/adapter.go\",\"internal/financial/qbo/provider.go\",\"internal/financial/types.go\"]},{\"name\":\"internal \u2014 health\",\"slug\":\"internal-health\",\"files\":[\"internal/health/disk_darwin.go\",\"internal/health/disk_linux.go\",\"internal/health/disk_windows.go\",\"internal/health/handler.go\"]},{\"name\":\"internal \u2014 helpdesk\",\"slug\":\"internal-helpdesk\",\"files\":[\"internal/helpdesk/capabilities.go\",\"internal/helpdesk/config_audit.go\",\"internal/helpdesk/config_handler.go\",\"internal/helpdesk/config_seed.go\",\"internal/helpdesk/contract.go\",\"internal/helpdesk/email.go\",\"internal/helpdesk/handler.go\",\"internal/helpdesk/handler_v2.go\",\"internal/helpdesk/orchestrator.go\",\"internal/helpdesk/peek_handler.go\",\"internal/helpdesk/peek_hudu.go\",\"internal/helpdesk/peek_store.go\",\"internal/helpdesk/peek_types.go\",\"internal/helpdesk/recorder_session.go\",\"internal/helpdesk/recordings_handler.go\",\"internal/helpdesk/sla.go\",\"internal/helpdesk/store.go\",\"internal/helpdesk/ticket_detail_helpers.go\",\"internal/helpdesk/tickets_prefs.go\",\"internal/helpdesk/tracking.go\",\"internal/helpdesk/types.go\",\"internal/helpdesk/types_itil.go\",\"internal/helpdesk/workflow.go\"]},{\"name\":\"internal \u2014 hudu\",\"slug\":\"internal-hudu\",\"files\":[\"internal/hudu/client.go\",\"internal/hudu/handler.go\",\"internal/hudu/types.go\"]},{\"name\":\"internal \u2014 industry\",\"slug\":\"internal-industry\",\"files\":[\"internal/industry/handler.go\"]},{\"name\":\"internal \u2014 infra\",\"slug\":\"internal-infra\",\"files\":[\"internal/infra/audit.go\",\"internal/infra/handler.go\",\"internal/infra/hudu.go\",\"internal/infra/runbook.go\",\"internal/infra/ssrf_guard.go\",\"internal/infra/types.go\",\"internal/infra/verify.go\"]},{\"name\":\"internal \u2014 itil\",\"slug\":\"internal-itil\",\"files\":[\"internal/itil/itil_handler.go\",\"internal/itil/itil_store.go\",\"internal/itil/itil_types.go\"]},{\"name\":\"internal \u2014 kb\",\"slug\":\"internal-kb\",\"files\":[\"internal/kb/handler.go\",\"internal/kb/types.go\"]},{\"name\":\"internal \u2014 legal\",\"slug\":\"internal-legal\",\"files\":[\"internal/legal/ach_submit_helpers.go\",\"internal/legal/analyze.go\",\"internal/legal/billing_handler.go\",\"internal/legal/billing_types.go\",\"internal/legal/contract_summary_generator.go\",\"internal/legal/crypto.go\",\"internal/legal/docx_renderer.go\",\"internal/legal/edit_contract_form_helpers.go\",\"internal/legal/executed_package.go\",\"internal/legal/handler.go\",\"internal/legal/msa_generator.go\",\"internal/legal/order_generator.go\",\"internal/legal/quote_conversion.go\",\"internal/legal/sign_contract_helpers.go\",\"internal/legal/signature_stamper.go\",\"internal/legal/signing_page_helpers.go\",\"internal/legal/sync.go\",\"internal/legal/types.go\"]},{\"name\":\"internal \u2014 lifecycle\",\"slug\":\"internal-lifecycle\",\"files\":[\"internal/lifecycle/lifecycle.go\"]},{\"name\":\"internal \u2014 mcpclient\",\"slug\":\"internal-mcpclient\",\"files\":[\"internal/mcpclient/client.go\"]},{\"name\":\"internal \u2014 mcpvendors\",\"slug\":\"internal-mcpvendors\",\"files\":[\"internal/mcpvendors/factory.go\",\"internal/mcpvendors/pax8/pax8.go\",\"internal/mcpvendors/resolver.go\"]},{\"name\":\"internal \u2014 metasploit\",\"slug\":\"internal-metasploit\",\"files\":[\"internal/metasploit/client.go\",\"internal/metasploit/handler.go\",\"internal/metasploit/types.go\"]},{\"name\":\"internal \u2014 middleware\",\"slug\":\"internal-middleware\",\"files\":[\"internal/middleware/csrf.go\",\"internal/middleware/middleware.go\"]},{\"name\":\"internal \u2014 mobile\",\"slug\":\"internal-mobile\",\"files\":[\"internal/mobile/handler.go\"]},{\"name\":\"internal \u2014 nexie\",\"slug\":\"internal-nexie\",\"files\":[\"internal/nexie/handler.go\",\"internal/nexie/query.go\",\"internal/nexie/schema.go\"]},{\"name\":\"internal \u2014 nexiq\",\"slug\":\"internal-nexiq\",\"files\":[\"internal/nexiq/cascade.go\",\"internal/nexiq/cockpit.go\",\"internal/nexiq/events.go\",\"internal/nexiq/events_test.go\",\"internal/nexiq/goals.go\",\"internal/nexiq/gut_check.go\",\"internal/nexiq/handler.go\",\"internal/nexiq/pages.go\",\"internal/nexiq/permissions.go\",\"internal/nexiq/scenarios.go\",\"internal/nexiq/stubs.go\",\"internal/nexiq/workspace.go\"]},{\"name\":\"internal \u2014 notify\",\"slug\":\"internal-notify\",\"files\":[\"internal/notify/service.go\"]},{\"name\":\"internal \u2014 onboarding\",\"slug\":\"internal-onboarding\",\"files\":[\"internal/onboarding/blueprint_injection.go\",\"internal/onboarding/handler.go\",\"internal/onboarding/phases.go\",\"internal/onboarding/types.go\"]},{\"name\":\"internal \u2014 performance\",\"slug\":\"internal-performance\",\"files\":[\"internal/performance/analyzer.go\",\"internal/performance/calculator.go\",\"internal/performance/collector.go\",\"internal/performance/handler.go\",\"internal/performance/qbo_mapper.go\",\"internal/performance/store.go\",\"internal/performance/types.go\"]},{\"name\":\"internal \u2014 po\",\"slug\":\"internal-po\",\"files\":[\"internal/po/handler.go\"]},{\"name\":\"internal \u2014 portal\",\"slug\":\"internal-portal\",\"files\":[\"internal/portal/admin.go\",\"internal/portal/handler.go\",\"internal/portal/middleware.go\",\"internal/portal/nexie.go\",\"internal/portal/nexie_tools.go\",\"internal/portal/pages.go\",\"internal/portal/risk_acceptances.go\",\"internal/portal/store.go\",\"internal/portal/types.go\",\"internal/portal/usage.go\"]},{\"name\":\"internal \u2014 procurement\",\"slug\":\"internal-procurement\",\"files\":[\"internal/procurement/audit.go\",\"internal/procurement/handler.go\",\"internal/procurement/remediation.go\",\"internal/procurement/service.go\",\"internal/procurement/types.go\"]},{\"name\":\"internal \u2014 product\",\"slug\":\"internal-product\",\"files\":[\"internal/product/handler.go\",\"internal/product/project_handler.go\",\"internal/product/types.go\",\"internal/product/types_project.go\"]},{\"name\":\"internal \u2014 project\",\"slug\":\"internal-project\",\"files\":[\"internal/project/costing.go\",\"internal/project/evm.go\",\"internal/project/handler.go\",\"internal/project/safe_columns.go\",\"internal/project/types.go\"]},{\"name\":\"internal \u2014 push\",\"slug\":\"internal-push\",\"files\":[\"internal/push/handler.go\"]},{\"name\":\"internal \u2014 quickbooks\",\"slug\":\"internal-quickbooks\",\"files\":[\"internal/quickbooks/client.go\",\"internal/quickbooks/employees_sync.go\",\"internal/quickbooks/oauth.go\",\"internal/quickbooks/po_adapter.go\",\"internal/quickbooks/push.go\",\"internal/quickbooks/recurring_billing.go\",\"internal/quickbooks/service.go\",\"internal/quickbooks/sync.go\",\"internal/quickbooks/timesheet_push.go\",\"internal/quickbooks/types.go\"]},{\"name\":\"internal \u2014 rbac\",\"slug\":\"internal-rbac\",\"files\":[\"internal/rbac/roles.go\"]},{\"name\":\"internal \u2014 review\",\"slug\":\"internal-review\",\"files\":[\"internal/review/binder.go\",\"internal/review/broker.go\",\"internal/review/handler.go\",\"internal/review/qr.go\",\"internal/review/repository.go\",\"internal/review/security.go\",\"internal/review/seeder.go\",\"internal/review/types.go\"]},{\"name\":\"internal \u2014 risk\",\"slug\":\"internal-risk\",\"files\":[\"internal/risk/handler.go\",\"internal/risk/print.go\",\"internal/risk/registry.go\",\"internal/risk/service.go\",\"internal/risk/types.go\"]},{\"name\":\"internal \u2014 rmm\",\"slug\":\"internal-rmm\",\"files\":[\"internal/rmm/agent_api.go\",\"internal/rmm/agent_update.go\",\"internal/rmm/crypto/tls.go\",\"internal/rmm/device_api.go\",\"internal/rmm/enroll.go\",\"internal/rmm/handler.go\",\"internal/rmm/install.go\",\"internal/rmm/installers/linux.sh\",\"internal/rmm/installers/macos.sh\",\"internal/rmm/installers/nexusos-agent.wxs.tmpl\",\"internal/rmm/installers/windows.ps1\",\"internal/rmm/msi.go\",\"internal/rmm/protocol/compliance.go\",\"internal/rmm/protocol/inventory.go\",\"internal/rmm/protocol/types.go\",\"internal/rmm/recorder.go\",\"internal/rmm/recordings.go\",\"internal/rmm/remote/desktop.go\",\"internal/rmm/remote/session.go\",\"internal/rmm/remote/tunnel.go\",\"internal/rmm/sentinel.go\",\"internal/rmm/sentinel_socks_hub.go\",\"internal/rmm/session_token.go\",\"internal/rmm/types.go\",\"internal/rmm/update_api.go\"]},{\"name\":\"internal \u2014 sentinel\",\"slug\":\"internal-sentinel\",\"files\":[\"internal/sentinel/capability.go\",\"internal/sentinel/secrets.go\",\"internal/sentinel/types.go\"]},{\"name\":\"internal \u2014 settings\",\"slug\":\"internal-settings\",\"files\":[\"internal/settings/appearance_handler.go\",\"internal/settings/tab_order_handler.go\",\"internal/settings/tab_visibility_handler.go\"]},{\"name\":\"internal \u2014 siem\",\"slug\":\"internal-siem\",\"files\":[\"internal/siem/handler.go\",\"internal/siem/pcap.go\",\"internal/siem/syslog.go\",\"internal/siem/types.go\"]},{\"name\":\"internal \u2014 survey\",\"slug\":\"internal-survey\",\"files\":[\"internal/survey/audio.go\",\"internal/survey/handler.go\",\"internal/survey/types.go\",\"internal/survey/vision.go\",\"internal/survey/voice.go\"]},{\"name\":\"internal \u2014 tenant\",\"slug\":\"internal-tenant\",\"files\":[\"internal/tenant/handler.go\",\"internal/tenant/mcp_oauth.go\"]},{\"name\":\"internal \u2014 timer\",\"slug\":\"internal-timer\",\"files\":[\"internal/timer/billing_context_helpers.go\",\"internal/timer/escalate_helpers.go\",\"internal/timer/handler.go\",\"internal/timer/list_timers_helpers.go\",\"internal/timer/stop_helpers.go\"]},{\"name\":\"internal \u2014 timesheet\",\"slug\":\"internal-timesheet\",\"files\":[\"internal/timesheet/handler.go\",\"internal/timesheet/store.go\",\"internal/timesheet/types.go\"]},{\"name\":\"internal \u2014 ui\",\"slug\":\"internal-ui\",\"files\":[\"internal/ui/preferences.go\",\"internal/ui/render.go\",\"internal/ui/render_v2_test.go\",\"internal/ui/static/css/epa.css\",\"internal/ui/static/css/helpdesk-glass.css\",\"internal/ui/static/css/helpdesk-peek.css\",\"internal/ui/static/css/nexus.css\",\"internal/ui/static/css/theme-mixer.css\",\"internal/ui/static/icons/placeholder.txt\",\"internal/ui/static/js/app.js\",\"internal/ui/static/js/csrf.js\",\"internal/ui/static/js/sigpad.js\",\"internal/ui/static/js/sw.js\",\"internal/ui/static/manifest-mobile.json\",\"internal/ui/static/manifest.json\",\"internal/ui/templates/assessment/detail.html\",\"internal/ui/templates/assessment/form.html\",\"internal/ui/templates/assessment/list.html\",\"internal/ui/templates/auth/invite.html\",\"internal/ui/templates/auth/login.html\",\"internal/ui/templates/bidding/audit.html\",\"internal/ui/templates/bidding/bid_form.html\",\"internal/ui/templates/bidding/calibration.html\",\"internal/ui/templates/bidding/cost_engine.html\",\"internal/ui/templates/bidding/detail.html\",\"internal/ui/templates/bidding/drawing_selection.html\",\"internal/ui/templates/bidding/list.html\",\"internal/ui/templates/bidding/print_detail.html\",\"internal/ui/templates/bidding/print_shadow.html\",\"internal/ui/templates/bidding/settings.html\",\"internal/ui/templates/bidding/takeoff.html\",\"internal/ui/templates/bidding/vision_audit.html\",\"internal/ui/templates/billing/aging.html\",\"internal/ui/templates/billing/invoice_detail.html\",\"internal/ui/templates/billing/invoice_edit.html\",\"internal/ui/templates/billing/invoice_form.html\",\"internal/ui/templates/billing/invoice_print.html\",\"internal/ui/templates/billing/invoices.html\",\"internal/ui/templates/billing/quote_approve.html\",\"internal/ui/templates/billing/quote_detail.html\",\"internal/ui/templates/billing/quote_edit.html\",\"internal/ui/templates/billing/quote_form.html\",\"internal/ui/templates/billing/quote_print.html\",\"internal/ui/templates/billing/quotes.html\",\"internal/ui/templates/billing/transcript_import.html\",\"internal/ui/templates/cmmc/assessments.html\",\"internal/ui/templates/cmmc/connect.html\",\"internal/ui/templates/cmmc/control_detail.html\",\"internal/ui/templates/cmmc/controls.html\",\"internal/ui/templates/cmmc/cui_scope.html\",\"internal/ui/templates/cmmc/dashboard.html\",\"internal/ui/templates/cmmc/dispositions.html\",\"internal/ui/templates/cmmc/evidence_guide.html\",\"internal/ui/templates/cmmc/poam.html\",\"internal/ui/templates/cmmc/select_company.html\",\"internal/ui/templates/cmmc/ssp.html\",\"internal/ui/templates/compliance/approve.html\",\"internal/ui/templates/compliance/client_overview.html\",\"internal/ui/templates/compliance/device_detail.html\",\"internal/ui/templates/compliance/documents.html\",\"internal/ui/templates/compliance/frameworks.html\",\"internal/ui/templates/compliance/gap_analysis.html\",\"internal/ui/templates/composer/client_hub.html\",\"internal/ui/templates/composer/controls.html\",\"internal/ui/templates/composer/dashboard.html\",\"internal/ui/templates/composer/dispositions.html\",\"internal/ui/templates/composer/framework_clients.html\",\"internal/ui/templates/composer/hub.html\",\"internal/ui/templates/composer/live_review_setup.html\",\"internal/ui/templates/composer/ncsr/action_plan.html\",\"internal/ui/templates/composer/ncsr/assessment.html\",\"internal/ui/templates/composer/ncsr/crm_compliance.html\",\"internal/ui/templates/composer/ncsr/landing.html\",\"internal/ui/templates/composer/ncsr/reviews.html\",\"internal/ui/templates/composer/poam.html\",\"internal/ui/templates/composer/reviews_history.html\",\"internal/ui/templates/crm/activities.html\",\"internal/ui/templates/crm/activity_form.html\",\"internal/ui/templates/crm/campaign_detail.html\",\"internal/ui/templates/crm/campaign_form.html\",\"internal/ui/templates/crm/campaigns.html\",\"internal/ui/templates/crm/client_detail.html\",\"internal/ui/templates/crm/client_form.html\",\"internal/ui/templates/crm/clients.html\",\"internal/ui/templates/crm/contact_detail.html\",\"internal/ui/templates/crm/contact_form.html\",\"internal/ui/templates/crm/contacts.html\",\"internal/ui/templates/crm/deal_form.html\",\"internal/ui/templates/crm/pipeline.html\",\"internal/ui/templates/cybersec/dashboard.html\",\"internal/ui/templates/cybersec/scan_detail.html\",\"internal/ui/templates/cybersec/scan_form.html\",\"internal/ui/templates/cybersec/scan_library.html\",\"internal/ui/templates/cybersec/scan_report.html\",\"internal/ui/templates/cybersec/scans.html\",\"internal/ui/templates/cybersec/sessions.html\",\"internal/ui/templates/cybersec/siem.html\",\"internal/ui/templates/dashboard/index.html\",\"internal/ui/templates/dispatch/board.html\",\"internal/ui/templates/dispatch/calendar.html\",\"internal/ui/templates/dispatch/kanban.html\",\"internal/ui/templates/dispatch/list.html\",\"internal/ui/templates/dispatch/map.html\",\"internal/ui/templates/dispatch/settings.html\",\"internal/ui/templates/dispatch/tv.html\",\"internal/ui/templates/dispatch/unassigned.html\",\"internal/ui/templates/distributor/catalog.html\",\"internal/ui/templates/distributor/ccr_modal.html\",\"internal/ui/templates/distributor/contract_section.html\",\"internal/ui/templates/distributor/subs_partial.html\",\"internal/ui/templates/drawing/detail.html\",\"internal/ui/templates/drawing/form.html\",\"internal/ui/templates/drawing/list.html\",\"internal/ui/templates/drawing/view3d.html\",\"internal/ui/templates/employee/detail.html\",\"internal/ui/templates/employee/list.html\",\"internal/ui/templates/epa/company_tab.html\",\"internal/ui/templates/epa/dashboard.html\",\"internal/ui/templates/helpdesk/_v2_asset_console_real.html\",\"internal/ui/templates/helpdesk/_v2_card_drawer.html\",\"internal/ui/templates/helpdesk/_v2_kebab_menu.html\",\"internal/ui/templates/helpdesk/_v2_panel_asset.html\",\"internal/ui/templates/helpdesk/_v2_panel_contact.html\",\"internal/ui/templates/helpdesk/_v2_panel_contract.html\",\"internal/ui/templates/helpdesk/_v2_panel_conversation.html\",\"internal/ui/templates/helpdesk/_v2_panel_customer.html\",\"internal/ui/templates/helpdesk/change_detail.html\",\"internal/ui/templates/helpdesk/change_form.html\",\"internal/ui/templates/helpdesk/cmdb_detail.html\",\"internal/ui/templates/helpdesk/cmdb_form.html\",\"internal/ui/templates/helpdesk/contract_detail.html\",\"internal/ui/templates/helpdesk/contract_form.html\",\"internal/ui/templates/helpdesk/contracts.html\",\"internal/ui/templates/helpdesk/email_templates.html\",\"internal/ui/templates/helpdesk/kanban.html\",\"internal/ui/templates/helpdesk/peek.html\",\"internal/ui/templates/helpdesk/problem_detail.html\",\"internal/ui/templates/helpdesk/problem_form.html\",\"internal/ui/templates/helpdesk/service_form.html\",\"internal/ui/templates/helpdesk/services.html\",\"internal/ui/templates/helpdesk/ticket_detail.html\",\"internal/ui/templates/helpdesk/ticket_detail_v2.html\",\"internal/ui/templates/helpdesk/ticket_form.html\",\"internal/ui/templates/helpdesk/ticket_preview.html\",\"internal/ui/templates/helpdesk/tickets.html\",\"internal/ui/templates/helpdesk/tickets_brief.html\",\"internal/ui/templates/industry/detail.html\",\"internal/ui/templates/industry/list.html\",\"internal/ui/templates/infra/dashboard.html\",\"internal/ui/templates/infra/report.html\",\"internal/ui/templates/infra/select_company.html\",\"internal/ui/templates/itil/changes.html\",\"internal/ui/templates/itil/cmdb.html\",\"internal/ui/templates/itil/problems.html\",\"internal/ui/templates/kb/article.html\",\"internal/ui/templates/kb/article_form.html\",\"internal/ui/templates/kb/articles.html\",\"internal/ui/templates/kb/search.html\",\"internal/ui/templates/layouts/base.html\",\"internal/ui/templates/layouts/mobile.html\",\"internal/ui/templates/legal/contract_detail.html\",\"internal/ui/templates/legal/contract_form.html\",\"internal/ui/templates/legal/contract_sign.html\",\"internal/ui/templates/legal/contracts.html\",\"internal/ui/templates/mobile/agent_detail.html\",\"internal/ui/templates/mobile/agents.html\",\"internal/ui/templates/mobile/alerts.html\",\"internal/ui/templates/mobile/clients.html\",\"internal/ui/templates/mobile/dashboard.html\",\"internal/ui/templates/mobile/invoices.html\",\"internal/ui/templates/mobile/project_detail.html\",\"internal/ui/templates/mobile/projects.html\",\"internal/ui/templates/mobile/purchase_order_new.html\",\"internal/ui/templates/mobile/purchase_orders.html\",\"internal/ui/templates/mobile/security.html\",\"internal/ui/templates/mobile/settings.html\",\"internal/ui/templates/mobile/ticket_detail.html\",\"internal/ui/templates/mobile/ticket_new.html\",\"internal/ui/templates/mobile/tickets.html\",\"internal/ui/templates/mobile/time.html\",\"internal/ui/templates/mobile/timesheet.html\",\"internal/ui/templates/nexiq/gut_check.html\",\"internal/ui/templates/nexiq/index.html\",\"internal/ui/templates/nexiq/lever_lab.html\",\"internal/ui/templates/nexiq/pbr_sheets.html\",\"internal/ui/templates/nexiq/plan_cascade.html\",\"internal/ui/templates/nexiq/sales_cockpit.html\",\"internal/ui/templates/nexiq/sales_workspace.html\",\"internal/ui/templates/notifications/center.html\",\"internal/ui/templates/onboarding/detail.html\",\"internal/ui/templates/onboarding/list.html\",\"internal/ui/templates/onboarding/new.html\",\"internal/ui/templates/partials/brief_body.html\",\"internal/ui/templates/partials/compliance_stepper.html\",\"internal/ui/templates/partials/drag_grip.html\",\"internal/ui/templates/partials/photo_analysis_card.html\",\"internal/ui/templates/partials/portal_shell.html\",\"internal/ui/templates/partials/pulse_nav.html\",\"internal/ui/templates/partials/stats.html\",\"internal/ui/templates/performance/counterbalance.html\",\"internal/ui/templates/performance/dashboard.html\",\"internal/ui/templates/performance/enterprise_value.html\",\"internal/ui/templates/performance/input_editor.html\",\"internal/ui/templates/performance/mrr_context.html\",\"internal/ui/templates/performance/report.html\",\"internal/ui/templates/performance/report_print.html\",\"internal/ui/templates/performance/reports.html\",\"internal/ui/templates/performance/salaries.html\",\"internal/ui/templates/performance/section.html\",\"internal/ui/templates/performance/settings.html\",\"internal/ui/templates/performance/tools.html\",\"internal/ui/templates/performance/trends.html\",\"internal/ui/templates/po/form.html\",\"internal/ui/templates/po/list.html\",\"internal/ui/templates/po/print.html\",\"internal/ui/templates/po/quick.html\",\"internal/ui/templates/po/settings.html\",\"internal/ui/templates/portal/contract_detail.html\",\"internal/ui/templates/portal/contracts.html\",\"internal/ui/templates/portal/forbidden.html\",\"internal/ui/templates/portal/invoice_detail.html\",\"internal/ui/templates/portal/invoices.html\",\"internal/ui/templates/portal/kb.html\",\"internal/ui/templates/portal/kb_article.html\",\"internal/ui/templates/portal/landing.html\",\"internal/ui/templates/portal/login.html\",\"internal/ui/templates/portal/profile.html\",\"internal/ui/templates/portal/quote_detail.html\",\"internal/ui/templates/portal/quotes.html\",\"internal/ui/templates/portal/risk_acceptance_sign.html\",\"internal/ui/templates/portal/risk_acceptances.html\",\"internal/ui/templates/portal/support_consent.html\",\"internal/ui/templates/portal/ticket_detail.html\",\"internal/ui/templates/portal/ticket_new.html\",\"internal/ui/templates/portal/tickets.html\",\"internal/ui/templates/procurement/detail.html\",\"internal/ui/templates/procurement/list.html\",\"internal/ui/templates/procurement/remediation.html\",\"internal/ui/templates/products/assignments.html\",\"internal/ui/templates/products/catalog.html\",\"internal/ui/templates/products/product_detail.html\",\"internal/ui/templates/products/product_form.html\",\"internal/ui/templates/products/product_legal_docs_partial.html\",\"internal/ui/templates/projects/board.html\",\"internal/ui/templates/projects/cr_sign.html\",\"internal/ui/templates/projects/detail.html\",\"internal/ui/templates/projects/list.html\",\"internal/ui/templates/projects/project_form.html\",\"internal/ui/templates/projects/tabs/budget.html\",\"internal/ui/templates/projects/tabs/changes.html\",\"internal/ui/templates/projects/tabs/costs.html\",\"internal/ui/templates/projects/tabs/evm.html\",\"internal/ui/templates/projects/tabs/gantt.html\",\"internal/ui/templates/projects/tabs/lessons.html\",\"internal/ui/templates/projects/tabs/notes.html\",\"internal/ui/templates/projects/tabs/overview.html\",\"internal/ui/templates/projects/tabs/profitability.html\",\"internal/ui/templates/projects/tabs/risks.html\",\"internal/ui/templates/projects/tabs/stakeholders.html\",\"internal/ui/templates/projects/tabs/tasks.html\",\"internal/ui/templates/projects/tabs/team.html\",\"internal/ui/templates/review/binder.html\",\"internal/ui/templates/review/host.html\",\"internal/ui/templates/review/index.html\",\"internal/ui/templates/review/live.html\",\"internal/ui/templates/risk/modal.html\",\"internal/ui/templates/risk/register.html\",\"internal/ui/templates/rmm/agent_detail.html\",\"internal/ui/templates/rmm/agents.html\",\"internal/ui/templates/rmm/dashboard.html\",\"internal/ui/templates/rmm/deploy.html\",\"internal/ui/templates/rmm/desktop.html\",\"internal/ui/templates/rmm/recording_detail.html\",\"internal/ui/templates/rmm/recordings.html\",\"internal/ui/templates/rmm/sentinel.html\",\"internal/ui/templates/rmm/terminal.html\",\"internal/ui/templates/settings/ai_usage.html\",\"internal/ui/templates/settings/api_keys.html\",\"internal/ui/templates/settings/appearance.html\",\"internal/ui/templates/settings/audit.html\",\"internal/ui/templates/settings/billing.html\",\"internal/ui/templates/settings/certificates.html\",\"internal/ui/templates/settings/escalation.html\",\"internal/ui/templates/settings/health.html\",\"internal/ui/templates/settings/helpdesk.html\",\"internal/ui/templates/settings/integrations.html\",\"internal/ui/templates/settings/legal.html\",\"internal/ui/templates/settings/mcp.html\",\"internal/ui/templates/settings/metasploit.html\",\"internal/ui/templates/settings/mfa_setup.html\",\"internal/ui/templates/settings/orchestrator.html\",\"internal/ui/templates/settings/portal_roles.html\",\"internal/ui/templates/settings/profile.html\",\"internal/ui/templates/settings/qb_mappings.html\",\"internal/ui/templates/settings/quickbooks.html\",\"internal/ui/templates/settings/sessions.html\",\"internal/ui/templates/settings/tenant.html\",\"internal/ui/templates/settings/users.html\",\"internal/ui/templates/settings/voip.html\",\"internal/ui/templates/survey/audio_recordings.html\",\"internal/ui/templates/survey/departure_gate.html\",\"internal/ui/templates/survey/detail.html\",\"internal/ui/templates/survey/form.html\",\"internal/ui/templates/survey/list.html\",\"internal/ui/templates/survey/photo_analysis.html\",\"internal/ui/templates/survey/photo_analysis_detail.html\",\"internal/ui/templates/survey/photo_analysis_results.html\",\"internal/ui/templates/survey/print.html\",\"internal/ui/templates/survey/settings.html\",\"internal/ui/templates/survey/template_edit.html\",\"internal/ui/templates/survey/walkthrough.html\",\"internal/ui/templates/timesheet/all.html\",\"internal/ui/templates/timesheet/approvals.html\",\"internal/ui/templates/timesheet/detail.html\",\"internal/ui/templates/timesheet/my_timesheet.html\",\"internal/ui/templates/workorder/detail.html\",\"internal/ui/templates/workorder/form.html\",\"internal/ui/templates/workorder/list.html\",\"internal/ui/theme_mixer_test.go\"]},{\"name\":\"internal \u2014 version\",\"slug\":\"internal-version\",\"files\":[\"internal/version/version.go\"]},{\"name\":\"internal \u2014 voip\",\"slug\":\"internal-voip\",\"files\":[\"internal/voip/handler.go\",\"internal/voip/types.go\"]},{\"name\":\"internal \u2014 workorder\",\"slug\":\"internal-workorder\",\"files\":[\"internal/workorder/handler.go\",\"internal/workorder/types.go\"]}]},{\"name\":\"mockups\",\"slug\":\"mockups\",\"files\":[],\"children\":[{\"name\":\"mockups \u2014 mockups\",\"slug\":\"mockups-mockups\",\"files\":[\"mockups/bid_drawing_page_selection.html\",\"mockups/crm_client_compliance_tab.html\",\"mockups/distributor_s1_vendor_settings.html\",\"mockups/distributor_s2_catalog_browser.html\",\"mockups/distributor_s3_client_subscriptions.html\",\"mockups/distributor_s4_contract_distributor_lines.html\",\"mockups/distributor_s5_product_catalog.html\",\"mockups/distributor_s6_invoice_distributor_cost.html\",\"mockups/ncsr_action_plan_post_review.html\",\"mockups/ncsr_assessment_full_page.html\",\"mockups/ncsr_maturity_trajectory.html\",\"mockups/ncsr_post_review_overview.html\",\"mockups/ncsr_reviews_history_tab.html\",\"mockups/ncsr_sectioned_multi_reviewer.html\",\"mockups/pick-pages-hover.html\",\"mockups/pick-pages-mockup.html\",\"mockups/pick-pages-placement.html\",\"mockups/pick-pages-redesign.html\",\"mockups/slideout_mockup.html\"]},{\"name\":\"mockups \u2014 nexusiq\",\"slug\":\"mockups-nexusiq\",\"files\":[\"mockups/nexusiq/ARCHITECTURE.md\",\"mockups/nexusiq/gut_check.html\",\"mockups/nexusiq/index.html\",\"mockups/nexusiq/lever_lab.html\",\"mockups/nexusiq/plan_cascade.html\",\"mockups/nexusiq/proposal_v2_sales_cockpit.html\",\"mockups/nexusiq/proposal_v2_sales_workspace.html\",\"mockups/nexusiq/proposal_v3_pbr_dense_grid.html\",\"mockups/nexusiq/proposal_v3b_pbr_full_sheet.html\",\"mockups/nexusiq/sales_cockpit.html\",\"mockups/nexusiq/sales_workspace.html\"]}]},{\"name\":\"scripts\",\"slug\":\"scripts\",\"files\":[\"scripts/changelog/main.go\",\"scripts/check-tenant-id.sh\",\"scripts/proxmox-deploy.sh\",\"scripts/security_gen/main.go\",\"scripts/seed.go\"]}];\nvar META = {\"fromCommit\":\"43c92f159b145209223d9d356cde48096b752c07\",\"generatedAt\":\"2026-05-25T10:26:14.152Z\",\"model\":\"claude-haiku-4-5-20251001\",\"moduleFiles\":{\"Root\":[\"AGENTS.md\",\"CLAUDE.md\",\"Dockerfile\",\"Makefile\",\"docker-compose.dev.yml\",\"docker-compose.yml\",\"go.mod\"],\"branding\":[\"branding/BRAND_GUIDELINES.md\",\"branding/COLOR_PALETTE.md\",\"branding/adobe_firefly_prompt.md\",\"branding/landing_page_sample.html\",\"branding/letterhead_sample_1.html\",\"branding/letterhead_sample_2.html\",\"branding/letterhead_sample_3.html\",\"branding/nexoscore_logo_v1.html\",\"branding/nexoscore_logo_v2.html\",\"branding/typography_sample_1.html\",\"branding/typography_sample_2.html\",\"branding/typography_sample_3.html\",\"branding/typography_sample_4a.html\",\"branding/typography_sample_4b.html\",\"branding/typography_sample_4c.html\",\"branding/typography_sample_4d.html\"],\"cmd\":[\"cmd/auth-gate/main.go\",\"cmd/psa/main.go\",\"cmd/qb-push-items/main.go\"],\"docs\":[\"docs/2026-04-07-Meeting-Email-Security-Flow.md\",\"docs/API_REFERENCE.md\",\"docs/ARCHITECTURE.md\",\"docs/AUDIT_CRM_TENANT_SCOPE.md\",\"docs/BASE_HTML_MIGRATION_PLAN.md\",\"docs/BID_ENGINE_TEST_GUIDE.md\",\"docs/CMMC_ONE_CLICK_COMPLIANCE_PLAN.md\",\"docs/CMMC_ONE_CLICK_TEST_GUIDE.md\",\"docs/CODEBASE_OBSERVATIONS.md\",\"docs/COMPLIANCE_COMPOSER_PLAN.md\",\"docs/DEPENDENCIES-EXTERNAL.md\",\"docs/DEPENDENCIES.md\",\"docs/DISTRIBUTOR_PHASE3_PROCUREMENT_PLAN.md\",\"docs/EMAIL_SECURITY_TRIAGE_PLAN.md\",\"docs/INFRASTRUCTURE.md\",\"docs/INVENTORY.md\",\"docs/LIVE_REVIEW_ENGINE_MANUAL.md\",\"docs/NCSR_INTEGRATION_PLAN.md\",\"docs/NexusOS_Orchestrator_Architecture.md\",\"docs/NexusOS_Sales_Lifecycle_Automation_Plan.md\",\"docs/NexusOS_Sales_Lifecycle_Test_Plan.md\",\"docs/NexusRMM-Status-Update-Jon.md\",\"docs/ONBOARDING.md\",\"docs/Orchestrator_Hex_Detail_UI_Artifact.md\",\"docs/PRODUCTS_SERVICES_CATALOG.md\",\"docs/PROJECT_MANAGEMENT.md\",\"docs/PR_A_Helpdesk-Peek.md\",\"docs/PR_B_Helpdesk-Hub.md\",\"docs/Phase9_Action_Configuration_Plan.md\",\"docs/RISK_ACCEPTANCE_DISPOSITION_GUIDE.md\",\"docs/ROADMAP.md\",\"docs/gen_ach_user_guide.py\",\"docs/gen_ach_whitepaper.py\",\"docs/gen_onboarding_guide.py\",\"docs/gen_onboarding_user_guide.py\",\"docs/gen_schema_poster.py\",\"docs/lifecycle-test-manual.md\",\"docs/orchestrator_ui_enhancement_artifact.md\",\"docs/teleport-deployment-guide.md\",\"docs/tickets-er-diagram.html\",\"docs/work-order-manual.md\",\"docs/architecture/ARCHITECTURE.md\",\"docs/architecture/PROJECT_STATUS.md\",\"docs/architecture/REMOTE_DESKTOP_SPEC.md\",\"docs/guide/MULTI_REVIEWER_TEST_GUIDE.md\",\"docs/guide/NCSR_USER_GUIDE.md\",\"docs/guide/PHASE3_PROCUREMENT_TESTING_GUIDE.md\",\"docs/guide/README.md\",\"docs/guide/RISK_ACCEPTANCE_USER_GUIDE.md\",\"docs/guide/billing-contracts.md\",\"docs/guide/dispatch.md\",\"docs/guide/employee-hr.md\",\"docs/guide/projects.md\",\"docs/integrations/BACKLOG.md\",\"docs/mockups/brief_mockup_v2.html\",\"docs/mockups/live_review_generalization.html\",\"docs/mockups/phase3/_shared.css\",\"docs/mockups/phase3/agreement-saas.html\",\"docs/mockups/phase3/contract-materials.html\",\"docs/mockups/phase3/index.html\",\"docs/mockups/phase3/procurement-detail.html\",\"docs/mockups/phase3/procurement-list.html\",\"docs/mockups/phase3/project-po-integration.html\",\"docs/mockups/phase3/remediation-queue.html\",\"docs/mockups/phase3/settings-distributors.html\",\"docs/mockups/phase3/ticket-po-integration.html\",\"docs/mockups/review_host_completion.html\",\"docs/mockups/review_ipad_completion.html\",\"docs/modules/auth/API.md\",\"docs/modules/auth/MANUAL.md\",\"docs/modules/auth/SOURCE_MAP.md\",\"docs/modules/auth/TODO.md\",\"docs/modules/auth/WHITEPAPER.md\",\"docs/modules/billing/API.md\",\"docs/modules/billing/MANUAL.md\",\"docs/modules/billing/SOURCE_MAP.md\",\"docs/modules/billing/TODO.md\",\"docs/modules/billing/WHITEPAPER.md\",\"docs/modules/certificates/API.md\",\"docs/modules/certificates/MANUAL.md\",\"docs/modules/certificates/SOURCE_MAP.md\",\"docs/modules/certificates/TODO.md\",\"docs/modules/certificates/WHITEPAPER.md\",\"docs/modules/compliance/API.md\",\"docs/modules/compliance/MANUAL.md\",\"docs/modules/compliance/SOURCE_MAP.md\",\"docs/modules/compliance/TODO.md\",\"docs/modules/compliance/WHITEPAPER.md\",\"docs/modules/crm/API.md\",\"docs/modules/crm/MANUAL.md\",\"docs/modules/crm/SOURCE_MAP.md\",\"docs/modules/crm/TODO.md\",\"docs/modules/crm/WHITEPAPER.md\",\"docs/modules/cybersec/API.md\",\"docs/modules/cybersec/MANUAL.md\",\"docs/modules/cybersec/SOURCE_MAP.md\",\"docs/modules/cybersec/TODO.md\",\"docs/modules/cybersec/WHITEPAPER.md\",\"docs/modules/devices/API.md\",\"docs/modules/devices/MANUAL.md\",\"docs/modules/devices/SOURCE_MAP.md\",\"docs/modules/devices/TODO.md\",\"docs/modules/devices/WHITEPAPER.md\",\"docs/modules/email-security/API.md\",\"docs/modules/email-security/MANUAL.md\",\"docs/modules/email-security/SOURCE_MAP.md\",\"docs/modules/email-security/TODO.md\",\"docs/modules/email-security/WHITEPAPER.md\",\"docs/modules/health/API.md\",\"docs/modules/health/MANUAL.md\",\"docs/modules/health/SOURCE_MAP.md\",\"docs/modules/health/TODO.md\",\"docs/modules/health/WHITEPAPER.md\",\"docs/modules/helpdesk/API.md\",\"docs/modules/helpdesk/MANUAL.md\",\"docs/modules/helpdesk/SOURCE_MAP.md\",\"docs/modules/helpdesk/TODO.md\",\"docs/modules/helpdesk/WHITEPAPER.md\",\"docs/modules/hudu/API.md\",\"docs/modules/hudu/MANUAL.md\",\"docs/modules/hudu/SOURCE_MAP.md\",\"docs/modules/hudu/TODO.md\",\"docs/modules/hudu/WHITEPAPER.md\",\"docs/modules/knowledge-base/API.md\",\"docs/modules/knowledge-base/MANUAL.md\",\"docs/modules/knowledge-base/SOURCE_MAP.md\",\"docs/modules/knowledge-base/TODO.md\",\"docs/modules/knowledge-base/WHITEPAPER.md\",\"docs/modules/legal/API.md\",\"docs/modules/legal/MANUAL.md\",\"docs/modules/legal/SOURCE_MAP.md\",\"docs/modules/legal/TODO.md\",\"docs/modules/legal/WHITEPAPER.md\",\"docs/modules/metasploit/API.md\",\"docs/modules/metasploit/MANUAL.md\",\"docs/modules/metasploit/SOURCE_MAP.md\",\"docs/modules/metasploit/TODO.md\",\"docs/modules/metasploit/WHITEPAPER.md\",\"docs/modules/mobile/API.md\",\"docs/modules/mobile/MANUAL.md\",\"docs/modules/mobile/SOURCE_MAP.md\",\"docs/modules/mobile/TODO.md\",\"docs/modules/mobile/WHITEPAPER.md\",\"docs/modules/nexie-ai/API.md\",\"docs/modules/nexie-ai/MANUAL.md\",\"docs/modules/nexie-ai/SOURCE_MAP.md\",\"docs/modules/nexie-ai/TODO.md\",\"docs/modules/nexie-ai/WHITEPAPER.md\",\"docs/modules/onboarding/API.md\",\"docs/modules/onboarding/MANUAL.md\",\"docs/modules/onboarding/SOURCE_MAP.md\",\"docs/modules/onboarding/TODO.md\",\"docs/modules/onboarding/WHITEPAPER.md\",\"docs/modules/products/API.md\",\"docs/modules/products/MANUAL.md\",\"docs/modules/products/SOURCE_MAP.md\",\"docs/modules/products/TODO.md\",\"docs/modules/products/WHITEPAPER.md\",\"docs/modules/projects/API.md\",\"docs/modules/projects/MANUAL.md\",\"docs/modules/projects/SOURCE_MAP.md\",\"docs/modules/projects/TODO.md\",\"docs/modules/projects/WHITEPAPER.md\",\"docs/modules/purchase-orders/API.md\",\"docs/modules/purchase-orders/MANUAL.md\",\"docs/modules/purchase-orders/SOURCE_MAP.md\",\"docs/modules/purchase-orders/TODO.md\",\"docs/modules/purchase-orders/WHITEPAPER.md\",\"docs/modules/push-notifications/API.md\",\"docs/modules/push-notifications/MANUAL.md\",\"docs/modules/push-notifications/SOURCE_MAP.md\",\"docs/modules/push-notifications/TODO.md\",\"docs/modules/push-notifications/WHITEPAPER.md\",\"docs/modules/quickbooks/API.md\",\"docs/modules/quickbooks/MANUAL.md\",\"docs/modules/quickbooks/SOURCE_MAP.md\",\"docs/modules/quickbooks/TODO.md\",\"docs/modules/quickbooks/WHITEPAPER.md\",\"docs/modules/rmm/AGENT_AUDIT.md\",\"docs/modules/rmm/AGENT_REQUIREMENTS.md\",\"docs/modules/rmm/API.md\",\"docs/modules/rmm/MANUAL.md\",\"docs/modules/rmm/SOURCE_MAP.md\",\"docs/modules/rmm/TODO.md\",\"docs/modules/rmm/WHITEPAPER.md\",\"docs/modules/settings/API.md\",\"docs/modules/settings/MANUAL.md\",\"docs/modules/settings/SOURCE_MAP.md\",\"docs/modules/settings/TODO.md\",\"docs/modules/settings/WHITEPAPER.md\",\"docs/modules/siem/API.md\",\"docs/modules/siem/MANUAL.md\",\"docs/modules/siem/SOURCE_MAP.md\",\"docs/modules/siem/TODO.md\",\"docs/modules/siem/WHITEPAPER.md\",\"docs/modules/timer/API.md\",\"docs/modules/timer/MANUAL.md\",\"docs/modules/timer/SOURCE_MAP.md\",\"docs/modules/timer/TODO.md\",\"docs/modules/timer/WHITEPAPER.md\",\"docs/modules/voip/API.md\",\"docs/modules/voip/MANUAL.md\",\"docs/modules/voip/SOURCE_MAP.md\",\"docs/modules/voip/TODO.md\",\"docs/modules/voip/WHITEPAPER.md\",\"docs/security/EVIDENCE.md\",\"docs/security/audit-catalog.md\",\"docs/security/controls/soc2-cc6.1-logical-access.md\",\"docs/security/crypto-inventory.md\",\"docs/security/index.html\",\"docs/security/network-surface.md\",\"docs/security/threats/rmm.md\",\"docs/ui-samples/action_config_panels_mockup.html\",\"docs/ui-samples/hex-frosted-matte-sample.html\",\"docs/ui-samples/hex-frosted-white-ceramic-sample.html\",\"docs/ui-samples/hex-glass-buttons-sample.html\",\"docs/ui-samples/hex-glass-light-mode-sample.html\",\"docs/ui-samples/hex-glass-theme-sample.html\",\"docs/ui-samples/orchestrator_module_detail_mockup.html\"],\"docs \u2014 docs\":[\"docs/2026-04-07-Meeting-Email-Security-Flow.md\",\"docs/API_REFERENCE.md\",\"docs/ARCHITECTURE.md\",\"docs/AUDIT_CRM_TENANT_SCOPE.md\",\"docs/BASE_HTML_MIGRATION_PLAN.md\",\"docs/BID_ENGINE_TEST_GUIDE.md\",\"docs/CMMC_ONE_CLICK_COMPLIANCE_PLAN.md\",\"docs/CMMC_ONE_CLICK_TEST_GUIDE.md\",\"docs/CODEBASE_OBSERVATIONS.md\",\"docs/COMPLIANCE_COMPOSER_PLAN.md\",\"docs/DEPENDENCIES-EXTERNAL.md\",\"docs/DEPENDENCIES.md\",\"docs/DISTRIBUTOR_PHASE3_PROCUREMENT_PLAN.md\",\"docs/EMAIL_SECURITY_TRIAGE_PLAN.md\",\"docs/INFRASTRUCTURE.md\",\"docs/INVENTORY.md\",\"docs/LIVE_REVIEW_ENGINE_MANUAL.md\",\"docs/NCSR_INTEGRATION_PLAN.md\",\"docs/NexusOS_Orchestrator_Architecture.md\",\"docs/NexusOS_Sales_Lifecycle_Automation_Plan.md\",\"docs/NexusOS_Sales_Lifecycle_Test_Plan.md\",\"docs/NexusRMM-Status-Update-Jon.md\",\"docs/ONBOARDING.md\",\"docs/Orchestrator_Hex_Detail_UI_Artifact.md\",\"docs/PRODUCTS_SERVICES_CATALOG.md\",\"docs/PROJECT_MANAGEMENT.md\",\"docs/PR_A_Helpdesk-Peek.md\",\"docs/PR_B_Helpdesk-Hub.md\",\"docs/Phase9_Action_Configuration_Plan.md\",\"docs/RISK_ACCEPTANCE_DISPOSITION_GUIDE.md\",\"docs/ROADMAP.md\",\"docs/gen_ach_user_guide.py\",\"docs/gen_ach_whitepaper.py\",\"docs/gen_onboarding_guide.py\",\"docs/gen_onboarding_user_guide.py\",\"docs/gen_schema_poster.py\",\"docs/lifecycle-test-manual.md\",\"docs/orchestrator_ui_enhancement_artifact.md\",\"docs/teleport-deployment-guide.md\",\"docs/tickets-er-diagram.html\",\"docs/work-order-manual.md\"],\"docs \u2014 architecture\":[\"docs/architecture/ARCHITECTURE.md\",\"docs/architecture/PROJECT_STATUS.md\",\"docs/architecture/REMOTE_DESKTOP_SPEC.md\"],\"docs \u2014 guide\":[\"docs/guide/MULTI_REVIEWER_TEST_GUIDE.md\",\"docs/guide/NCSR_USER_GUIDE.md\",\"docs/guide/PHASE3_PROCUREMENT_TESTING_GUIDE.md\",\"docs/guide/README.md\",\"docs/guide/RISK_ACCEPTANCE_USER_GUIDE.md\",\"docs/guide/billing-contracts.md\",\"docs/guide/dispatch.md\",\"docs/guide/employee-hr.md\",\"docs/guide/projects.md\"],\"docs \u2014 integrations\":[\"docs/integrations/BACKLOG.md\"],\"docs \u2014 mockups\":[\"docs/mockups/brief_mockup_v2.html\",\"docs/mockups/live_review_generalization.html\",\"docs/mockups/phase3/_shared.css\",\"docs/mockups/phase3/agreement-saas.html\",\"docs/mockups/phase3/contract-materials.html\",\"docs/mockups/phase3/index.html\",\"docs/mockups/phase3/procurement-detail.html\",\"docs/mockups/phase3/procurement-list.html\",\"docs/mockups/phase3/project-po-integration.html\",\"docs/mockups/phase3/remediation-queue.html\",\"docs/mockups/phase3/settings-distributors.html\",\"docs/mockups/phase3/ticket-po-integration.html\",\"docs/mockups/review_host_completion.html\",\"docs/mockups/review_ipad_completion.html\"],\"docs \u2014 modules\":[\"docs/modules/auth/API.md\",\"docs/modules/auth/MANUAL.md\",\"docs/modules/auth/SOURCE_MAP.md\",\"docs/modules/auth/TODO.md\",\"docs/modules/auth/WHITEPAPER.md\",\"docs/modules/billing/API.md\",\"docs/modules/billing/MANUAL.md\",\"docs/modules/billing/SOURCE_MAP.md\",\"docs/modules/billing/TODO.md\",\"docs/modules/billing/WHITEPAPER.md\",\"docs/modules/certificates/API.md\",\"docs/modules/certificates/MANUAL.md\",\"docs/modules/certificates/SOURCE_MAP.md\",\"docs/modules/certificates/TODO.md\",\"docs/modules/certificates/WHITEPAPER.md\",\"docs/modules/compliance/API.md\",\"docs/modules/compliance/MANUAL.md\",\"docs/modules/compliance/SOURCE_MAP.md\",\"docs/modules/compliance/TODO.md\",\"docs/modules/compliance/WHITEPAPER.md\",\"docs/modules/crm/API.md\",\"docs/modules/crm/MANUAL.md\",\"docs/modules/crm/SOURCE_MAP.md\",\"docs/modules/crm/TODO.md\",\"docs/modules/crm/WHITEPAPER.md\",\"docs/modules/cybersec/API.md\",\"docs/modules/cybersec/MANUAL.md\",\"docs/modules/cybersec/SOURCE_MAP.md\",\"docs/modules/cybersec/TODO.md\",\"docs/modules/cybersec/WHITEPAPER.md\",\"docs/modules/devices/API.md\",\"docs/modules/devices/MANUAL.md\",\"docs/modules/devices/SOURCE_MAP.md\",\"docs/modules/devices/TODO.md\",\"docs/modules/devices/WHITEPAPER.md\",\"docs/modules/email-security/API.md\",\"docs/modules/email-security/MANUAL.md\",\"docs/modules/email-security/SOURCE_MAP.md\",\"docs/modules/email-security/TODO.md\",\"docs/modules/email-security/WHITEPAPER.md\",\"docs/modules/health/API.md\",\"docs/modules/health/MANUAL.md\",\"docs/modules/health/SOURCE_MAP.md\",\"docs/modules/health/TODO.md\",\"docs/modules/health/WHITEPAPER.md\",\"docs/modules/helpdesk/API.md\",\"docs/modules/helpdesk/MANUAL.md\",\"docs/modules/helpdesk/SOURCE_MAP.md\",\"docs/modules/helpdesk/TODO.md\",\"docs/modules/helpdesk/WHITEPAPER.md\",\"docs/modules/hudu/API.md\",\"docs/modules/hudu/MANUAL.md\",\"docs/modules/hudu/SOURCE_MAP.md\",\"docs/modules/hudu/TODO.md\",\"docs/modules/hudu/WHITEPAPER.md\",\"docs/modules/knowledge-base/API.md\",\"docs/modules/knowledge-base/MANUAL.md\",\"docs/modules/knowledge-base/SOURCE_MAP.md\",\"docs/modules/knowledge-base/TODO.md\",\"docs/modules/knowledge-base/WHITEPAPER.md\",\"docs/modules/legal/API.md\",\"docs/modules/legal/MANUAL.md\",\"docs/modules/legal/SOURCE_MAP.md\",\"docs/modules/legal/TODO.md\",\"docs/modules/legal/WHITEPAPER.md\",\"docs/modules/metasploit/API.md\",\"docs/modules/metasploit/MANUAL.md\",\"docs/modules/metasploit/SOURCE_MAP.md\",\"docs/modules/metasploit/TODO.md\",\"docs/modules/metasploit/WHITEPAPER.md\",\"docs/modules/mobile/API.md\",\"docs/modules/mobile/MANUAL.md\",\"docs/modules/mobile/SOURCE_MAP.md\",\"docs/modules/mobile/TODO.md\",\"docs/modules/mobile/WHITEPAPER.md\",\"docs/modules/nexie-ai/API.md\",\"docs/modules/nexie-ai/MANUAL.md\",\"docs/modules/nexie-ai/SOURCE_MAP.md\",\"docs/modules/nexie-ai/TODO.md\",\"docs/modules/nexie-ai/WHITEPAPER.md\",\"docs/modules/onboarding/API.md\",\"docs/modules/onboarding/MANUAL.md\",\"docs/modules/onboarding/SOURCE_MAP.md\",\"docs/modules/onboarding/TODO.md\",\"docs/modules/onboarding/WHITEPAPER.md\",\"docs/modules/products/API.md\",\"docs/modules/products/MANUAL.md\",\"docs/modules/products/SOURCE_MAP.md\",\"docs/modules/products/TODO.md\",\"docs/modules/products/WHITEPAPER.md\",\"docs/modules/projects/API.md\",\"docs/modules/projects/MANUAL.md\",\"docs/modules/projects/SOURCE_MAP.md\",\"docs/modules/projects/TODO.md\",\"docs/modules/projects/WHITEPAPER.md\",\"docs/modules/purchase-orders/API.md\",\"docs/modules/purchase-orders/MANUAL.md\",\"docs/modules/purchase-orders/SOURCE_MAP.md\",\"docs/modules/purchase-orders/TODO.md\",\"docs/modules/purchase-orders/WHITEPAPER.md\",\"docs/modules/push-notifications/API.md\",\"docs/modules/push-notifications/MANUAL.md\",\"docs/modules/push-notifications/SOURCE_MAP.md\",\"docs/modules/push-notifications/TODO.md\",\"docs/modules/push-notifications/WHITEPAPER.md\",\"docs/modules/quickbooks/API.md\",\"docs/modules/quickbooks/MANUAL.md\",\"docs/modules/quickbooks/SOURCE_MAP.md\",\"docs/modules/quickbooks/TODO.md\",\"docs/modules/quickbooks/WHITEPAPER.md\",\"docs/modules/rmm/AGENT_AUDIT.md\",\"docs/modules/rmm/AGENT_REQUIREMENTS.md\",\"docs/modules/rmm/API.md\",\"docs/modules/rmm/MANUAL.md\",\"docs/modules/rmm/SOURCE_MAP.md\",\"docs/modules/rmm/TODO.md\",\"docs/modules/rmm/WHITEPAPER.md\",\"docs/modules/settings/API.md\",\"docs/modules/settings/MANUAL.md\",\"docs/modules/settings/SOURCE_MAP.md\",\"docs/modules/settings/TODO.md\",\"docs/modules/settings/WHITEPAPER.md\",\"docs/modules/siem/API.md\",\"docs/modules/siem/MANUAL.md\",\"docs/modules/siem/SOURCE_MAP.md\",\"docs/modules/siem/TODO.md\",\"docs/modules/siem/WHITEPAPER.md\",\"docs/modules/timer/API.md\",\"docs/modules/timer/MANUAL.md\",\"docs/modules/timer/SOURCE_MAP.md\",\"docs/modules/timer/TODO.md\",\"docs/modules/timer/WHITEPAPER.md\",\"docs/modules/voip/API.md\",\"docs/modules/voip/MANUAL.md\",\"docs/modules/voip/SOURCE_MAP.md\",\"docs/modules/voip/TODO.md\",\"docs/modules/voip/WHITEPAPER.md\"],\"docs \u2014 security\":[\"docs/security/EVIDENCE.md\",\"docs/security/audit-catalog.md\",\"docs/security/controls/soc2-cc6.1-logical-access.md\",\"docs/security/crypto-inventory.md\",\"docs/security/index.html\",\"docs/security/network-surface.md\",\"docs/security/threats/rmm.md\"],\"docs \u2014 ui-samples\":[\"docs/ui-samples/action_config_panels_mockup.html\",\"docs/ui-samples/hex-frosted-matte-sample.html\",\"docs/ui-samples/hex-frosted-white-ceramic-sample.html\",\"docs/ui-samples/hex-glass-buttons-sample.html\",\"docs/ui-samples/hex-glass-light-mode-sample.html\",\"docs/ui-samples/hex-glass-theme-sample.html\",\"docs/ui-samples/orchestrator_module_detail_mockup.html\"],\"extensions\":[\"extensions/chrome/background.js\",\"extensions/chrome/content.js\",\"extensions/chrome/manifest.json\",\"extensions/chrome/popup.html\",\"extensions/chrome/popup.js\"],\"internal\":[\"internal/ai/claude.go\",\"internal/ai/redact.go\",\"internal/assessment/analyzer.go\",\"internal/assessment/approval_gate.go\",\"internal/assessment/blueprint_builder.go\",\"internal/assessment/document_processor.go\",\"internal/assessment/handler.go\",\"internal/assessment/quote_converter.go\",\"internal/assessment/scheduled_ops.go\",\"internal/assessment/types.go\",\"internal/auth/handler.go\",\"internal/auth/jwt.go\",\"internal/auth/mfa.go\",\"internal/auth/middleware.go\",\"internal/auth/password.go\",\"internal/auth/session.go\",\"internal/auth/sso.go\",\"internal/bidengine/addendum_apply.go\",\"internal/bidengine/addendum_split.go\",\"internal/bidengine/audit.go\",\"internal/bidengine/audit_handler.go\",\"internal/bidengine/audit_resolve.go\",\"internal/bidengine/audit_view.go\",\"internal/bidengine/drawing_discipline.go\",\"internal/bidengine/engine.go\",\"internal/bidengine/engine_test.go\",\"internal/bidengine/exclusion_extractor.go\",\"internal/bidengine/exclusion_extractor_test.go\",\"internal/bidengine/flow.go\",\"internal/bidengine/generate_from_drawings.go\",\"internal/bidengine/generate_from_spec_helpers.go\",\"internal/bidengine/handler.go\",\"internal/bidengine/outcomes.go\",\"internal/bidengine/page_selection.go\",\"internal/bidengine/processing_lock.go\",\"internal/bidengine/reconcile.go\",\"internal/bidengine/scope_summary.go\",\"internal/bidengine/trade_profile.go\",\"internal/bidengine/types.go\",\"internal/bidengine/vision_audit.go\",\"internal/billing/email.go\",\"internal/billing/invoice_attachments.go\",\"internal/billing/invoice_handler.go\",\"internal/billing/quote_attachments.go\",\"internal/billing/quote_handler.go\",\"internal/billing/transcript_analyzer.go\",\"internal/billing/transcript_handler.go\",\"internal/billing/types.go\",\"internal/certs/ca.go\",\"internal/certs/handler.go\",\"internal/certs/store.go\",\"internal/cmmc/binder_render.go\",\"internal/cmmc/connect.go\",\"internal/cmmc/disposition.go\",\"internal/cmmc/handler.go\",\"internal/cmmc/nexie.go\",\"internal/cmmc/policy_render.go\",\"internal/cmmc/risk_resolver.go\",\"internal/cmmc/ssp_render.go\",\"internal/cmmc/types.go\",\"internal/compliance/handler.go\",\"internal/compliance/types.go\",\"internal/composer/disposition.go\",\"internal/composer/framework.go\",\"internal/composer/handler.go\",\"internal/composer/ncsr/action_plan.go\",\"internal/composer/ncsr/crm_compliance.go\",\"internal/composer/ncsr/delete.go\",\"internal/composer/ncsr/handler.go\",\"internal/composer/ncsr/parse_pdf.go\",\"internal/composer/ncsr/parse_pdf_test.go\",\"internal/composer/ncsr/parse_xlsx.go\",\"internal/composer/ncsr/parse_xlsx_test.go\",\"internal/composer/ncsr/post_review.go\",\"internal/composer/ncsr/review_queue.go\",\"internal/composer/ncsr/review_writeback.go\",\"internal/composer/ncsr/reviews_page.go\",\"internal/composer/ncsr/risk_resolver.go\",\"internal/composer/ncsr/seeder.go\",\"internal/composer/ncsr/trajectory.go\",\"internal/composer/ncsr/types.go\",\"internal/composer/nexie.go\",\"internal/composer/reviews_history.go\",\"internal/composer/risk_resolver.go\",\"internal/composer/start_review.go\",\"internal/composer/types.go\",\"internal/config/config.go\",\"internal/crm/accounts_handler.go\",\"internal/crm/activities_handler.go\",\"internal/crm/campaigns_handler.go\",\"internal/crm/contacts_handler.go\",\"internal/crm/deals_handler.go\",\"internal/crm/email_security_handler.go\",\"internal/crm/form_handlers.go\",\"internal/crm/handler.go\",\"internal/crm/lead_analysis.go\",\"internal/crm/lead_analysis_test.go\",\"internal/crm/pipelines_handler.go\",\"internal/crm/tenant_isolation_test.go\",\"internal/crm/types.go\",\"internal/cybersec/handler.go\",\"internal/cybersec/types.go\",\"internal/database/database.go\",\"internal/database/migrate.go\",\"internal/database/migrations/001_extensions.sql\",\"internal/database/migrations/002_tenants.sql\",\"internal/database/migrations/003_roles_permissions.sql\",\"internal/database/migrations/004_users.sql\",\"internal/database/migrations/005_companies_agents.sql\",\"internal/database/migrations/006_products_compliance.sql\",\"internal/database/migrations/007_sso.sql\",\"internal/database/migrations/008_helpdesk.sql\",\"internal/database/migrations/009_compliance_cybersec_extras.sql\",\"internal/database/migrations/010_projects.sql\",\"internal/database/migrations/011_itil.sql\",\"internal/database/migrations/012_quoting_invoicing.sql\",\"internal/database/migrations/013_crm.sql\",\"internal/database/migrations/014_audit_revisions.sql\",\"internal/database/migrations/015_quickbooks_integration.sql\",\"internal/database/migrations/016_quote_require_po.sql\",\"internal/database/migrations/017_certs_mcp_email.sql\",\"internal/database/migrations/018_user_favorites.sql\",\"internal/database/migrations/019_compliance_engine.sql\",\"internal/database/migrations/020_compliance_full_controls.sql\",\"internal/database/migrations/021_compliance_doc_import.sql\",\"internal/database/migrations/022_hipaa_full_controls.sql\",\"internal/database/migrations/023_pci_dss_full_controls.sql\",\"internal/database/migrations/024_soc2_full_controls.sql\",\"internal/database/migrations/025_cmmc_full_controls.sql\",\"internal/database/migrations/026_nist_800_53_full_controls.sql\",\"internal/database/migrations/027_ai_usage_log.sql\",\"internal/database/migrations/027b_compliance_framework_source_cols.sql\",\"internal/database/migrations/028_iso27001.sql\",\"internal/database/migrations/029_nist_csf.sql\",\"internal/database/migrations/030_nist_800_171.sql\",\"internal/database/migrations/031_fedramp.sql\",\"internal/database/migrations/032_iso27701.sql\",\"internal/database/migrations/033_hitrust.sql\",\"internal/database/migrations/034_ffiec.sql\",\"internal/database/migrations/035_stateramp.sql\",\"internal/database/migrations/036_cjis.sql\",\"internal/database/migrations/037_crm_client_enhancements.sql\",\"internal/database/migrations/038_communication_channels.sql\",\"internal/database/migrations/039_country_field.sql\",\"internal/database/migrations/040_rmm_merge.sql\",\"internal/database/migrations/041_legal_documents_and_contracts.sql\",\"internal/database/migrations/042_contract_service_types.sql\",\"internal/database/migrations/043_contract_sync.sql\",\"internal/database/migrations/044_teleport_integration.sql\",\"internal/database/migrations/045_agent_polling_teleport_token.sql\",\"internal/database/migrations/046_metasploit_integration.sql\",\"internal/database/migrations/047_client_vulnerability_config.sql\",\"internal/database/migrations/048_documentation_provider.sql\",\"internal/database/migrations/049_ai_auto_resolve.sql\",\"internal/database/migrations/050_siem.sql\",\"internal/database/migrations/051_billing_work_types_and_roles.sql\",\"internal/database/migrations/052_contract_categories.sql\",\"internal/database/migrations/053_contract_rate_overrides.sql\",\"internal/database/migrations/054_esign_multi_signature.sql\",\"internal/database/migrations/055_product_legal_documents.sql\",\"internal/database/migrations/056_quote_to_contract.sql\",\"internal/database/migrations/057_quote_attachments.sql\",\"internal/database/migrations/058_siem_capture_sessions.sql\",\"internal/database/migrations/059_siem_log_retention.sql\",\"internal/database/migrations/060_certificate_store.sql\",\"internal/database/migrations/061_device_credentials.sql\",\"internal/database/migrations/062_siem_remediation.sql\",\"internal/database/migrations/063_login_audit.sql\",\"internal/database/migrations/064_invitations.sql\",\"internal/database/migrations/065_rmm_enroll_token.sql\",\"internal/database/migrations/066_persistent_timers.sql\",\"internal/database/migrations/067_security_audit_log.sql\",\"internal/database/migrations/068_multi_timer.sql\",\"internal/database/migrations/069_ticket_contract_link.sql\",\"internal/database/migrations/070_company_escalation_policy.sql\",\"internal/database/migrations/071_quote_sla_escalation_policies.sql\",\"internal/database/migrations/072_nexie_time_entries.sql\",\"internal/database/migrations/073_ticket_config.sql\",\"internal/database/migrations/074_voip_integration.sql\",\"internal/database/migrations/075_quote_sequencing.sql\",\"internal/database/migrations/076_quote_sections.sql\",\"internal/database/migrations/077_fix_contract_policy_columns.sql\",\"internal/database/migrations/078_project_management_v2.sql\",\"internal/database/migrations/079_hudu_sync_status.sql\",\"internal/database/migrations/080_health_components.sql\",\"internal/database/migrations/082_ticket_charges.sql\",\"internal/database/migrations/083_time_entry_product.sql\",\"internal/database/migrations/084_purchase_orders.sql\",\"internal/database/migrations/085_po_approvers.sql\",\"internal/database/migrations/086_vendors.sql\",\"internal/database/migrations/087_cr_esign.sql\",\"internal/database/migrations/088_po_approval_signature.sql\",\"internal/database/migrations/089_web_push.sql\",\"internal/database/migrations/090_project_site_location.sql\",\"internal/database/migrations/091_orchestrator.sql\",\"internal/database/migrations/092_workflow_engine_v2.sql\",\"internal/database/migrations/093_rmm_session_tokens.sql\",\"internal/database/migrations/094_rmm_enrollment_tokens.sql\",\"internal/database/migrations/095_email_send_log.sql\",\"internal/database/migrations/096_client_security_profile.sql\",\"internal/database/migrations/097_client_onboarding.sql\",\"internal/database/migrations/098_ach_payment_method.sql\",\"internal/database/migrations/098_device_cert_columns.sql\",\"internal/database/migrations/099_orchestrator_packages.sql\",\"internal/database/migrations/099_sla_pause_support.sql\",\"internal/database/migrations/100_note_edit_tracking.sql\",\"internal/database/migrations/101_multi_pipeline.sql\",\"internal/database/migrations/102_invoice_attachments.sql\",\"internal/database/migrations/103_recurring_invoice_schedule.sql\",\"internal/database/migrations/104_timesheets.sql\",\"internal/database/migrations/105_qb_employees.sql\",\"internal/database/migrations/106_timesheet_permissions.sql\",\"internal/database/migrations/107_portal.sql\",\"internal/database/migrations/108_portal_permissions.sql\",\"internal/database/migrations/109_contacts_country.sql\",\"internal/database/migrations/110_kb_visibility.sql\",\"internal/database/migrations/111_invoice_qb_link.sql\",\"internal/database/migrations/112_ai_usage_portal.sql\",\"internal/database/migrations/113_portal_roles.sql\",\"internal/database/migrations/114_portal_role_labels.sql\",\"internal/database/migrations/115_user_ui_theme.sql\",\"internal/database/migrations/116_user_ui_preferences.sql\",\"internal/database/migrations/117_industry_intelligence.sql\",\"internal/database/migrations/118_assessments.sql\",\"internal/database/migrations/119_orchestrator_events.sql\",\"internal/database/migrations/120_notification_actions.sql\",\"internal/database/migrations/121_scheduled_operations.sql\",\"internal/database/migrations/122_pipeline_presets.sql\",\"internal/database/migrations/123_industry_business_types.sql\",\"internal/database/migrations/124_compliance_frameworks_sync.sql\",\"internal/database/migrations/125_epa.sql\",\"internal/database/migrations/126_epa_client_contact_asset.sql\",\"internal/database/migrations/127_agent_releases.sql\",\"internal/database/migrations/128_agent_releases_v11_seed.sql\",\"internal/database/migrations/129_agent_releases_go_v110.sql\",\"internal/database/migrations/129_custom_actions.sql\",\"internal/database/migrations/131_agent_releases_go_v111.sql\",\"internal/database/migrations/132_contacts_nexie_epa_enabled.sql\",\"internal/database/migrations/133_nexuspulse.sql\",\"internal/database/migrations/134_nexuspulse_reports.sql\",\"internal/database/migrations/135_nexuspulse_rebrand_codes.sql\",\"internal/database/migrations/136_product_qbo_accounts.sql\",\"internal/database/migrations/137_product_cost.sql\",\"internal/database/migrations/138_product_subcontractor.sql\",\"internal/database/migrations/139_product_image.sql\",\"internal/database/migrations/140_product_sku.sql\",\"internal/database/migrations/141_product_supplier.sql\",\"internal/database/migrations/142_bid_engine.sql\",\"internal/database/migrations/143_bid_spec_compliance.sql\",\"internal/database/migrations/144_product_labor_hours.sql\",\"internal/database/migrations/145_bid_card_types.sql\",\"internal/database/migrations/146_bid_revisions.sql\",\"internal/database/migrations/147_bid_sections.sql\",\"internal/database/migrations/148_bid_trade_scope.sql\",\"internal/database/migrations/149_section_scope_link.sql\",\"internal/database/migrations/150_bid_address_fields.sql\",\"internal/database/migrations/151_spec_doc_ai_cost.sql\",\"internal/database/migrations/152_site_surveys.sql\",\"internal/database/migrations/153_crm_company_type.sql\",\"internal/database/migrations/154_survey_checklist_seeds.sql\",\"internal/database/migrations/155_orchestrator_survey_bid_presets.sql\",\"internal/database/migrations/156_timer_survey_support.sql\",\"internal/database/migrations/157_survey_photo_analysis.sql\",\"internal/database/migrations/158_orchestrator_photo_analysis_preset.sql\",\"internal/database/migrations/159_survey_poc_contact.sql\",\"internal/database/migrations/160_survey_audio.sql\",\"internal/database/migrations/161_drawings.sql\",\"internal/database/migrations/162_photo_thumbnails.sql\",\"internal/database/migrations/163_assessment_work_type.sql\",\"internal/database/migrations/164_work_orders.sql\",\"internal/database/migrations/165_full_lifecycle_presets.sql\",\"internal/database/migrations/166_dispatch.sql\",\"internal/database/migrations/167_employee.sql\",\"internal/database/migrations/168_seed_employees.sql\",\"internal/database/migrations/169_recorder_sessions_segments.sql\",\"internal/database/migrations/170_role_rates.sql\",\"internal/database/migrations/171_time_entry_costs.sql\",\"internal/database/migrations/172_role_rates_billing_link.sql\",\"internal/database/migrations/173_cmmc_l2_module.sql\",\"internal/database/migrations/174_cmmc_company_branding.sql\",\"internal/database/migrations/175_recorder_consent.sql\",\"internal/database/migrations/176_infra_devices.sql\",\"internal/database/migrations/177_agents_compliance_data.sql\",\"internal/database/migrations/178_cmmc_ingested_docs.sql\",\"internal/database/migrations/179_compliance_composer.sql\",\"internal/database/migrations/180_sentinel_devices.sql\",\"internal/database/migrations/181_sentinel_schema_isolation.sql\",\"internal/database/migrations/182_sentinel_audit.sql\",\"internal/database/migrations/183_agents_sentinel_hmac_key.sql\",\"internal/database/migrations/183_risk_disposition.sql\",\"internal/database/migrations/184_ncsr_assessments.sql\",\"internal/database/migrations/185_recorder_consent_cancelled.sql\",\"internal/database/migrations/185_risk_acceptances.sql\",\"internal/database/migrations/186_live_review_engine.sql\",\"internal/database/migrations/187_review_token_revocation.sql\",\"internal/database/migrations/188_ncsr_plan_jobs.sql\",\"internal/database/migrations/189_review_decision_writeback.sql\",\"internal/database/migrations/189_tickets_pinned.sql\",\"internal/database/migrations/190_backfill_action_decline_from_area.sql\",\"internal/database/migrations/190_tickets_source_enum.sql\",\"internal/database/migrations/191_backfill_action_decision_from_area.sql\",\"internal/database/migrations/192_brief_v2_fields.sql\",\"internal/database/migrations/193_ticket_followers.sql\",\"internal/database/migrations/194_review_multi_reviewer.sql\",\"internal/database/migrations/195_mcp_oauth_columns.sql\",\"internal/database/migrations/196_distributor_ingest.sql\",\"internal/database/migrations/196_helpdesk_config_tables.sql\",\"internal/database/migrations/197_tickets_fk_indexes.sql\",\"internal/database/migrations/200_contract_lock_state.sql\",\"internal/database/migrations/201_contract_line_exclusions.sql\",\"internal/database/migrations/202_contract_change_requests.sql\",\"internal/database/migrations/203_contract_line_audit.sql\",\"internal/database/migrations/204_nexiq_foundation.sql\",\"internal/database/migrations/205_pbr_command_control_fields.sql\",\"internal/database/migrations/206_bid_drawing_reconciliation.sql\",\"internal/database/migrations/207_bid_mode.sql\",\"internal/database/migrations/208_quote_totals_trigger.sql\",\"internal/database/migrations/209_invoice_totals_trigger.sql\",\"internal/database/migrations/210_lifecycle_events.sql\",\"internal/database/migrations/210_quote_line_client_selection.sql\",\"internal/database/migrations/211_quote_totals_effective.sql\",\"internal/database/migrations/212_phase3_purchase_orders_procurement.sql\",\"internal/database/migrations/213_po_remediation_queue.sql\",\"internal/database/migrations/214_vendor_distributor_auto_dispatch.sql\",\"internal/database/migrations/215_purchase_order_audit.sql\",\"internal/database/migrations/216_contract_line_procurement.sql\",\"internal/database/migrations/217_deprecate_project_purchase_orders.sql\",\"internal/database/migrations/218_bid_ai_scope_summary.sql\",\"internal/database/migrations/219_bid_nexie_last_error.sql\",\"internal/database/migrations/220_drawings_page_selection.sql\",\"internal/database/migrations/221_peek_summary.sql\",\"internal/database/migrations/222_bid_line_part_number.sql\",\"internal/database/migrations/223_bid_addendum_changes.sql\",\"internal/database/migrations/224_drawing_supersession.sql\",\"internal/database/migrations/225_addendum_diff_status.sql\",\"internal/database/migrations/226_addendum_audit_trail.sql\",\"internal/database/migrations/227_addendum_decision_audit.sql\",\"internal/database/migrations/228_addendum_scope_exclusion.sql\",\"internal/database/migrations/229_backfill_orphan_recon_line_items.sql\",\"internal/database/migrations/230_bid_line_scope_flags.sql\",\"internal/database/migrations/231_bid_trade_profiles.sql\",\"internal/database/migrations/232_bid_outcomes.sql\",\"internal/database/migrations/233_drawings_extracted_text.sql\",\"internal/database/migrations/234_bid_audit_acks.sql\",\"internal/database/migrations/235_tag_tables_tenant_id.sql\",\"internal/database/migrations/236_campaign_recipients_tenant_id.sql\",\"internal/devices/fortigate.go\",\"internal/devices/handler.go\",\"internal/dispatch/graph_calendar.go\",\"internal/dispatch/handler.go\",\"internal/dispatch/types.go\",\"internal/distributor/catalog_query.go\",\"internal/distributor/contract_query.go\",\"internal/distributor/handler.go\",\"internal/distributor/provisioner.go\",\"internal/distributor/scheduler.go\",\"internal/distributor/status.go\",\"internal/distributor/subs_query.go\",\"internal/distributor/sync.go\",\"internal/distributor/types.go\",\"internal/distributor/wrapper.go\",\"internal/drawing/handler.go\",\"internal/drawing/isometric.go\",\"internal/drawing/takeoff.go\",\"internal/drawing/types.go\",\"internal/drawing/vision.go\",\"internal/emailsec/graph.go\",\"internal/emailsec/handler.go\",\"internal/emailsec/headers.go\",\"internal/emailsec/llm_triage.go\",\"internal/emailsec/pipeline.go\",\"internal/emailsec/scoring.go\",\"internal/employee/handler.go\",\"internal/employee/types.go\",\"internal/epa/handler.go\",\"internal/epa/store.go\",\"internal/epa/types.go\",\"internal/financial/provider.go\",\"internal/financial/qbo/adapter.go\",\"internal/financial/qbo/provider.go\",\"internal/financial/types.go\",\"internal/health/disk_darwin.go\",\"internal/health/disk_linux.go\",\"internal/health/disk_windows.go\",\"internal/health/handler.go\",\"internal/helpdesk/capabilities.go\",\"internal/helpdesk/config_audit.go\",\"internal/helpdesk/config_handler.go\",\"internal/helpdesk/config_seed.go\",\"internal/helpdesk/contract.go\",\"internal/helpdesk/email.go\",\"internal/helpdesk/handler.go\",\"internal/helpdesk/handler_v2.go\",\"internal/helpdesk/orchestrator.go\",\"internal/helpdesk/peek_handler.go\",\"internal/helpdesk/peek_hudu.go\",\"internal/helpdesk/peek_store.go\",\"internal/helpdesk/peek_types.go\",\"internal/helpdesk/recorder_session.go\",\"internal/helpdesk/recordings_handler.go\",\"internal/helpdesk/sla.go\",\"internal/helpdesk/store.go\",\"internal/helpdesk/ticket_detail_helpers.go\",\"internal/helpdesk/tickets_prefs.go\",\"internal/helpdesk/tracking.go\",\"internal/helpdesk/types.go\",\"internal/helpdesk/types_itil.go\",\"internal/helpdesk/workflow.go\",\"internal/hudu/client.go\",\"internal/hudu/handler.go\",\"internal/hudu/types.go\",\"internal/industry/handler.go\",\"internal/infra/audit.go\",\"internal/infra/handler.go\",\"internal/infra/hudu.go\",\"internal/infra/runbook.go\",\"internal/infra/ssrf_guard.go\",\"internal/infra/types.go\",\"internal/infra/verify.go\",\"internal/itil/itil_handler.go\",\"internal/itil/itil_store.go\",\"internal/itil/itil_types.go\",\"internal/kb/handler.go\",\"internal/kb/types.go\",\"internal/legal/ach_submit_helpers.go\",\"internal/legal/analyze.go\",\"internal/legal/billing_handler.go\",\"internal/legal/billing_types.go\",\"internal/legal/contract_summary_generator.go\",\"internal/legal/crypto.go\",\"internal/legal/docx_renderer.go\",\"internal/legal/edit_contract_form_helpers.go\",\"internal/legal/executed_package.go\",\"internal/legal/handler.go\",\"internal/legal/msa_generator.go\",\"internal/legal/order_generator.go\",\"internal/legal/quote_conversion.go\",\"internal/legal/sign_contract_helpers.go\",\"internal/legal/signature_stamper.go\",\"internal/legal/signing_page_helpers.go\",\"internal/legal/sync.go\",\"internal/legal/types.go\",\"internal/lifecycle/lifecycle.go\",\"internal/mcpclient/client.go\",\"internal/mcpvendors/factory.go\",\"internal/mcpvendors/pax8/pax8.go\",\"internal/mcpvendors/resolver.go\",\"internal/metasploit/client.go\",\"internal/metasploit/handler.go\",\"internal/metasploit/types.go\",\"internal/middleware/csrf.go\",\"internal/middleware/middleware.go\",\"internal/mobile/handler.go\",\"internal/nexie/handler.go\",\"internal/nexie/query.go\",\"internal/nexie/schema.go\",\"internal/nexiq/cascade.go\",\"internal/nexiq/cockpit.go\",\"internal/nexiq/events.go\",\"internal/nexiq/events_test.go\",\"internal/nexiq/goals.go\",\"internal/nexiq/gut_check.go\",\"internal/nexiq/handler.go\",\"internal/nexiq/pages.go\",\"internal/nexiq/permissions.go\",\"internal/nexiq/scenarios.go\",\"internal/nexiq/stubs.go\",\"internal/nexiq/workspace.go\",\"internal/notify/service.go\",\"internal/onboarding/blueprint_injection.go\",\"internal/onboarding/handler.go\",\"internal/onboarding/phases.go\",\"internal/onboarding/types.go\",\"internal/performance/analyzer.go\",\"internal/performance/calculator.go\",\"internal/performance/collector.go\",\"internal/performance/handler.go\",\"internal/performance/qbo_mapper.go\",\"internal/performance/store.go\",\"internal/performance/types.go\",\"internal/po/handler.go\",\"internal/portal/admin.go\",\"internal/portal/handler.go\",\"internal/portal/middleware.go\",\"internal/portal/nexie.go\",\"internal/portal/nexie_tools.go\",\"internal/portal/pages.go\",\"internal/portal/risk_acceptances.go\",\"internal/portal/store.go\",\"internal/portal/types.go\",\"internal/portal/usage.go\",\"internal/procurement/audit.go\",\"internal/procurement/handler.go\",\"internal/procurement/remediation.go\",\"internal/procurement/service.go\",\"internal/procurement/types.go\",\"internal/product/handler.go\",\"internal/product/project_handler.go\",\"internal/product/types.go\",\"internal/product/types_project.go\",\"internal/project/costing.go\",\"internal/project/evm.go\",\"internal/project/handler.go\",\"internal/project/safe_columns.go\",\"internal/project/types.go\",\"internal/push/handler.go\",\"internal/quickbooks/client.go\",\"internal/quickbooks/employees_sync.go\",\"internal/quickbooks/oauth.go\",\"internal/quickbooks/po_adapter.go\",\"internal/quickbooks/push.go\",\"internal/quickbooks/recurring_billing.go\",\"internal/quickbooks/service.go\",\"internal/quickbooks/sync.go\",\"internal/quickbooks/timesheet_push.go\",\"internal/quickbooks/types.go\",\"internal/rbac/roles.go\",\"internal/review/binder.go\",\"internal/review/broker.go\",\"internal/review/handler.go\",\"internal/review/qr.go\",\"internal/review/repository.go\",\"internal/review/security.go\",\"internal/review/seeder.go\",\"internal/review/types.go\",\"internal/risk/handler.go\",\"internal/risk/print.go\",\"internal/risk/registry.go\",\"internal/risk/service.go\",\"internal/risk/types.go\",\"internal/rmm/agent_api.go\",\"internal/rmm/agent_update.go\",\"internal/rmm/crypto/tls.go\",\"internal/rmm/device_api.go\",\"internal/rmm/enroll.go\",\"internal/rmm/handler.go\",\"internal/rmm/install.go\",\"internal/rmm/installers/linux.sh\",\"internal/rmm/installers/macos.sh\",\"internal/rmm/installers/nexusos-agent.wxs.tmpl\",\"internal/rmm/installers/windows.ps1\",\"internal/rmm/msi.go\",\"internal/rmm/protocol/compliance.go\",\"internal/rmm/protocol/inventory.go\",\"internal/rmm/protocol/types.go\",\"internal/rmm/recorder.go\",\"internal/rmm/recordings.go\",\"internal/rmm/remote/desktop.go\",\"internal/rmm/remote/session.go\",\"internal/rmm/remote/tunnel.go\",\"internal/rmm/sentinel.go\",\"internal/rmm/sentinel_socks_hub.go\",\"internal/rmm/session_token.go\",\"internal/rmm/types.go\",\"internal/rmm/update_api.go\",\"internal/sentinel/capability.go\",\"internal/sentinel/secrets.go\",\"internal/sentinel/types.go\",\"internal/settings/appearance_handler.go\",\"internal/settings/tab_order_handler.go\",\"internal/settings/tab_visibility_handler.go\",\"internal/siem/handler.go\",\"internal/siem/pcap.go\",\"internal/siem/syslog.go\",\"internal/siem/types.go\",\"internal/survey/audio.go\",\"internal/survey/handler.go\",\"internal/survey/types.go\",\"internal/survey/vision.go\",\"internal/survey/voice.go\",\"internal/tenant/handler.go\",\"internal/tenant/mcp_oauth.go\",\"internal/timer/billing_context_helpers.go\",\"internal/timer/escalate_helpers.go\",\"internal/timer/handler.go\",\"internal/timer/list_timers_helpers.go\",\"internal/timer/stop_helpers.go\",\"internal/timesheet/handler.go\",\"internal/timesheet/store.go\",\"internal/timesheet/types.go\",\"internal/ui/preferences.go\",\"internal/ui/render.go\",\"internal/ui/render_v2_test.go\",\"internal/ui/static/css/epa.css\",\"internal/ui/static/css/helpdesk-glass.css\",\"internal/ui/static/css/helpdesk-peek.css\",\"internal/ui/static/css/nexus.css\",\"internal/ui/static/css/theme-mixer.css\",\"internal/ui/static/icons/placeholder.txt\",\"internal/ui/static/js/app.js\",\"internal/ui/static/js/csrf.js\",\"internal/ui/static/js/sigpad.js\",\"internal/ui/static/js/sw.js\",\"internal/ui/static/manifest-mobile.json\",\"internal/ui/static/manifest.json\",\"internal/ui/templates/assessment/detail.html\",\"internal/ui/templates/assessment/form.html\",\"internal/ui/templates/assessment/list.html\",\"internal/ui/templates/auth/invite.html\",\"internal/ui/templates/auth/login.html\",\"internal/ui/templates/bidding/audit.html\",\"internal/ui/templates/bidding/bid_form.html\",\"internal/ui/templates/bidding/calibration.html\",\"internal/ui/templates/bidding/cost_engine.html\",\"internal/ui/templates/bidding/detail.html\",\"internal/ui/templates/bidding/drawing_selection.html\",\"internal/ui/templates/bidding/list.html\",\"internal/ui/templates/bidding/print_detail.html\",\"internal/ui/templates/bidding/print_shadow.html\",\"internal/ui/templates/bidding/settings.html\",\"internal/ui/templates/bidding/takeoff.html\",\"internal/ui/templates/bidding/vision_audit.html\",\"internal/ui/templates/billing/aging.html\",\"internal/ui/templates/billing/invoice_detail.html\",\"internal/ui/templates/billing/invoice_edit.html\",\"internal/ui/templates/billing/invoice_form.html\",\"internal/ui/templates/billing/invoice_print.html\",\"internal/ui/templates/billing/invoices.html\",\"internal/ui/templates/billing/quote_approve.html\",\"internal/ui/templates/billing/quote_detail.html\",\"internal/ui/templates/billing/quote_edit.html\",\"internal/ui/templates/billing/quote_form.html\",\"internal/ui/templates/billing/quote_print.html\",\"internal/ui/templates/billing/quotes.html\",\"internal/ui/templates/billing/transcript_import.html\",\"internal/ui/templates/cmmc/assessments.html\",\"internal/ui/templates/cmmc/connect.html\",\"internal/ui/templates/cmmc/control_detail.html\",\"internal/ui/templates/cmmc/controls.html\",\"internal/ui/templates/cmmc/cui_scope.html\",\"internal/ui/templates/cmmc/dashboard.html\",\"internal/ui/templates/cmmc/dispositions.html\",\"internal/ui/templates/cmmc/evidence_guide.html\",\"internal/ui/templates/cmmc/poam.html\",\"internal/ui/templates/cmmc/select_company.html\",\"internal/ui/templates/cmmc/ssp.html\",\"internal/ui/templates/compliance/approve.html\",\"internal/ui/templates/compliance/client_overview.html\",\"internal/ui/templates/compliance/device_detail.html\",\"internal/ui/templates/compliance/documents.html\",\"internal/ui/templates/compliance/frameworks.html\",\"internal/ui/templates/compliance/gap_analysis.html\",\"internal/ui/templates/composer/client_hub.html\",\"internal/ui/templates/composer/controls.html\",\"internal/ui/templates/composer/dashboard.html\",\"internal/ui/templates/composer/dispositions.html\",\"internal/ui/templates/composer/framework_clients.html\",\"internal/ui/templates/composer/hub.html\",\"internal/ui/templates/composer/live_review_setup.html\",\"internal/ui/templates/composer/ncsr/action_plan.html\",\"internal/ui/templates/composer/ncsr/assessment.html\",\"internal/ui/templates/composer/ncsr/crm_compliance.html\",\"internal/ui/templates/composer/ncsr/landing.html\",\"internal/ui/templates/composer/ncsr/reviews.html\",\"internal/ui/templates/composer/poam.html\",\"internal/ui/templates/composer/reviews_history.html\",\"internal/ui/templates/crm/activities.html\",\"internal/ui/templates/crm/activity_form.html\",\"internal/ui/templates/crm/campaign_detail.html\",\"internal/ui/templates/crm/campaign_form.html\",\"internal/ui/templates/crm/campaigns.html\",\"internal/ui/templates/crm/client_detail.html\",\"internal/ui/templates/crm/client_form.html\",\"internal/ui/templates/crm/clients.html\",\"internal/ui/templates/crm/contact_detail.html\",\"internal/ui/templates/crm/contact_form.html\",\"internal/ui/templates/crm/contacts.html\",\"internal/ui/templates/crm/deal_form.html\",\"internal/ui/templates/crm/pipeline.html\",\"internal/ui/templates/cybersec/dashboard.html\",\"internal/ui/templates/cybersec/scan_detail.html\",\"internal/ui/templates/cybersec/scan_form.html\",\"internal/ui/templates/cybersec/scan_library.html\",\"internal/ui/templates/cybersec/scan_report.html\",\"internal/ui/templates/cybersec/scans.html\",\"internal/ui/templates/cybersec/sessions.html\",\"internal/ui/templates/cybersec/siem.html\",\"internal/ui/templates/dashboard/index.html\",\"internal/ui/templates/dispatch/board.html\",\"internal/ui/templates/dispatch/calendar.html\",\"internal/ui/templates/dispatch/kanban.html\",\"internal/ui/templates/dispatch/list.html\",\"internal/ui/templates/dispatch/map.html\",\"internal/ui/templates/dispatch/settings.html\",\"internal/ui/templates/dispatch/tv.html\",\"internal/ui/templates/dispatch/unassigned.html\",\"internal/ui/templates/distributor/catalog.html\",\"internal/ui/templates/distributor/ccr_modal.html\",\"internal/ui/templates/distributor/contract_section.html\",\"internal/ui/templates/distributor/subs_partial.html\",\"internal/ui/templates/drawing/detail.html\",\"internal/ui/templates/drawing/form.html\",\"internal/ui/templates/drawing/list.html\",\"internal/ui/templates/drawing/view3d.html\",\"internal/ui/templates/employee/detail.html\",\"internal/ui/templates/employee/list.html\",\"internal/ui/templates/epa/company_tab.html\",\"internal/ui/templates/epa/dashboard.html\",\"internal/ui/templates/helpdesk/_v2_asset_console_real.html\",\"internal/ui/templates/helpdesk/_v2_card_drawer.html\",\"internal/ui/templates/helpdesk/_v2_kebab_menu.html\",\"internal/ui/templates/helpdesk/_v2_panel_asset.html\",\"internal/ui/templates/helpdesk/_v2_panel_contact.html\",\"internal/ui/templates/helpdesk/_v2_panel_contract.html\",\"internal/ui/templates/helpdesk/_v2_panel_conversation.html\",\"internal/ui/templates/helpdesk/_v2_panel_customer.html\",\"internal/ui/templates/helpdesk/change_detail.html\",\"internal/ui/templates/helpdesk/change_form.html\",\"internal/ui/templates/helpdesk/cmdb_detail.html\",\"internal/ui/templates/helpdesk/cmdb_form.html\",\"internal/ui/templates/helpdesk/contract_detail.html\",\"internal/ui/templates/helpdesk/contract_form.html\",\"internal/ui/templates/helpdesk/contracts.html\",\"internal/ui/templates/helpdesk/email_templates.html\",\"internal/ui/templates/helpdesk/kanban.html\",\"internal/ui/templates/helpdesk/peek.html\",\"internal/ui/templates/helpdesk/problem_detail.html\",\"internal/ui/templates/helpdesk/problem_form.html\",\"internal/ui/templates/helpdesk/service_form.html\",\"internal/ui/templates/helpdesk/services.html\",\"internal/ui/templates/helpdesk/ticket_detail.html\",\"internal/ui/templates/helpdesk/ticket_detail_v2.html\",\"internal/ui/templates/helpdesk/ticket_form.html\",\"internal/ui/templates/helpdesk/ticket_preview.html\",\"internal/ui/templates/helpdesk/tickets.html\",\"internal/ui/templates/helpdesk/tickets_brief.html\",\"internal/ui/templates/industry/detail.html\",\"internal/ui/templates/industry/list.html\",\"internal/ui/templates/infra/dashboard.html\",\"internal/ui/templates/infra/report.html\",\"internal/ui/templates/infra/select_company.html\",\"internal/ui/templates/itil/changes.html\",\"internal/ui/templates/itil/cmdb.html\",\"internal/ui/templates/itil/problems.html\",\"internal/ui/templates/kb/article.html\",\"internal/ui/templates/kb/article_form.html\",\"internal/ui/templates/kb/articles.html\",\"internal/ui/templates/kb/search.html\",\"internal/ui/templates/layouts/base.html\",\"internal/ui/templates/layouts/mobile.html\",\"internal/ui/templates/legal/contract_detail.html\",\"internal/ui/templates/legal/contract_form.html\",\"internal/ui/templates/legal/contract_sign.html\",\"internal/ui/templates/legal/contracts.html\",\"internal/ui/templates/mobile/agent_detail.html\",\"internal/ui/templates/mobile/agents.html\",\"internal/ui/templates/mobile/alerts.html\",\"internal/ui/templates/mobile/clients.html\",\"internal/ui/templates/mobile/dashboard.html\",\"internal/ui/templates/mobile/invoices.html\",\"internal/ui/templates/mobile/project_detail.html\",\"internal/ui/templates/mobile/projects.html\",\"internal/ui/templates/mobile/purchase_order_new.html\",\"internal/ui/templates/mobile/purchase_orders.html\",\"internal/ui/templates/mobile/security.html\",\"internal/ui/templates/mobile/settings.html\",\"internal/ui/templates/mobile/ticket_detail.html\",\"internal/ui/templates/mobile/ticket_new.html\",\"internal/ui/templates/mobile/tickets.html\",\"internal/ui/templates/mobile/time.html\",\"internal/ui/templates/mobile/timesheet.html\",\"internal/ui/templates/nexiq/gut_check.html\",\"internal/ui/templates/nexiq/index.html\",\"internal/ui/templates/nexiq/lever_lab.html\",\"internal/ui/templates/nexiq/pbr_sheets.html\",\"internal/ui/templates/nexiq/plan_cascade.html\",\"internal/ui/templates/nexiq/sales_cockpit.html\",\"internal/ui/templates/nexiq/sales_workspace.html\",\"internal/ui/templates/notifications/center.html\",\"internal/ui/templates/onboarding/detail.html\",\"internal/ui/templates/onboarding/list.html\",\"internal/ui/templates/onboarding/new.html\",\"internal/ui/templates/partials/brief_body.html\",\"internal/ui/templates/partials/compliance_stepper.html\",\"internal/ui/templates/partials/drag_grip.html\",\"internal/ui/templates/partials/photo_analysis_card.html\",\"internal/ui/templates/partials/portal_shell.html\",\"internal/ui/templates/partials/pulse_nav.html\",\"internal/ui/templates/partials/stats.html\",\"internal/ui/templates/performance/counterbalance.html\",\"internal/ui/templates/performance/dashboard.html\",\"internal/ui/templates/performance/enterprise_value.html\",\"internal/ui/templates/performance/input_editor.html\",\"internal/ui/templates/performance/mrr_context.html\",\"internal/ui/templates/performance/report.html\",\"internal/ui/templates/performance/report_print.html\",\"internal/ui/templates/performance/reports.html\",\"internal/ui/templates/performance/salaries.html\",\"internal/ui/templates/performance/section.html\",\"internal/ui/templates/performance/settings.html\",\"internal/ui/templates/performance/tools.html\",\"internal/ui/templates/performance/trends.html\",\"internal/ui/templates/po/form.html\",\"internal/ui/templates/po/list.html\",\"internal/ui/templates/po/print.html\",\"internal/ui/templates/po/quick.html\",\"internal/ui/templates/po/settings.html\",\"internal/ui/templates/portal/contract_detail.html\",\"internal/ui/templates/portal/contracts.html\",\"internal/ui/templates/portal/forbidden.html\",\"internal/ui/templates/portal/invoice_detail.html\",\"internal/ui/templates/portal/invoices.html\",\"internal/ui/templates/portal/kb.html\",\"internal/ui/templates/portal/kb_article.html\",\"internal/ui/templates/portal/landing.html\",\"internal/ui/templates/portal/login.html\",\"internal/ui/templates/portal/profile.html\",\"internal/ui/templates/portal/quote_detail.html\",\"internal/ui/templates/portal/quotes.html\",\"internal/ui/templates/portal/risk_acceptance_sign.html\",\"internal/ui/templates/portal/risk_acceptances.html\",\"internal/ui/templates/portal/support_consent.html\",\"internal/ui/templates/portal/ticket_detail.html\",\"internal/ui/templates/portal/ticket_new.html\",\"internal/ui/templates/portal/tickets.html\",\"internal/ui/templates/procurement/detail.html\",\"internal/ui/templates/procurement/list.html\",\"internal/ui/templates/procurement/remediation.html\",\"internal/ui/templates/products/assignments.html\",\"internal/ui/templates/products/catalog.html\",\"internal/ui/templates/products/product_detail.html\",\"internal/ui/templates/products/product_form.html\",\"internal/ui/templates/products/product_legal_docs_partial.html\",\"internal/ui/templates/projects/board.html\",\"internal/ui/templates/projects/cr_sign.html\",\"internal/ui/templates/projects/detail.html\",\"internal/ui/templates/projects/list.html\",\"internal/ui/templates/projects/project_form.html\",\"internal/ui/templates/projects/tabs/budget.html\",\"internal/ui/templates/projects/tabs/changes.html\",\"internal/ui/templates/projects/tabs/costs.html\",\"internal/ui/templates/projects/tabs/evm.html\",\"internal/ui/templates/projects/tabs/gantt.html\",\"internal/ui/templates/projects/tabs/lessons.html\",\"internal/ui/templates/projects/tabs/notes.html\",\"internal/ui/templates/projects/tabs/overview.html\",\"internal/ui/templates/projects/tabs/profitability.html\",\"internal/ui/templates/projects/tabs/risks.html\",\"internal/ui/templates/projects/tabs/stakeholders.html\",\"internal/ui/templates/projects/tabs/tasks.html\",\"internal/ui/templates/projects/tabs/team.html\",\"internal/ui/templates/review/binder.html\",\"internal/ui/templates/review/host.html\",\"internal/ui/templates/review/index.html\",\"internal/ui/templates/review/live.html\",\"internal/ui/templates/risk/modal.html\",\"internal/ui/templates/risk/register.html\",\"internal/ui/templates/rmm/agent_detail.html\",\"internal/ui/templates/rmm/agents.html\",\"internal/ui/templates/rmm/dashboard.html\",\"internal/ui/templates/rmm/deploy.html\",\"internal/ui/templates/rmm/desktop.html\",\"internal/ui/templates/rmm/recording_detail.html\",\"internal/ui/templates/rmm/recordings.html\",\"internal/ui/templates/rmm/sentinel.html\",\"internal/ui/templates/rmm/terminal.html\",\"internal/ui/templates/settings/ai_usage.html\",\"internal/ui/templates/settings/api_keys.html\",\"internal/ui/templates/settings/appearance.html\",\"internal/ui/templates/settings/audit.html\",\"internal/ui/templates/settings/billing.html\",\"internal/ui/templates/settings/certificates.html\",\"internal/ui/templates/settings/escalation.html\",\"internal/ui/templates/settings/health.html\",\"internal/ui/templates/settings/helpdesk.html\",\"internal/ui/templates/settings/integrations.html\",\"internal/ui/templates/settings/legal.html\",\"internal/ui/templates/settings/mcp.html\",\"internal/ui/templates/settings/metasploit.html\",\"internal/ui/templates/settings/mfa_setup.html\",\"internal/ui/templates/settings/orchestrator.html\",\"internal/ui/templates/settings/portal_roles.html\",\"internal/ui/templates/settings/profile.html\",\"internal/ui/templates/settings/qb_mappings.html\",\"internal/ui/templates/settings/quickbooks.html\",\"internal/ui/templates/settings/sessions.html\",\"internal/ui/templates/settings/tenant.html\",\"internal/ui/templates/settings/users.html\",\"internal/ui/templates/settings/voip.html\",\"internal/ui/templates/survey/audio_recordings.html\",\"internal/ui/templates/survey/departure_gate.html\",\"internal/ui/templates/survey/detail.html\",\"internal/ui/templates/survey/form.html\",\"internal/ui/templates/survey/list.html\",\"internal/ui/templates/survey/photo_analysis.html\",\"internal/ui/templates/survey/photo_analysis_detail.html\",\"internal/ui/templates/survey/photo_analysis_results.html\",\"internal/ui/templates/survey/print.html\",\"internal/ui/templates/survey/settings.html\",\"internal/ui/templates/survey/template_edit.html\",\"internal/ui/templates/survey/walkthrough.html\",\"internal/ui/templates/timesheet/all.html\",\"internal/ui/templates/timesheet/approvals.html\",\"internal/ui/templates/timesheet/detail.html\",\"internal/ui/templates/timesheet/my_timesheet.html\",\"internal/ui/templates/workorder/detail.html\",\"internal/ui/templates/workorder/form.html\",\"internal/ui/templates/workorder/list.html\",\"internal/ui/theme_mixer_test.go\",\"internal/version/version.go\",\"internal/voip/handler.go\",\"internal/voip/types.go\",\"internal/workorder/handler.go\",\"internal/workorder/types.go\"],\"internal \u2014 ai\":[\"internal/ai/claude.go\",\"internal/ai/redact.go\"],\"internal \u2014 assessment\":[\"internal/assessment/analyzer.go\",\"internal/assessment/approval_gate.go\",\"internal/assessment/blueprint_builder.go\",\"internal/assessment/document_processor.go\",\"internal/assessment/handler.go\",\"internal/assessment/quote_converter.go\",\"internal/assessment/scheduled_ops.go\",\"internal/assessment/types.go\"],\"internal \u2014 auth\":[\"internal/auth/handler.go\",\"internal/auth/jwt.go\",\"internal/auth/mfa.go\",\"internal/auth/middleware.go\",\"internal/auth/password.go\",\"internal/auth/session.go\",\"internal/auth/sso.go\"],\"internal \u2014 bidengine\":[\"internal/bidengine/addendum_apply.go\",\"internal/bidengine/addendum_split.go\",\"internal/bidengine/audit.go\",\"internal/bidengine/audit_handler.go\",\"internal/bidengine/audit_resolve.go\",\"internal/bidengine/audit_view.go\",\"internal/bidengine/drawing_discipline.go\",\"internal/bidengine/engine.go\",\"internal/bidengine/engine_test.go\",\"internal/bidengine/exclusion_extractor.go\",\"internal/bidengine/exclusion_extractor_test.go\",\"internal/bidengine/flow.go\",\"internal/bidengine/generate_from_drawings.go\",\"internal/bidengine/generate_from_spec_helpers.go\",\"internal/bidengine/handler.go\",\"internal/bidengine/outcomes.go\",\"internal/bidengine/page_selection.go\",\"internal/bidengine/processing_lock.go\",\"internal/bidengine/reconcile.go\",\"internal/bidengine/scope_summary.go\",\"internal/bidengine/trade_profile.go\",\"internal/bidengine/types.go\",\"internal/bidengine/vision_audit.go\"],\"internal \u2014 billing\":[\"internal/billing/email.go\",\"internal/billing/invoice_attachments.go\",\"internal/billing/invoice_handler.go\",\"internal/billing/quote_attachments.go\",\"internal/billing/quote_handler.go\",\"internal/billing/transcript_analyzer.go\",\"internal/billing/transcript_handler.go\",\"internal/billing/types.go\"],\"internal \u2014 certs\":[\"internal/certs/ca.go\",\"internal/certs/handler.go\",\"internal/certs/store.go\"],\"internal \u2014 cmmc\":[\"internal/cmmc/binder_render.go\",\"internal/cmmc/connect.go\",\"internal/cmmc/disposition.go\",\"internal/cmmc/handler.go\",\"internal/cmmc/nexie.go\",\"internal/cmmc/policy_render.go\",\"internal/cmmc/risk_resolver.go\",\"internal/cmmc/ssp_render.go\",\"internal/cmmc/types.go\"],\"internal \u2014 compliance\":[\"internal/compliance/handler.go\",\"internal/compliance/types.go\"],\"internal \u2014 composer\":[\"internal/composer/disposition.go\",\"internal/composer/framework.go\",\"internal/composer/handler.go\",\"internal/composer/ncsr/action_plan.go\",\"internal/composer/ncsr/crm_compliance.go\",\"internal/composer/ncsr/delete.go\",\"internal/composer/ncsr/handler.go\",\"internal/composer/ncsr/parse_pdf.go\",\"internal/composer/ncsr/parse_pdf_test.go\",\"internal/composer/ncsr/parse_xlsx.go\",\"internal/composer/ncsr/parse_xlsx_test.go\",\"internal/composer/ncsr/post_review.go\",\"internal/composer/ncsr/review_queue.go\",\"internal/composer/ncsr/review_writeback.go\",\"internal/composer/ncsr/reviews_page.go\",\"internal/composer/ncsr/risk_resolver.go\",\"internal/composer/ncsr/seeder.go\",\"internal/composer/ncsr/trajectory.go\",\"internal/composer/ncsr/types.go\",\"internal/composer/nexie.go\",\"internal/composer/reviews_history.go\",\"internal/composer/risk_resolver.go\",\"internal/composer/start_review.go\",\"internal/composer/types.go\"],\"internal \u2014 config\":[\"internal/config/config.go\"],\"internal \u2014 crm\":[\"internal/crm/accounts_handler.go\",\"internal/crm/activities_handler.go\",\"internal/crm/campaigns_handler.go\",\"internal/crm/contacts_handler.go\",\"internal/crm/deals_handler.go\",\"internal/crm/email_security_handler.go\",\"internal/crm/form_handlers.go\",\"internal/crm/handler.go\",\"internal/crm/lead_analysis.go\",\"internal/crm/lead_analysis_test.go\",\"internal/crm/pipelines_handler.go\",\"internal/crm/tenant_isolation_test.go\",\"internal/crm/types.go\"],\"internal \u2014 cybersec\":[\"internal/cybersec/handler.go\",\"internal/cybersec/types.go\"],\"internal \u2014 database\":[\"internal/database/database.go\",\"internal/database/migrate.go\",\"internal/database/migrations/001_extensions.sql\",\"internal/database/migrations/002_tenants.sql\",\"internal/database/migrations/003_roles_permissions.sql\",\"internal/database/migrations/004_users.sql\",\"internal/database/migrations/005_companies_agents.sql\",\"internal/database/migrations/006_products_compliance.sql\",\"internal/database/migrations/007_sso.sql\",\"internal/database/migrations/008_helpdesk.sql\",\"internal/database/migrations/009_compliance_cybersec_extras.sql\",\"internal/database/migrations/010_projects.sql\",\"internal/database/migrations/011_itil.sql\",\"internal/database/migrations/012_quoting_invoicing.sql\",\"internal/database/migrations/013_crm.sql\",\"internal/database/migrations/014_audit_revisions.sql\",\"internal/database/migrations/015_quickbooks_integration.sql\",\"internal/database/migrations/016_quote_require_po.sql\",\"internal/database/migrations/017_certs_mcp_email.sql\",\"internal/database/migrations/018_user_favorites.sql\",\"internal/database/migrations/019_compliance_engine.sql\",\"internal/database/migrations/020_compliance_full_controls.sql\",\"internal/database/migrations/021_compliance_doc_import.sql\",\"internal/database/migrations/022_hipaa_full_controls.sql\",\"internal/database/migrations/023_pci_dss_full_controls.sql\",\"internal/database/migrations/024_soc2_full_controls.sql\",\"internal/database/migrations/025_cmmc_full_controls.sql\",\"internal/database/migrations/026_nist_800_53_full_controls.sql\",\"internal/database/migrations/027_ai_usage_log.sql\",\"internal/database/migrations/027b_compliance_framework_source_cols.sql\",\"internal/database/migrations/028_iso27001.sql\",\"internal/database/migrations/029_nist_csf.sql\",\"internal/database/migrations/030_nist_800_171.sql\",\"internal/database/migrations/031_fedramp.sql\",\"internal/database/migrations/032_iso27701.sql\",\"internal/database/migrations/033_hitrust.sql\",\"internal/database/migrations/034_ffiec.sql\",\"internal/database/migrations/035_stateramp.sql\",\"internal/database/migrations/036_cjis.sql\",\"internal/database/migrations/037_crm_client_enhancements.sql\",\"internal/database/migrations/038_communication_channels.sql\",\"internal/database/migrations/039_country_field.sql\",\"internal/database/migrations/040_rmm_merge.sql\",\"internal/database/migrations/041_legal_documents_and_contracts.sql\",\"internal/database/migrations/042_contract_service_types.sql\",\"internal/database/migrations/043_contract_sync.sql\",\"internal/database/migrations/044_teleport_integration.sql\",\"internal/database/migrations/045_agent_polling_teleport_token.sql\",\"internal/database/migrations/046_metasploit_integration.sql\",\"internal/database/migrations/047_client_vulnerability_config.sql\",\"internal/database/migrations/048_documentation_provider.sql\",\"internal/database/migrations/049_ai_auto_resolve.sql\",\"internal/database/migrations/050_siem.sql\",\"internal/database/migrations/051_billing_work_types_and_roles.sql\",\"internal/database/migrations/052_contract_categories.sql\",\"internal/database/migrations/053_contract_rate_overrides.sql\",\"internal/database/migrations/054_esign_multi_signature.sql\",\"internal/database/migrations/055_product_legal_documents.sql\",\"internal/database/migrations/056_quote_to_contract.sql\",\"internal/database/migrations/057_quote_attachments.sql\",\"internal/database/migrations/058_siem_capture_sessions.sql\",\"internal/database/migrations/059_siem_log_retention.sql\",\"internal/database/migrations/060_certificate_store.sql\",\"internal/database/migrations/061_device_credentials.sql\",\"internal/database/migrations/062_siem_remediation.sql\",\"internal/database/migrations/063_login_audit.sql\",\"internal/database/migrations/064_invitations.sql\",\"internal/database/migrations/065_rmm_enroll_token.sql\",\"internal/database/migrations/066_persistent_timers.sql\",\"internal/database/migrations/067_security_audit_log.sql\",\"internal/database/migrations/068_multi_timer.sql\",\"internal/database/migrations/069_ticket_contract_link.sql\",\"internal/database/migrations/070_company_escalation_policy.sql\",\"internal/database/migrations/071_quote_sla_escalation_policies.sql\",\"internal/database/migrations/072_nexie_time_entries.sql\",\"internal/database/migrations/073_ticket_config.sql\",\"internal/database/migrations/074_voip_integration.sql\",\"internal/database/migrations/075_quote_sequencing.sql\",\"internal/database/migrations/076_quote_sections.sql\",\"internal/database/migrations/077_fix_contract_policy_columns.sql\",\"internal/database/migrations/078_project_management_v2.sql\",\"internal/database/migrations/079_hudu_sync_status.sql\",\"internal/database/migrations/080_health_components.sql\",\"internal/database/migrations/082_ticket_charges.sql\",\"internal/database/migrations/083_time_entry_product.sql\",\"internal/database/migrations/084_purchase_orders.sql\",\"internal/database/migrations/085_po_approvers.sql\",\"internal/database/migrations/086_vendors.sql\",\"internal/database/migrations/087_cr_esign.sql\",\"internal/database/migrations/088_po_approval_signature.sql\",\"internal/database/migrations/089_web_push.sql\",\"internal/database/migrations/090_project_site_location.sql\",\"internal/database/migrations/091_orchestrator.sql\",\"internal/database/migrations/092_workflow_engine_v2.sql\",\"internal/database/migrations/093_rmm_session_tokens.sql\",\"internal/database/migrations/094_rmm_enrollment_tokens.sql\",\"internal/database/migrations/095_email_send_log.sql\",\"internal/database/migrations/096_client_security_profile.sql\",\"internal/database/migrations/097_client_onboarding.sql\",\"internal/database/migrations/098_ach_payment_method.sql\",\"internal/database/migrations/098_device_cert_columns.sql\",\"internal/database/migrations/099_orchestrator_packages.sql\",\"internal/database/migrations/099_sla_pause_support.sql\",\"internal/database/migrations/100_note_edit_tracking.sql\",\"internal/database/migrations/101_multi_pipeline.sql\",\"internal/database/migrations/102_invoice_attachments.sql\",\"internal/database/migrations/103_recurring_invoice_schedule.sql\",\"internal/database/migrations/104_timesheets.sql\",\"internal/database/migrations/105_qb_employees.sql\",\"internal/database/migrations/106_timesheet_permissions.sql\",\"internal/database/migrations/107_portal.sql\",\"internal/database/migrations/108_portal_permissions.sql\",\"internal/database/migrations/109_contacts_country.sql\",\"internal/database/migrations/110_kb_visibility.sql\",\"internal/database/migrations/111_invoice_qb_link.sql\",\"internal/database/migrations/112_ai_usage_portal.sql\",\"internal/database/migrations/113_portal_roles.sql\",\"internal/database/migrations/114_portal_role_labels.sql\",\"internal/database/migrations/115_user_ui_theme.sql\",\"internal/database/migrations/116_user_ui_preferences.sql\",\"internal/database/migrations/117_industry_intelligence.sql\",\"internal/database/migrations/118_assessments.sql\",\"internal/database/migrations/119_orchestrator_events.sql\",\"internal/database/migrations/120_notification_actions.sql\",\"internal/database/migrations/121_scheduled_operations.sql\",\"internal/database/migrations/122_pipeline_presets.sql\",\"internal/database/migrations/123_industry_business_types.sql\",\"internal/database/migrations/124_compliance_frameworks_sync.sql\",\"internal/database/migrations/125_epa.sql\",\"internal/database/migrations/126_epa_client_contact_asset.sql\",\"internal/database/migrations/127_agent_releases.sql\",\"internal/database/migrations/128_agent_releases_v11_seed.sql\",\"internal/database/migrations/129_agent_releases_go_v110.sql\",\"internal/database/migrations/129_custom_actions.sql\",\"internal/database/migrations/131_agent_releases_go_v111.sql\",\"internal/database/migrations/132_contacts_nexie_epa_enabled.sql\",\"internal/database/migrations/133_nexuspulse.sql\",\"internal/database/migrations/134_nexuspulse_reports.sql\",\"internal/database/migrations/135_nexuspulse_rebrand_codes.sql\",\"internal/database/migrations/136_product_qbo_accounts.sql\",\"internal/database/migrations/137_product_cost.sql\",\"internal/database/migrations/138_product_subcontractor.sql\",\"internal/database/migrations/139_product_image.sql\",\"internal/database/migrations/140_product_sku.sql\",\"internal/database/migrations/141_product_supplier.sql\",\"internal/database/migrations/142_bid_engine.sql\",\"internal/database/migrations/143_bid_spec_compliance.sql\",\"internal/database/migrations/144_product_labor_hours.sql\",\"internal/database/migrations/145_bid_card_types.sql\",\"internal/database/migrations/146_bid_revisions.sql\",\"internal/database/migrations/147_bid_sections.sql\",\"internal/database/migrations/148_bid_trade_scope.sql\",\"internal/database/migrations/149_section_scope_link.sql\",\"internal/database/migrations/150_bid_address_fields.sql\",\"internal/database/migrations/151_spec_doc_ai_cost.sql\",\"internal/database/migrations/152_site_surveys.sql\",\"internal/database/migrations/153_crm_company_type.sql\",\"internal/database/migrations/154_survey_checklist_seeds.sql\",\"internal/database/migrations/155_orchestrator_survey_bid_presets.sql\",\"internal/database/migrations/156_timer_survey_support.sql\",\"internal/database/migrations/157_survey_photo_analysis.sql\",\"internal/database/migrations/158_orchestrator_photo_analysis_preset.sql\",\"internal/database/migrations/159_survey_poc_contact.sql\",\"internal/database/migrations/160_survey_audio.sql\",\"internal/database/migrations/161_drawings.sql\",\"internal/database/migrations/162_photo_thumbnails.sql\",\"internal/database/migrations/163_assessment_work_type.sql\",\"internal/database/migrations/164_work_orders.sql\",\"internal/database/migrations/165_full_lifecycle_presets.sql\",\"internal/database/migrations/166_dispatch.sql\",\"internal/database/migrations/167_employee.sql\",\"internal/database/migrations/168_seed_employees.sql\",\"internal/database/migrations/169_recorder_sessions_segments.sql\",\"internal/database/migrations/170_role_rates.sql\",\"internal/database/migrations/171_time_entry_costs.sql\",\"internal/database/migrations/172_role_rates_billing_link.sql\",\"internal/database/migrations/173_cmmc_l2_module.sql\",\"internal/database/migrations/174_cmmc_company_branding.sql\",\"internal/database/migrations/175_recorder_consent.sql\",\"internal/database/migrations/176_infra_devices.sql\",\"internal/database/migrations/177_agents_compliance_data.sql\",\"internal/database/migrations/178_cmmc_ingested_docs.sql\",\"internal/database/migrations/179_compliance_composer.sql\",\"internal/database/migrations/180_sentinel_devices.sql\",\"internal/database/migrations/181_sentinel_schema_isolation.sql\",\"internal/database/migrations/182_sentinel_audit.sql\",\"internal/database/migrations/183_agents_sentinel_hmac_key.sql\",\"internal/database/migrations/183_risk_disposition.sql\",\"internal/database/migrations/184_ncsr_assessments.sql\",\"internal/database/migrations/185_recorder_consent_cancelled.sql\",\"internal/database/migrations/185_risk_acceptances.sql\",\"internal/database/migrations/186_live_review_engine.sql\",\"internal/database/migrations/187_review_token_revocation.sql\",\"internal/database/migrations/188_ncsr_plan_jobs.sql\",\"internal/database/migrations/189_review_decision_writeback.sql\",\"internal/database/migrations/189_tickets_pinned.sql\",\"internal/database/migrations/190_backfill_action_decline_from_area.sql\",\"internal/database/migrations/190_tickets_source_enum.sql\",\"internal/database/migrations/191_backfill_action_decision_from_area.sql\",\"internal/database/migrations/192_brief_v2_fields.sql\",\"internal/database/migrations/193_ticket_followers.sql\",\"internal/database/migrations/194_review_multi_reviewer.sql\",\"internal/database/migrations/195_mcp_oauth_columns.sql\",\"internal/database/migrations/196_distributor_ingest.sql\",\"internal/database/migrations/196_helpdesk_config_tables.sql\",\"internal/database/migrations/197_tickets_fk_indexes.sql\",\"internal/database/migrations/200_contract_lock_state.sql\",\"internal/database/migrations/201_contract_line_exclusions.sql\",\"internal/database/migrations/202_contract_change_requests.sql\",\"internal/database/migrations/203_contract_line_audit.sql\",\"internal/database/migrations/204_nexiq_foundation.sql\",\"internal/database/migrations/205_pbr_command_control_fields.sql\",\"internal/database/migrations/206_bid_drawing_reconciliation.sql\",\"internal/database/migrations/207_bid_mode.sql\",\"internal/database/migrations/208_quote_totals_trigger.sql\",\"internal/database/migrations/209_invoice_totals_trigger.sql\",\"internal/database/migrations/210_lifecycle_events.sql\",\"internal/database/migrations/210_quote_line_client_selection.sql\",\"internal/database/migrations/211_quote_totals_effective.sql\",\"internal/database/migrations/212_phase3_purchase_orders_procurement.sql\",\"internal/database/migrations/213_po_remediation_queue.sql\",\"internal/database/migrations/214_vendor_distributor_auto_dispatch.sql\",\"internal/database/migrations/215_purchase_order_audit.sql\",\"internal/database/migrations/216_contract_line_procurement.sql\",\"internal/database/migrations/217_deprecate_project_purchase_orders.sql\",\"internal/database/migrations/218_bid_ai_scope_summary.sql\",\"internal/database/migrations/219_bid_nexie_last_error.sql\",\"internal/database/migrations/220_drawings_page_selection.sql\",\"internal/database/migrations/221_peek_summary.sql\",\"internal/database/migrations/222_bid_line_part_number.sql\",\"internal/database/migrations/223_bid_addendum_changes.sql\",\"internal/database/migrations/224_drawing_supersession.sql\",\"internal/database/migrations/225_addendum_diff_status.sql\",\"internal/database/migrations/226_addendum_audit_trail.sql\",\"internal/database/migrations/227_addendum_decision_audit.sql\",\"internal/database/migrations/228_addendum_scope_exclusion.sql\",\"internal/database/migrations/229_backfill_orphan_recon_line_items.sql\",\"internal/database/migrations/230_bid_line_scope_flags.sql\",\"internal/database/migrations/231_bid_trade_profiles.sql\",\"internal/database/migrations/232_bid_outcomes.sql\",\"internal/database/migrations/233_drawings_extracted_text.sql\",\"internal/database/migrations/234_bid_audit_acks.sql\",\"internal/database/migrations/235_tag_tables_tenant_id.sql\",\"internal/database/migrations/236_campaign_recipients_tenant_id.sql\"],\"internal \u2014 devices\":[\"internal/devices/fortigate.go\",\"internal/devices/handler.go\"],\"internal \u2014 dispatch\":[\"internal/dispatch/graph_calendar.go\",\"internal/dispatch/handler.go\",\"internal/dispatch/types.go\"],\"internal \u2014 distributor\":[\"internal/distributor/catalog_query.go\",\"internal/distributor/contract_query.go\",\"internal/distributor/handler.go\",\"internal/distributor/provisioner.go\",\"internal/distributor/scheduler.go\",\"internal/distributor/status.go\",\"internal/distributor/subs_query.go\",\"internal/distributor/sync.go\",\"internal/distributor/types.go\",\"internal/distributor/wrapper.go\"],\"internal \u2014 drawing\":[\"internal/drawing/handler.go\",\"internal/drawing/isometric.go\",\"internal/drawing/takeoff.go\",\"internal/drawing/types.go\",\"internal/drawing/vision.go\"],\"internal \u2014 emailsec\":[\"internal/emailsec/graph.go\",\"internal/emailsec/handler.go\",\"internal/emailsec/headers.go\",\"internal/emailsec/llm_triage.go\",\"internal/emailsec/pipeline.go\",\"internal/emailsec/scoring.go\"],\"internal \u2014 employee\":[\"internal/employee/handler.go\",\"internal/employee/types.go\"],\"internal \u2014 epa\":[\"internal/epa/handler.go\",\"internal/epa/store.go\",\"internal/epa/types.go\"],\"internal \u2014 financial\":[\"internal/financial/provider.go\",\"internal/financial/qbo/adapter.go\",\"internal/financial/qbo/provider.go\",\"internal/financial/types.go\"],\"internal \u2014 health\":[\"internal/health/disk_darwin.go\",\"internal/health/disk_linux.go\",\"internal/health/disk_windows.go\",\"internal/health/handler.go\"],\"internal \u2014 helpdesk\":[\"internal/helpdesk/capabilities.go\",\"internal/helpdesk/config_audit.go\",\"internal/helpdesk/config_handler.go\",\"internal/helpdesk/config_seed.go\",\"internal/helpdesk/contract.go\",\"internal/helpdesk/email.go\",\"internal/helpdesk/handler.go\",\"internal/helpdesk/handler_v2.go\",\"internal/helpdesk/orchestrator.go\",\"internal/helpdesk/peek_handler.go\",\"internal/helpdesk/peek_hudu.go\",\"internal/helpdesk/peek_store.go\",\"internal/helpdesk/peek_types.go\",\"internal/helpdesk/recorder_session.go\",\"internal/helpdesk/recordings_handler.go\",\"internal/helpdesk/sla.go\",\"internal/helpdesk/store.go\",\"internal/helpdesk/ticket_detail_helpers.go\",\"internal/helpdesk/tickets_prefs.go\",\"internal/helpdesk/tracking.go\",\"internal/helpdesk/types.go\",\"internal/helpdesk/types_itil.go\",\"internal/helpdesk/workflow.go\"],\"internal \u2014 hudu\":[\"internal/hudu/client.go\",\"internal/hudu/handler.go\",\"internal/hudu/types.go\"],\"internal \u2014 industry\":[\"internal/industry/handler.go\"],\"internal \u2014 infra\":[\"internal/infra/audit.go\",\"internal/infra/handler.go\",\"internal/infra/hudu.go\",\"internal/infra/runbook.go\",\"internal/infra/ssrf_guard.go\",\"internal/infra/types.go\",\"internal/infra/verify.go\"],\"internal \u2014 itil\":[\"internal/itil/itil_handler.go\",\"internal/itil/itil_store.go\",\"internal/itil/itil_types.go\"],\"internal \u2014 kb\":[\"internal/kb/handler.go\",\"internal/kb/types.go\"],\"internal \u2014 legal\":[\"internal/legal/ach_submit_helpers.go\",\"internal/legal/analyze.go\",\"internal/legal/billing_handler.go\",\"internal/legal/billing_types.go\",\"internal/legal/contract_summary_generator.go\",\"internal/legal/crypto.go\",\"internal/legal/docx_renderer.go\",\"internal/legal/edit_contract_form_helpers.go\",\"internal/legal/executed_package.go\",\"internal/legal/handler.go\",\"internal/legal/msa_generator.go\",\"internal/legal/order_generator.go\",\"internal/legal/quote_conversion.go\",\"internal/legal/sign_contract_helpers.go\",\"internal/legal/signature_stamper.go\",\"internal/legal/signing_page_helpers.go\",\"internal/legal/sync.go\",\"internal/legal/types.go\"],\"internal \u2014 lifecycle\":[\"internal/lifecycle/lifecycle.go\"],\"internal \u2014 mcpclient\":[\"internal/mcpclient/client.go\"],\"internal \u2014 mcpvendors\":[\"internal/mcpvendors/factory.go\",\"internal/mcpvendors/pax8/pax8.go\",\"internal/mcpvendors/resolver.go\"],\"internal \u2014 metasploit\":[\"internal/metasploit/client.go\",\"internal/metasploit/handler.go\",\"internal/metasploit/types.go\"],\"internal \u2014 middleware\":[\"internal/middleware/csrf.go\",\"internal/middleware/middleware.go\"],\"internal \u2014 mobile\":[\"internal/mobile/handler.go\"],\"internal \u2014 nexie\":[\"internal/nexie/handler.go\",\"internal/nexie/query.go\",\"internal/nexie/schema.go\"],\"internal \u2014 nexiq\":[\"internal/nexiq/cascade.go\",\"internal/nexiq/cockpit.go\",\"internal/nexiq/events.go\",\"internal/nexiq/events_test.go\",\"internal/nexiq/goals.go\",\"internal/nexiq/gut_check.go\",\"internal/nexiq/handler.go\",\"internal/nexiq/pages.go\",\"internal/nexiq/permissions.go\",\"internal/nexiq/scenarios.go\",\"internal/nexiq/stubs.go\",\"internal/nexiq/workspace.go\"],\"internal \u2014 notify\":[\"internal/notify/service.go\"],\"internal \u2014 onboarding\":[\"internal/onboarding/blueprint_injection.go\",\"internal/onboarding/handler.go\",\"internal/onboarding/phases.go\",\"internal/onboarding/types.go\"],\"internal \u2014 performance\":[\"internal/performance/analyzer.go\",\"internal/performance/calculator.go\",\"internal/performance/collector.go\",\"internal/performance/handler.go\",\"internal/performance/qbo_mapper.go\",\"internal/performance/store.go\",\"internal/performance/types.go\"],\"internal \u2014 po\":[\"internal/po/handler.go\"],\"internal \u2014 portal\":[\"internal/portal/admin.go\",\"internal/portal/handler.go\",\"internal/portal/middleware.go\",\"internal/portal/nexie.go\",\"internal/portal/nexie_tools.go\",\"internal/portal/pages.go\",\"internal/portal/risk_acceptances.go\",\"internal/portal/store.go\",\"internal/portal/types.go\",\"internal/portal/usage.go\"],\"internal \u2014 procurement\":[\"internal/procurement/audit.go\",\"internal/procurement/handler.go\",\"internal/procurement/remediation.go\",\"internal/procurement/service.go\",\"internal/procurement/types.go\"],\"internal \u2014 product\":[\"internal/product/handler.go\",\"internal/product/project_handler.go\",\"internal/product/types.go\",\"internal/product/types_project.go\"],\"internal \u2014 project\":[\"internal/project/costing.go\",\"internal/project/evm.go\",\"internal/project/handler.go\",\"internal/project/safe_columns.go\",\"internal/project/types.go\"],\"internal \u2014 push\":[\"internal/push/handler.go\"],\"internal \u2014 quickbooks\":[\"internal/quickbooks/client.go\",\"internal/quickbooks/employees_sync.go\",\"internal/quickbooks/oauth.go\",\"internal/quickbooks/po_adapter.go\",\"internal/quickbooks/push.go\",\"internal/quickbooks/recurring_billing.go\",\"internal/quickbooks/service.go\",\"internal/quickbooks/sync.go\",\"internal/quickbooks/timesheet_push.go\",\"internal/quickbooks/types.go\"],\"internal \u2014 rbac\":[\"internal/rbac/roles.go\"],\"internal \u2014 review\":[\"internal/review/binder.go\",\"internal/review/broker.go\",\"internal/review/handler.go\",\"internal/review/qr.go\",\"internal/review/repository.go\",\"internal/review/security.go\",\"internal/review/seeder.go\",\"internal/review/types.go\"],\"internal \u2014 risk\":[\"internal/risk/handler.go\",\"internal/risk/print.go\",\"internal/risk/registry.go\",\"internal/risk/service.go\",\"internal/risk/types.go\"],\"internal \u2014 rmm\":[\"internal/rmm/agent_api.go\",\"internal/rmm/agent_update.go\",\"internal/rmm/crypto/tls.go\",\"internal/rmm/device_api.go\",\"internal/rmm/enroll.go\",\"internal/rmm/handler.go\",\"internal/rmm/install.go\",\"internal/rmm/installers/linux.sh\",\"internal/rmm/installers/macos.sh\",\"internal/rmm/installers/nexusos-agent.wxs.tmpl\",\"internal/rmm/installers/windows.ps1\",\"internal/rmm/msi.go\",\"internal/rmm/protocol/compliance.go\",\"internal/rmm/protocol/inventory.go\",\"internal/rmm/protocol/types.go\",\"internal/rmm/recorder.go\",\"internal/rmm/recordings.go\",\"internal/rmm/remote/desktop.go\",\"internal/rmm/remote/session.go\",\"internal/rmm/remote/tunnel.go\",\"internal/rmm/sentinel.go\",\"internal/rmm/sentinel_socks_hub.go\",\"internal/rmm/session_token.go\",\"internal/rmm/types.go\",\"internal/rmm/update_api.go\"],\"internal \u2014 sentinel\":[\"internal/sentinel/capability.go\",\"internal/sentinel/secrets.go\",\"internal/sentinel/types.go\"],\"internal \u2014 settings\":[\"internal/settings/appearance_handler.go\",\"internal/settings/tab_order_handler.go\",\"internal/settings/tab_visibility_handler.go\"],\"internal \u2014 siem\":[\"internal/siem/handler.go\",\"internal/siem/pcap.go\",\"internal/siem/syslog.go\",\"internal/siem/types.go\"],\"internal \u2014 survey\":[\"internal/survey/audio.go\",\"internal/survey/handler.go\",\"internal/survey/types.go\",\"internal/survey/vision.go\",\"internal/survey/voice.go\"],\"internal \u2014 tenant\":[\"internal/tenant/handler.go\",\"internal/tenant/mcp_oauth.go\"],\"internal \u2014 timer\":[\"internal/timer/billing_context_helpers.go\",\"internal/timer/escalate_helpers.go\",\"internal/timer/handler.go\",\"internal/timer/list_timers_helpers.go\",\"internal/timer/stop_helpers.go\"],\"internal \u2014 timesheet\":[\"internal/timesheet/handler.go\",\"internal/timesheet/store.go\",\"internal/timesheet/types.go\"],\"internal \u2014 ui\":[\"internal/ui/preferences.go\",\"internal/ui/render.go\",\"internal/ui/render_v2_test.go\",\"internal/ui/static/css/epa.css\",\"internal/ui/static/css/helpdesk-glass.css\",\"internal/ui/static/css/helpdesk-peek.css\",\"internal/ui/static/css/nexus.css\",\"internal/ui/static/css/theme-mixer.css\",\"internal/ui/static/icons/placeholder.txt\",\"internal/ui/static/js/app.js\",\"internal/ui/static/js/csrf.js\",\"internal/ui/static/js/sigpad.js\",\"internal/ui/static/js/sw.js\",\"internal/ui/static/manifest-mobile.json\",\"internal/ui/static/manifest.json\",\"internal/ui/templates/assessment/detail.html\",\"internal/ui/templates/assessment/form.html\",\"internal/ui/templates/assessment/list.html\",\"internal/ui/templates/auth/invite.html\",\"internal/ui/templates/auth/login.html\",\"internal/ui/templates/bidding/audit.html\",\"internal/ui/templates/bidding/bid_form.html\",\"internal/ui/templates/bidding/calibration.html\",\"internal/ui/templates/bidding/cost_engine.html\",\"internal/ui/templates/bidding/detail.html\",\"internal/ui/templates/bidding/drawing_selection.html\",\"internal/ui/templates/bidding/list.html\",\"internal/ui/templates/bidding/print_detail.html\",\"internal/ui/templates/bidding/print_shadow.html\",\"internal/ui/templates/bidding/settings.html\",\"internal/ui/templates/bidding/takeoff.html\",\"internal/ui/templates/bidding/vision_audit.html\",\"internal/ui/templates/billing/aging.html\",\"internal/ui/templates/billing/invoice_detail.html\",\"internal/ui/templates/billing/invoice_edit.html\",\"internal/ui/templates/billing/invoice_form.html\",\"internal/ui/templates/billing/invoice_print.html\",\"internal/ui/templates/billing/invoices.html\",\"internal/ui/templates/billing/quote_approve.html\",\"internal/ui/templates/billing/quote_detail.html\",\"internal/ui/templates/billing/quote_edit.html\",\"internal/ui/templates/billing/quote_form.html\",\"internal/ui/templates/billing/quote_print.html\",\"internal/ui/templates/billing/quotes.html\",\"internal/ui/templates/billing/transcript_import.html\",\"internal/ui/templates/cmmc/assessments.html\",\"internal/ui/templates/cmmc/connect.html\",\"internal/ui/templates/cmmc/control_detail.html\",\"internal/ui/templates/cmmc/controls.html\",\"internal/ui/templates/cmmc/cui_scope.html\",\"internal/ui/templates/cmmc/dashboard.html\",\"internal/ui/templates/cmmc/dispositions.html\",\"internal/ui/templates/cmmc/evidence_guide.html\",\"internal/ui/templates/cmmc/poam.html\",\"internal/ui/templates/cmmc/select_company.html\",\"internal/ui/templates/cmmc/ssp.html\",\"internal/ui/templates/compliance/approve.html\",\"internal/ui/templates/compliance/client_overview.html\",\"internal/ui/templates/compliance/device_detail.html\",\"internal/ui/templates/compliance/documents.html\",\"internal/ui/templates/compliance/frameworks.html\",\"internal/ui/templates/compliance/gap_analysis.html\",\"internal/ui/templates/composer/client_hub.html\",\"internal/ui/templates/composer/controls.html\",\"internal/ui/templates/composer/dashboard.html\",\"internal/ui/templates/composer/dispositions.html\",\"internal/ui/templates/composer/framework_clients.html\",\"internal/ui/templates/composer/hub.html\",\"internal/ui/templates/composer/live_review_setup.html\",\"internal/ui/templates/composer/ncsr/action_plan.html\",\"internal/ui/templates/composer/ncsr/assessment.html\",\"internal/ui/templates/composer/ncsr/crm_compliance.html\",\"internal/ui/templates/composer/ncsr/landing.html\",\"internal/ui/templates/composer/ncsr/reviews.html\",\"internal/ui/templates/composer/poam.html\",\"internal/ui/templates/composer/reviews_history.html\",\"internal/ui/templates/crm/activities.html\",\"internal/ui/templates/crm/activity_form.html\",\"internal/ui/templates/crm/campaign_detail.html\",\"internal/ui/templates/crm/campaign_form.html\",\"internal/ui/templates/crm/campaigns.html\",\"internal/ui/templates/crm/client_detail.html\",\"internal/ui/templates/crm/client_form.html\",\"internal/ui/templates/crm/clients.html\",\"internal/ui/templates/crm/contact_detail.html\",\"internal/ui/templates/crm/contact_form.html\",\"internal/ui/templates/crm/contacts.html\",\"internal/ui/templates/crm/deal_form.html\",\"internal/ui/templates/crm/pipeline.html\",\"internal/ui/templates/cybersec/dashboard.html\",\"internal/ui/templates/cybersec/scan_detail.html\",\"internal/ui/templates/cybersec/scan_form.html\",\"internal/ui/templates/cybersec/scan_library.html\",\"internal/ui/templates/cybersec/scan_report.html\",\"internal/ui/templates/cybersec/scans.html\",\"internal/ui/templates/cybersec/sessions.html\",\"internal/ui/templates/cybersec/siem.html\",\"internal/ui/templates/dashboard/index.html\",\"internal/ui/templates/dispatch/board.html\",\"internal/ui/templates/dispatch/calendar.html\",\"internal/ui/templates/dispatch/kanban.html\",\"internal/ui/templates/dispatch/list.html\",\"internal/ui/templates/dispatch/map.html\",\"internal/ui/templates/dispatch/settings.html\",\"internal/ui/templates/dispatch/tv.html\",\"internal/ui/templates/dispatch/unassigned.html\",\"internal/ui/templates/distributor/catalog.html\",\"internal/ui/templates/distributor/ccr_modal.html\",\"internal/ui/templates/distributor/contract_section.html\",\"internal/ui/templates/distributor/subs_partial.html\",\"internal/ui/templates/drawing/detail.html\",\"internal/ui/templates/drawing/form.html\",\"internal/ui/templates/drawing/list.html\",\"internal/ui/templates/drawing/view3d.html\",\"internal/ui/templates/employee/detail.html\",\"internal/ui/templates/employee/list.html\",\"internal/ui/templates/epa/company_tab.html\",\"internal/ui/templates/epa/dashboard.html\",\"internal/ui/templates/helpdesk/_v2_asset_console_real.html\",\"internal/ui/templates/helpdesk/_v2_card_drawer.html\",\"internal/ui/templates/helpdesk/_v2_kebab_menu.html\",\"internal/ui/templates/helpdesk/_v2_panel_asset.html\",\"internal/ui/templates/helpdesk/_v2_panel_contact.html\",\"internal/ui/templates/helpdesk/_v2_panel_contract.html\",\"internal/ui/templates/helpdesk/_v2_panel_conversation.html\",\"internal/ui/templates/helpdesk/_v2_panel_customer.html\",\"internal/ui/templates/helpdesk/change_detail.html\",\"internal/ui/templates/helpdesk/change_form.html\",\"internal/ui/templates/helpdesk/cmdb_detail.html\",\"internal/ui/templates/helpdesk/cmdb_form.html\",\"internal/ui/templates/helpdesk/contract_detail.html\",\"internal/ui/templates/helpdesk/contract_form.html\",\"internal/ui/templates/helpdesk/contracts.html\",\"internal/ui/templates/helpdesk/email_templates.html\",\"internal/ui/templates/helpdesk/kanban.html\",\"internal/ui/templates/helpdesk/peek.html\",\"internal/ui/templates/helpdesk/problem_detail.html\",\"internal/ui/templates/helpdesk/problem_form.html\",\"internal/ui/templates/helpdesk/service_form.html\",\"internal/ui/templates/helpdesk/services.html\",\"internal/ui/templates/helpdesk/ticket_detail.html\",\"internal/ui/templates/helpdesk/ticket_detail_v2.html\",\"internal/ui/templates/helpdesk/ticket_form.html\",\"internal/ui/templates/helpdesk/ticket_preview.html\",\"internal/ui/templates/helpdesk/tickets.html\",\"internal/ui/templates/helpdesk/tickets_brief.html\",\"internal/ui/templates/industry/detail.html\",\"internal/ui/templates/industry/list.html\",\"internal/ui/templates/infra/dashboard.html\",\"internal/ui/templates/infra/report.html\",\"internal/ui/templates/infra/select_company.html\",\"internal/ui/templates/itil/changes.html\",\"internal/ui/templates/itil/cmdb.html\",\"internal/ui/templates/itil/problems.html\",\"internal/ui/templates/kb/article.html\",\"internal/ui/templates/kb/article_form.html\",\"internal/ui/templates/kb/articles.html\",\"internal/ui/templates/kb/search.html\",\"internal/ui/templates/layouts/base.html\",\"internal/ui/templates/layouts/mobile.html\",\"internal/ui/templates/legal/contract_detail.html\",\"internal/ui/templates/legal/contract_form.html\",\"internal/ui/templates/legal/contract_sign.html\",\"internal/ui/templates/legal/contracts.html\",\"internal/ui/templates/mobile/agent_detail.html\",\"internal/ui/templates/mobile/agents.html\",\"internal/ui/templates/mobile/alerts.html\",\"internal/ui/templates/mobile/clients.html\",\"internal/ui/templates/mobile/dashboard.html\",\"internal/ui/templates/mobile/invoices.html\",\"internal/ui/templates/mobile/project_detail.html\",\"internal/ui/templates/mobile/projects.html\",\"internal/ui/templates/mobile/purchase_order_new.html\",\"internal/ui/templates/mobile/purchase_orders.html\",\"internal/ui/templates/mobile/security.html\",\"internal/ui/templates/mobile/settings.html\",\"internal/ui/templates/mobile/ticket_detail.html\",\"internal/ui/templates/mobile/ticket_new.html\",\"internal/ui/templates/mobile/tickets.html\",\"internal/ui/templates/mobile/time.html\",\"internal/ui/templates/mobile/timesheet.html\",\"internal/ui/templates/nexiq/gut_check.html\",\"internal/ui/templates/nexiq/index.html\",\"internal/ui/templates/nexiq/lever_lab.html\",\"internal/ui/templates/nexiq/pbr_sheets.html\",\"internal/ui/templates/nexiq/plan_cascade.html\",\"internal/ui/templates/nexiq/sales_cockpit.html\",\"internal/ui/templates/nexiq/sales_workspace.html\",\"internal/ui/templates/notifications/center.html\",\"internal/ui/templates/onboarding/detail.html\",\"internal/ui/templates/onboarding/list.html\",\"internal/ui/templates/onboarding/new.html\",\"internal/ui/templates/partials/brief_body.html\",\"internal/ui/templates/partials/compliance_stepper.html\",\"internal/ui/templates/partials/drag_grip.html\",\"internal/ui/templates/partials/photo_analysis_card.html\",\"internal/ui/templates/partials/portal_shell.html\",\"internal/ui/templates/partials/pulse_nav.html\",\"internal/ui/templates/partials/stats.html\",\"internal/ui/templates/performance/counterbalance.html\",\"internal/ui/templates/performance/dashboard.html\",\"internal/ui/templates/performance/enterprise_value.html\",\"internal/ui/templates/performance/input_editor.html\",\"internal/ui/templates/performance/mrr_context.html\",\"internal/ui/templates/performance/report.html\",\"internal/ui/templates/performance/report_print.html\",\"internal/ui/templates/performance/reports.html\",\"internal/ui/templates/performance/salaries.html\",\"internal/ui/templates/performance/section.html\",\"internal/ui/templates/performance/settings.html\",\"internal/ui/templates/performance/tools.html\",\"internal/ui/templates/performance/trends.html\",\"internal/ui/templates/po/form.html\",\"internal/ui/templates/po/list.html\",\"internal/ui/templates/po/print.html\",\"internal/ui/templates/po/quick.html\",\"internal/ui/templates/po/settings.html\",\"internal/ui/templates/portal/contract_detail.html\",\"internal/ui/templates/portal/contracts.html\",\"internal/ui/templates/portal/forbidden.html\",\"internal/ui/templates/portal/invoice_detail.html\",\"internal/ui/templates/portal/invoices.html\",\"internal/ui/templates/portal/kb.html\",\"internal/ui/templates/portal/kb_article.html\",\"internal/ui/templates/portal/landing.html\",\"internal/ui/templates/portal/login.html\",\"internal/ui/templates/portal/profile.html\",\"internal/ui/templates/portal/quote_detail.html\",\"internal/ui/templates/portal/quotes.html\",\"internal/ui/templates/portal/risk_acceptance_sign.html\",\"internal/ui/templates/portal/risk_acceptances.html\",\"internal/ui/templates/portal/support_consent.html\",\"internal/ui/templates/portal/ticket_detail.html\",\"internal/ui/templates/portal/ticket_new.html\",\"internal/ui/templates/portal/tickets.html\",\"internal/ui/templates/procurement/detail.html\",\"internal/ui/templates/procurement/list.html\",\"internal/ui/templates/procurement/remediation.html\",\"internal/ui/templates/products/assignments.html\",\"internal/ui/templates/products/catalog.html\",\"internal/ui/templates/products/product_detail.html\",\"internal/ui/templates/products/product_form.html\",\"internal/ui/templates/products/product_legal_docs_partial.html\",\"internal/ui/templates/projects/board.html\",\"internal/ui/templates/projects/cr_sign.html\",\"internal/ui/templates/projects/detail.html\",\"internal/ui/templates/projects/list.html\",\"internal/ui/templates/projects/project_form.html\",\"internal/ui/templates/projects/tabs/budget.html\",\"internal/ui/templates/projects/tabs/changes.html\",\"internal/ui/templates/projects/tabs/costs.html\",\"internal/ui/templates/projects/tabs/evm.html\",\"internal/ui/templates/projects/tabs/gantt.html\",\"internal/ui/templates/projects/tabs/lessons.html\",\"internal/ui/templates/projects/tabs/notes.html\",\"internal/ui/templates/projects/tabs/overview.html\",\"internal/ui/templates/projects/tabs/profitability.html\",\"internal/ui/templates/projects/tabs/risks.html\",\"internal/ui/templates/projects/tabs/stakeholders.html\",\"internal/ui/templates/projects/tabs/tasks.html\",\"internal/ui/templates/projects/tabs/team.html\",\"internal/ui/templates/review/binder.html\",\"internal/ui/templates/review/host.html\",\"internal/ui/templates/review/index.html\",\"internal/ui/templates/review/live.html\",\"internal/ui/templates/risk/modal.html\",\"internal/ui/templates/risk/register.html\",\"internal/ui/templates/rmm/agent_detail.html\",\"internal/ui/templates/rmm/agents.html\",\"internal/ui/templates/rmm/dashboard.html\",\"internal/ui/templates/rmm/deploy.html\",\"internal/ui/templates/rmm/desktop.html\",\"internal/ui/templates/rmm/recording_detail.html\",\"internal/ui/templates/rmm/recordings.html\",\"internal/ui/templates/rmm/sentinel.html\",\"internal/ui/templates/rmm/terminal.html\",\"internal/ui/templates/settings/ai_usage.html\",\"internal/ui/templates/settings/api_keys.html\",\"internal/ui/templates/settings/appearance.html\",\"internal/ui/templates/settings/audit.html\",\"internal/ui/templates/settings/billing.html\",\"internal/ui/templates/settings/certificates.html\",\"internal/ui/templates/settings/escalation.html\",\"internal/ui/templates/settings/health.html\",\"internal/ui/templates/settings/helpdesk.html\",\"internal/ui/templates/settings/integrations.html\",\"internal/ui/templates/settings/legal.html\",\"internal/ui/templates/settings/mcp.html\",\"internal/ui/templates/settings/metasploit.html\",\"internal/ui/templates/settings/mfa_setup.html\",\"internal/ui/templates/settings/orchestrator.html\",\"internal/ui/templates/settings/portal_roles.html\",\"internal/ui/templates/settings/profile.html\",\"internal/ui/templates/settings/qb_mappings.html\",\"internal/ui/templates/settings/quickbooks.html\",\"internal/ui/templates/settings/sessions.html\",\"internal/ui/templates/settings/tenant.html\",\"internal/ui/templates/settings/users.html\",\"internal/ui/templates/settings/voip.html\",\"internal/ui/templates/survey/audio_recordings.html\",\"internal/ui/templates/survey/departure_gate.html\",\"internal/ui/templates/survey/detail.html\",\"internal/ui/templates/survey/form.html\",\"internal/ui/templates/survey/list.html\",\"internal/ui/templates/survey/photo_analysis.html\",\"internal/ui/templates/survey/photo_analysis_detail.html\",\"internal/ui/templates/survey/photo_analysis_results.html\",\"internal/ui/templates/survey/print.html\",\"internal/ui/templates/survey/settings.html\",\"internal/ui/templates/survey/template_edit.html\",\"internal/ui/templates/survey/walkthrough.html\",\"internal/ui/templates/timesheet/all.html\",\"internal/ui/templates/timesheet/approvals.html\",\"internal/ui/templates/timesheet/detail.html\",\"internal/ui/templates/timesheet/my_timesheet.html\",\"internal/ui/templates/workorder/detail.html\",\"internal/ui/templates/workorder/form.html\",\"internal/ui/templates/workorder/list.html\",\"internal/ui/theme_mixer_test.go\"],\"internal \u2014 version\":[\"internal/version/version.go\"],\"internal \u2014 voip\":[\"internal/voip/handler.go\",\"internal/voip/types.go\"],\"internal \u2014 workorder\":[\"internal/workorder/handler.go\",\"internal/workorder/types.go\"],\"mockups\":[\"mockups/bid_drawing_page_selection.html\",\"mockups/crm_client_compliance_tab.html\",\"mockups/distributor_s1_vendor_settings.html\",\"mockups/distributor_s2_catalog_browser.html\",\"mockups/distributor_s3_client_subscriptions.html\",\"mockups/distributor_s4_contract_distributor_lines.html\",\"mockups/distributor_s5_product_catalog.html\",\"mockups/distributor_s6_invoice_distributor_cost.html\",\"mockups/ncsr_action_plan_post_review.html\",\"mockups/ncsr_assessment_full_page.html\",\"mockups/ncsr_maturity_trajectory.html\",\"mockups/ncsr_post_review_overview.html\",\"mockups/ncsr_reviews_history_tab.html\",\"mockups/ncsr_sectioned_multi_reviewer.html\",\"mockups/pick-pages-hover.html\",\"mockups/pick-pages-mockup.html\",\"mockups/pick-pages-placement.html\",\"mockups/pick-pages-redesign.html\",\"mockups/slideout_mockup.html\",\"mockups/nexusiq/ARCHITECTURE.md\",\"mockups/nexusiq/gut_check.html\",\"mockups/nexusiq/index.html\",\"mockups/nexusiq/lever_lab.html\",\"mockups/nexusiq/plan_cascade.html\",\"mockups/nexusiq/proposal_v2_sales_cockpit.html\",\"mockups/nexusiq/proposal_v2_sales_workspace.html\",\"mockups/nexusiq/proposal_v3_pbr_dense_grid.html\",\"mockups/nexusiq/proposal_v3b_pbr_full_sheet.html\",\"mockups/nexusiq/sales_cockpit.html\",\"mockups/nexusiq/sales_workspace.html\"],\"mockups \u2014 mockups\":[\"mockups/bid_drawing_page_selection.html\",\"mockups/crm_client_compliance_tab.html\",\"mockups/distributor_s1_vendor_settings.html\",\"mockups/distributor_s2_catalog_browser.html\",\"mockups/distributor_s3_client_subscriptions.html\",\"mockups/distributor_s4_contract_distributor_lines.html\",\"mockups/distributor_s5_product_catalog.html\",\"mockups/distributor_s6_invoice_distributor_cost.html\",\"mockups/ncsr_action_plan_post_review.html\",\"mockups/ncsr_assessment_full_page.html\",\"mockups/ncsr_maturity_trajectory.html\",\"mockups/ncsr_post_review_overview.html\",\"mockups/ncsr_reviews_history_tab.html\",\"mockups/ncsr_sectioned_multi_reviewer.html\",\"mockups/pick-pages-hover.html\",\"mockups/pick-pages-mockup.html\",\"mockups/pick-pages-placement.html\",\"mockups/pick-pages-redesign.html\",\"mockups/slideout_mockup.html\"],\"mockups \u2014 nexusiq\":[\"mockups/nexusiq/ARCHITECTURE.md\",\"mockups/nexusiq/gut_check.html\",\"mockups/nexusiq/index.html\",\"mockups/nexusiq/lever_lab.html\",\"mockups/nexusiq/plan_cascade.html\",\"mockups/nexusiq/proposal_v2_sales_cockpit.html\",\"mockups/nexusiq/proposal_v2_sales_workspace.html\",\"mockups/nexusiq/proposal_v3_pbr_dense_grid.html\",\"mockups/nexusiq/proposal_v3b_pbr_full_sheet.html\",\"mockups/nexusiq/sales_cockpit.html\",\"mockups/nexusiq/sales_workspace.html\"],\"scripts\":[\"scripts/changelog/main.go\",\"scripts/check-tenant-id.sh\",\"scripts/proxmox-deploy.sh\",\"scripts/security_gen/main.go\",\"scripts/seed.go\"]},\"moduleTree\":[{\"name\":\"Root\",\"slug\":\"root\",\"files\":[\"AGENTS.md\",\"CLAUDE.md\",\"Dockerfile\",\"Makefile\",\"docker-compose.dev.yml\",\"docker-compose.yml\",\"go.mod\"]},{\"name\":\"branding\",\"slug\":\"branding\",\"files\":[\"branding/BRAND_GUIDELINES.md\",\"branding/COLOR_PALETTE.md\",\"branding/adobe_firefly_prompt.md\",\"branding/landing_page_sample.html\",\"branding/letterhead_sample_1.html\",\"branding/letterhead_sample_2.html\",\"branding/letterhead_sample_3.html\",\"branding/nexoscore_logo_v1.html\",\"branding/nexoscore_logo_v2.html\",\"branding/typography_sample_1.html\",\"branding/typography_sample_2.html\",\"branding/typography_sample_3.html\",\"branding/typography_sample_4a.html\",\"branding/typography_sample_4b.html\",\"branding/typography_sample_4c.html\",\"branding/typography_sample_4d.html\"]},{\"name\":\"cmd\",\"slug\":\"cmd\",\"files\":[\"cmd/auth-gate/main.go\",\"cmd/psa/main.go\",\"cmd/qb-push-items/main.go\"]},{\"name\":\"docs\",\"slug\":\"docs\",\"files\":[],\"children\":[{\"name\":\"docs \u2014 docs\",\"slug\":\"docs-docs\",\"files\":[\"docs/2026-04-07-Meeting-Email-Security-Flow.md\",\"docs/API_REFERENCE.md\",\"docs/ARCHITECTURE.md\",\"docs/AUDIT_CRM_TENANT_SCOPE.md\",\"docs/BASE_HTML_MIGRATION_PLAN.md\",\"docs/BID_ENGINE_TEST_GUIDE.md\",\"docs/CMMC_ONE_CLICK_COMPLIANCE_PLAN.md\",\"docs/CMMC_ONE_CLICK_TEST_GUIDE.md\",\"docs/CODEBASE_OBSERVATIONS.md\",\"docs/COMPLIANCE_COMPOSER_PLAN.md\",\"docs/DEPENDENCIES-EXTERNAL.md\",\"docs/DEPENDENCIES.md\",\"docs/DISTRIBUTOR_PHASE3_PROCUREMENT_PLAN.md\",\"docs/EMAIL_SECURITY_TRIAGE_PLAN.md\",\"docs/INFRASTRUCTURE.md\",\"docs/INVENTORY.md\",\"docs/LIVE_REVIEW_ENGINE_MANUAL.md\",\"docs/NCSR_INTEGRATION_PLAN.md\",\"docs/NexusOS_Orchestrator_Architecture.md\",\"docs/NexusOS_Sales_Lifecycle_Automation_Plan.md\",\"docs/NexusOS_Sales_Lifecycle_Test_Plan.md\",\"docs/NexusRMM-Status-Update-Jon.md\",\"docs/ONBOARDING.md\",\"docs/Orchestrator_Hex_Detail_UI_Artifact.md\",\"docs/PRODUCTS_SERVICES_CATALOG.md\",\"docs/PROJECT_MANAGEMENT.md\",\"docs/PR_A_Helpdesk-Peek.md\",\"docs/PR_B_Helpdesk-Hub.md\",\"docs/Phase9_Action_Configuration_Plan.md\",\"docs/RISK_ACCEPTANCE_DISPOSITION_GUIDE.md\",\"docs/ROADMAP.md\",\"docs/gen_ach_user_guide.py\",\"docs/gen_ach_whitepaper.py\",\"docs/gen_onboarding_guide.py\",\"docs/gen_onboarding_user_guide.py\",\"docs/gen_schema_poster.py\",\"docs/lifecycle-test-manual.md\",\"docs/orchestrator_ui_enhancement_artifact.md\",\"docs/teleport-deployment-guide.md\",\"docs/tickets-er-diagram.html\",\"docs/work-order-manual.md\"]},{\"name\":\"docs \u2014 architecture\",\"slug\":\"docs-architecture\",\"files\":[\"docs/architecture/ARCHITECTURE.md\",\"docs/architecture/PROJECT_STATUS.md\",\"docs/architecture/REMOTE_DESKTOP_SPEC.md\"]},{\"name\":\"docs \u2014 guide\",\"slug\":\"docs-guide\",\"files\":[\"docs/guide/MULTI_REVIEWER_TEST_GUIDE.md\",\"docs/guide/NCSR_USER_GUIDE.md\",\"docs/guide/PHASE3_PROCUREMENT_TESTING_GUIDE.md\",\"docs/guide/README.md\",\"docs/guide/RISK_ACCEPTANCE_USER_GUIDE.md\",\"docs/guide/billing-contracts.md\",\"docs/guide/dispatch.md\",\"docs/guide/employee-hr.md\",\"docs/guide/projects.md\"]},{\"name\":\"docs \u2014 integrations\",\"slug\":\"docs-integrations\",\"files\":[\"docs/integrations/BACKLOG.md\"]},{\"name\":\"docs \u2014 mockups\",\"slug\":\"docs-mockups\",\"files\":[\"docs/mockups/brief_mockup_v2.html\",\"docs/mockups/live_review_generalization.html\",\"docs/mockups/phase3/_shared.css\",\"docs/mockups/phase3/agreement-saas.html\",\"docs/mockups/phase3/contract-materials.html\",\"docs/mockups/phase3/index.html\",\"docs/mockups/phase3/procurement-detail.html\",\"docs/mockups/phase3/procurement-list.html\",\"docs/mockups/phase3/project-po-integration.html\",\"docs/mockups/phase3/remediation-queue.html\",\"docs/mockups/phase3/settings-distributors.html\",\"docs/mockups/phase3/ticket-po-integration.html\",\"docs/mockups/review_host_completion.html\",\"docs/mockups/review_ipad_completion.html\"]},{\"name\":\"docs \u2014 modules\",\"slug\":\"docs-modules\",\"files\":[\"docs/modules/auth/API.md\",\"docs/modules/auth/MANUAL.md\",\"docs/modules/auth/SOURCE_MAP.md\",\"docs/modules/auth/TODO.md\",\"docs/modules/auth/WHITEPAPER.md\",\"docs/modules/billing/API.md\",\"docs/modules/billing/MANUAL.md\",\"docs/modules/billing/SOURCE_MAP.md\",\"docs/modules/billing/TODO.md\",\"docs/modules/billing/WHITEPAPER.md\",\"docs/modules/certificates/API.md\",\"docs/modules/certificates/MANUAL.md\",\"docs/modules/certificates/SOURCE_MAP.md\",\"docs/modules/certificates/TODO.md\",\"docs/modules/certificates/WHITEPAPER.md\",\"docs/modules/compliance/API.md\",\"docs/modules/compliance/MANUAL.md\",\"docs/modules/compliance/SOURCE_MAP.md\",\"docs/modules/compliance/TODO.md\",\"docs/modules/compliance/WHITEPAPER.md\",\"docs/modules/crm/API.md\",\"docs/modules/crm/MANUAL.md\",\"docs/modules/crm/SOURCE_MAP.md\",\"docs/modules/crm/TODO.md\",\"docs/modules/crm/WHITEPAPER.md\",\"docs/modules/cybersec/API.md\",\"docs/modules/cybersec/MANUAL.md\",\"docs/modules/cybersec/SOURCE_MAP.md\",\"docs/modules/cybersec/TODO.md\",\"docs/modules/cybersec/WHITEPAPER.md\",\"docs/modules/devices/API.md\",\"docs/modules/devices/MANUAL.md\",\"docs/modules/devices/SOURCE_MAP.md\",\"docs/modules/devices/TODO.md\",\"docs/modules/devices/WHITEPAPER.md\",\"docs/modules/email-security/API.md\",\"docs/modules/email-security/MANUAL.md\",\"docs/modules/email-security/SOURCE_MAP.md\",\"docs/modules/email-security/TODO.md\",\"docs/modules/email-security/WHITEPAPER.md\",\"docs/modules/health/API.md\",\"docs/modules/health/MANUAL.md\",\"docs/modules/health/SOURCE_MAP.md\",\"docs/modules/health/TODO.md\",\"docs/modules/health/WHITEPAPER.md\",\"docs/modules/helpdesk/API.md\",\"docs/modules/helpdesk/MANUAL.md\",\"docs/modules/helpdesk/SOURCE_MAP.md\",\"docs/modules/helpdesk/TODO.md\",\"docs/modules/helpdesk/WHITEPAPER.md\",\"docs/modules/hudu/API.md\",\"docs/modules/hudu/MANUAL.md\",\"docs/modules/hudu/SOURCE_MAP.md\",\"docs/modules/hudu/TODO.md\",\"docs/modules/hudu/WHITEPAPER.md\",\"docs/modules/knowledge-base/API.md\",\"docs/modules/knowledge-base/MANUAL.md\",\"docs/modules/knowledge-base/SOURCE_MAP.md\",\"docs/modules/knowledge-base/TODO.md\",\"docs/modules/knowledge-base/WHITEPAPER.md\",\"docs/modules/legal/API.md\",\"docs/modules/legal/MANUAL.md\",\"docs/modules/legal/SOURCE_MAP.md\",\"docs/modules/legal/TODO.md\",\"docs/modules/legal/WHITEPAPER.md\",\"docs/modules/metasploit/API.md\",\"docs/modules/metasploit/MANUAL.md\",\"docs/modules/metasploit/SOURCE_MAP.md\",\"docs/modules/metasploit/TODO.md\",\"docs/modules/metasploit/WHITEPAPER.md\",\"docs/modules/mobile/API.md\",\"docs/modules/mobile/MANUAL.md\",\"docs/modules/mobile/SOURCE_MAP.md\",\"docs/modules/mobile/TODO.md\",\"docs/modules/mobile/WHITEPAPER.md\",\"docs/modules/nexie-ai/API.md\",\"docs/modules/nexie-ai/MANUAL.md\",\"docs/modules/nexie-ai/SOURCE_MAP.md\",\"docs/modules/nexie-ai/TODO.md\",\"docs/modules/nexie-ai/WHITEPAPER.md\",\"docs/modules/onboarding/API.md\",\"docs/modules/onboarding/MANUAL.md\",\"docs/modules/onboarding/SOURCE_MAP.md\",\"docs/modules/onboarding/TODO.md\",\"docs/modules/onboarding/WHITEPAPER.md\",\"docs/modules/products/API.md\",\"docs/modules/products/MANUAL.md\",\"docs/modules/products/SOURCE_MAP.md\",\"docs/modules/products/TODO.md\",\"docs/modules/products/WHITEPAPER.md\",\"docs/modules/projects/API.md\",\"docs/modules/projects/MANUAL.md\",\"docs/modules/projects/SOURCE_MAP.md\",\"docs/modules/projects/TODO.md\",\"docs/modules/projects/WHITEPAPER.md\",\"docs/modules/purchase-orders/API.md\",\"docs/modules/purchase-orders/MANUAL.md\",\"docs/modules/purchase-orders/SOURCE_MAP.md\",\"docs/modules/purchase-orders/TODO.md\",\"docs/modules/purchase-orders/WHITEPAPER.md\",\"docs/modules/push-notifications/API.md\",\"docs/modules/push-notifications/MANUAL.md\",\"docs/modules/push-notifications/SOURCE_MAP.md\",\"docs/modules/push-notifications/TODO.md\",\"docs/modules/push-notifications/WHITEPAPER.md\",\"docs/modules/quickbooks/API.md\",\"docs/modules/quickbooks/MANUAL.md\",\"docs/modules/quickbooks/SOURCE_MAP.md\",\"docs/modules/quickbooks/TODO.md\",\"docs/modules/quickbooks/WHITEPAPER.md\",\"docs/modules/rmm/AGENT_AUDIT.md\",\"docs/modules/rmm/AGENT_REQUIREMENTS.md\",\"docs/modules/rmm/API.md\",\"docs/modules/rmm/MANUAL.md\",\"docs/modules/rmm/SOURCE_MAP.md\",\"docs/modules/rmm/TODO.md\",\"docs/modules/rmm/WHITEPAPER.md\",\"docs/modules/settings/API.md\",\"docs/modules/settings/MANUAL.md\",\"docs/modules/settings/SOURCE_MAP.md\",\"docs/modules/settings/TODO.md\",\"docs/modules/settings/WHITEPAPER.md\",\"docs/modules/siem/API.md\",\"docs/modules/siem/MANUAL.md\",\"docs/modules/siem/SOURCE_MAP.md\",\"docs/modules/siem/TODO.md\",\"docs/modules/siem/WHITEPAPER.md\",\"docs/modules/timer/API.md\",\"docs/modules/timer/MANUAL.md\",\"docs/modules/timer/SOURCE_MAP.md\",\"docs/modules/timer/TODO.md\",\"docs/modules/timer/WHITEPAPER.md\",\"docs/modules/voip/API.md\",\"docs/modules/voip/MANUAL.md\",\"docs/modules/voip/SOURCE_MAP.md\",\"docs/modules/voip/TODO.md\",\"docs/modules/voip/WHITEPAPER.md\"]},{\"name\":\"docs \u2014 security\",\"slug\":\"docs-security\",\"files\":[\"docs/security/EVIDENCE.md\",\"docs/security/audit-catalog.md\",\"docs/security/controls/soc2-cc6.1-logical-access.md\",\"docs/security/crypto-inventory.md\",\"docs/security/index.html\",\"docs/security/network-surface.md\",\"docs/security/threats/rmm.md\"]},{\"name\":\"docs \u2014 ui-samples\",\"slug\":\"docs-ui-samples\",\"files\":[\"docs/ui-samples/action_config_panels_mockup.html\",\"docs/ui-samples/hex-frosted-matte-sample.html\",\"docs/ui-samples/hex-frosted-white-ceramic-sample.html\",\"docs/ui-samples/hex-glass-buttons-sample.html\",\"docs/ui-samples/hex-glass-light-mode-sample.html\",\"docs/ui-samples/hex-glass-theme-sample.html\",\"docs/ui-samples/orchestrator_module_detail_mockup.html\"]}]},{\"name\":\"extensions\",\"slug\":\"extensions\",\"files\":[\"extensions/chrome/background.js\",\"extensions/chrome/content.js\",\"extensions/chrome/manifest.json\",\"extensions/chrome/popup.html\",\"extensions/chrome/popup.js\"]},{\"name\":\"internal\",\"slug\":\"internal\",\"files\":[],\"children\":[{\"name\":\"internal \u2014 ai\",\"slug\":\"internal-ai\",\"files\":[\"internal/ai/claude.go\",\"internal/ai/redact.go\"]},{\"name\":\"internal \u2014 assessment\",\"slug\":\"internal-assessment\",\"files\":[\"internal/assessment/analyzer.go\",\"internal/assessment/approval_gate.go\",\"internal/assessment/blueprint_builder.go\",\"internal/assessment/document_processor.go\",\"internal/assessment/handler.go\",\"internal/assessment/quote_converter.go\",\"internal/assessment/scheduled_ops.go\",\"internal/assessment/types.go\"]},{\"name\":\"internal \u2014 auth\",\"slug\":\"internal-auth\",\"files\":[\"internal/auth/handler.go\",\"internal/auth/jwt.go\",\"internal/auth/mfa.go\",\"internal/auth/middleware.go\",\"internal/auth/password.go\",\"internal/auth/session.go\",\"internal/auth/sso.go\"]},{\"name\":\"internal \u2014 bidengine\",\"slug\":\"internal-bidengine\",\"files\":[\"internal/bidengine/addendum_apply.go\",\"internal/bidengine/addendum_split.go\",\"internal/bidengine/audit.go\",\"internal/bidengine/audit_handler.go\",\"internal/bidengine/audit_resolve.go\",\"internal/bidengine/audit_view.go\",\"internal/bidengine/drawing_discipline.go\",\"internal/bidengine/engine.go\",\"internal/bidengine/engine_test.go\",\"internal/bidengine/exclusion_extractor.go\",\"internal/bidengine/exclusion_extractor_test.go\",\"internal/bidengine/flow.go\",\"internal/bidengine/generate_from_drawings.go\",\"internal/bidengine/generate_from_spec_helpers.go\",\"internal/bidengine/handler.go\",\"internal/bidengine/outcomes.go\",\"internal/bidengine/page_selection.go\",\"internal/bidengine/processing_lock.go\",\"internal/bidengine/reconcile.go\",\"internal/bidengine/scope_summary.go\",\"internal/bidengine/trade_profile.go\",\"internal/bidengine/types.go\",\"internal/bidengine/vision_audit.go\"]},{\"name\":\"internal \u2014 billing\",\"slug\":\"internal-billing\",\"files\":[\"internal/billing/email.go\",\"internal/billing/invoice_attachments.go\",\"internal/billing/invoice_handler.go\",\"internal/billing/quote_attachments.go\",\"internal/billing/quote_handler.go\",\"internal/billing/transcript_analyzer.go\",\"internal/billing/transcript_handler.go\",\"internal/billing/types.go\"]},{\"name\":\"internal \u2014 certs\",\"slug\":\"internal-certs\",\"files\":[\"internal/certs/ca.go\",\"internal/certs/handler.go\",\"internal/certs/store.go\"]},{\"name\":\"internal \u2014 cmmc\",\"slug\":\"internal-cmmc\",\"files\":[\"internal/cmmc/binder_render.go\",\"internal/cmmc/connect.go\",\"internal/cmmc/disposition.go\",\"internal/cmmc/handler.go\",\"internal/cmmc/nexie.go\",\"internal/cmmc/policy_render.go\",\"internal/cmmc/risk_resolver.go\",\"internal/cmmc/ssp_render.go\",\"internal/cmmc/types.go\"]},{\"name\":\"internal \u2014 compliance\",\"slug\":\"internal-compliance\",\"files\":[\"internal/compliance/handler.go\",\"internal/compliance/types.go\"]},{\"name\":\"internal \u2014 composer\",\"slug\":\"internal-composer\",\"files\":[\"internal/composer/disposition.go\",\"internal/composer/framework.go\",\"internal/composer/handler.go\",\"internal/composer/ncsr/action_plan.go\",\"internal/composer/ncsr/crm_compliance.go\",\"internal/composer/ncsr/delete.go\",\"internal/composer/ncsr/handler.go\",\"internal/composer/ncsr/parse_pdf.go\",\"internal/composer/ncsr/parse_pdf_test.go\",\"internal/composer/ncsr/parse_xlsx.go\",\"internal/composer/ncsr/parse_xlsx_test.go\",\"internal/composer/ncsr/post_review.go\",\"internal/composer/ncsr/review_queue.go\",\"internal/composer/ncsr/review_writeback.go\",\"internal/composer/ncsr/reviews_page.go\",\"internal/composer/ncsr/risk_resolver.go\",\"internal/composer/ncsr/seeder.go\",\"internal/composer/ncsr/trajectory.go\",\"internal/composer/ncsr/types.go\",\"internal/composer/nexie.go\",\"internal/composer/reviews_history.go\",\"internal/composer/risk_resolver.go\",\"internal/composer/start_review.go\",\"internal/composer/types.go\"]},{\"name\":\"internal \u2014 config\",\"slug\":\"internal-config\",\"files\":[\"internal/config/config.go\"]},{\"name\":\"internal \u2014 crm\",\"slug\":\"internal-crm\",\"files\":[\"internal/crm/accounts_handler.go\",\"internal/crm/activities_handler.go\",\"internal/crm/campaigns_handler.go\",\"internal/crm/contacts_handler.go\",\"internal/crm/deals_handler.go\",\"internal/crm/email_security_handler.go\",\"internal/crm/form_handlers.go\",\"internal/crm/handler.go\",\"internal/crm/lead_analysis.go\",\"internal/crm/lead_analysis_test.go\",\"internal/crm/pipelines_handler.go\",\"internal/crm/tenant_isolation_test.go\",\"internal/crm/types.go\"]},{\"name\":\"internal \u2014 cybersec\",\"slug\":\"internal-cybersec\",\"files\":[\"internal/cybersec/handler.go\",\"internal/cybersec/types.go\"]},{\"name\":\"internal \u2014 database\",\"slug\":\"internal-database\",\"files\":[\"internal/database/database.go\",\"internal/database/migrate.go\",\"internal/database/migrations/001_extensions.sql\",\"internal/database/migrations/002_tenants.sql\",\"internal/database/migrations/003_roles_permissions.sql\",\"internal/database/migrations/004_users.sql\",\"internal/database/migrations/005_companies_agents.sql\",\"internal/database/migrations/006_products_compliance.sql\",\"internal/database/migrations/007_sso.sql\",\"internal/database/migrations/008_helpdesk.sql\",\"internal/database/migrations/009_compliance_cybersec_extras.sql\",\"internal/database/migrations/010_projects.sql\",\"internal/database/migrations/011_itil.sql\",\"internal/database/migrations/012_quoting_invoicing.sql\",\"internal/database/migrations/013_crm.sql\",\"internal/database/migrations/014_audit_revisions.sql\",\"internal/database/migrations/015_quickbooks_integration.sql\",\"internal/database/migrations/016_quote_require_po.sql\",\"internal/database/migrations/017_certs_mcp_email.sql\",\"internal/database/migrations/018_user_favorites.sql\",\"internal/database/migrations/019_compliance_engine.sql\",\"internal/database/migrations/020_compliance_full_controls.sql\",\"internal/database/migrations/021_compliance_doc_import.sql\",\"internal/database/migrations/022_hipaa_full_controls.sql\",\"internal/database/migrations/023_pci_dss_full_controls.sql\",\"internal/database/migrations/024_soc2_full_controls.sql\",\"internal/database/migrations/025_cmmc_full_controls.sql\",\"internal/database/migrations/026_nist_800_53_full_controls.sql\",\"internal/database/migrations/027_ai_usage_log.sql\",\"internal/database/migrations/027b_compliance_framework_source_cols.sql\",\"internal/database/migrations/028_iso27001.sql\",\"internal/database/migrations/029_nist_csf.sql\",\"internal/database/migrations/030_nist_800_171.sql\",\"internal/database/migrations/031_fedramp.sql\",\"internal/database/migrations/032_iso27701.sql\",\"internal/database/migrations/033_hitrust.sql\",\"internal/database/migrations/034_ffiec.sql\",\"internal/database/migrations/035_stateramp.sql\",\"internal/database/migrations/036_cjis.sql\",\"internal/database/migrations/037_crm_client_enhancements.sql\",\"internal/database/migrations/038_communication_channels.sql\",\"internal/database/migrations/039_country_field.sql\",\"internal/database/migrations/040_rmm_merge.sql\",\"internal/database/migrations/041_legal_documents_and_contracts.sql\",\"internal/database/migrations/042_contract_service_types.sql\",\"internal/database/migrations/043_contract_sync.sql\",\"internal/database/migrations/044_teleport_integration.sql\",\"internal/database/migrations/045_agent_polling_teleport_token.sql\",\"internal/database/migrations/046_metasploit_integration.sql\",\"internal/database/migrations/047_client_vulnerability_config.sql\",\"internal/database/migrations/048_documentation_provider.sql\",\"internal/database/migrations/049_ai_auto_resolve.sql\",\"internal/database/migrations/050_siem.sql\",\"internal/database/migrations/051_billing_work_types_and_roles.sql\",\"internal/database/migrations/052_contract_categories.sql\",\"internal/database/migrations/053_contract_rate_overrides.sql\",\"internal/database/migrations/054_esign_multi_signature.sql\",\"internal/database/migrations/055_product_legal_documents.sql\",\"internal/database/migrations/056_quote_to_contract.sql\",\"internal/database/migrations/057_quote_attachments.sql\",\"internal/database/migrations/058_siem_capture_sessions.sql\",\"internal/database/migrations/059_siem_log_retention.sql\",\"internal/database/migrations/060_certificate_store.sql\",\"internal/database/migrations/061_device_credentials.sql\",\"internal/database/migrations/062_siem_remediation.sql\",\"internal/database/migrations/063_login_audit.sql\",\"internal/database/migrations/064_invitations.sql\",\"internal/database/migrations/065_rmm_enroll_token.sql\",\"internal/database/migrations/066_persistent_timers.sql\",\"internal/database/migrations/067_security_audit_log.sql\",\"internal/database/migrations/068_multi_timer.sql\",\"internal/database/migrations/069_ticket_contract_link.sql\",\"internal/database/migrations/070_company_escalation_policy.sql\",\"internal/database/migrations/071_quote_sla_escalation_policies.sql\",\"internal/database/migrations/072_nexie_time_entries.sql\",\"internal/database/migrations/073_ticket_config.sql\",\"internal/database/migrations/074_voip_integration.sql\",\"internal/database/migrations/075_quote_sequencing.sql\",\"internal/database/migrations/076_quote_sections.sql\",\"internal/database/migrations/077_fix_contract_policy_columns.sql\",\"internal/database/migrations/078_project_management_v2.sql\",\"internal/database/migrations/079_hudu_sync_status.sql\",\"internal/database/migrations/080_health_components.sql\",\"internal/database/migrations/082_ticket_charges.sql\",\"internal/database/migrations/083_time_entry_product.sql\",\"internal/database/migrations/084_purchase_orders.sql\",\"internal/database/migrations/085_po_approvers.sql\",\"internal/database/migrations/086_vendors.sql\",\"internal/database/migrations/087_cr_esign.sql\",\"internal/database/migrations/088_po_approval_signature.sql\",\"internal/database/migrations/089_web_push.sql\",\"internal/database/migrations/090_project_site_location.sql\",\"internal/database/migrations/091_orchestrator.sql\",\"internal/database/migrations/092_workflow_engine_v2.sql\",\"internal/database/migrations/093_rmm_session_tokens.sql\",\"internal/database/migrations/094_rmm_enrollment_tokens.sql\",\"internal/database/migrations/095_email_send_log.sql\",\"internal/database/migrations/096_client_security_profile.sql\",\"internal/database/migrations/097_client_onboarding.sql\",\"internal/database/migrations/098_ach_payment_method.sql\",\"internal/database/migrations/098_device_cert_columns.sql\",\"internal/database/migrations/099_orchestrator_packages.sql\",\"internal/database/migrations/099_sla_pause_support.sql\",\"internal/database/migrations/100_note_edit_tracking.sql\",\"internal/database/migrations/101_multi_pipeline.sql\",\"internal/database/migrations/102_invoice_attachments.sql\",\"internal/database/migrations/103_recurring_invoice_schedule.sql\",\"internal/database/migrations/104_timesheets.sql\",\"internal/database/migrations/105_qb_employees.sql\",\"internal/database/migrations/106_timesheet_permissions.sql\",\"internal/database/migrations/107_portal.sql\",\"internal/database/migrations/108_portal_permissions.sql\",\"internal/database/migrations/109_contacts_country.sql\",\"internal/database/migrations/110_kb_visibility.sql\",\"internal/database/migrations/111_invoice_qb_link.sql\",\"internal/database/migrations/112_ai_usage_portal.sql\",\"internal/database/migrations/113_portal_roles.sql\",\"internal/database/migrations/114_portal_role_labels.sql\",\"internal/database/migrations/115_user_ui_theme.sql\",\"internal/database/migrations/116_user_ui_preferences.sql\",\"internal/database/migrations/117_industry_intelligence.sql\",\"internal/database/migrations/118_assessments.sql\",\"internal/database/migrations/119_orchestrator_events.sql\",\"internal/database/migrations/120_notification_actions.sql\",\"internal/database/migrations/121_scheduled_operations.sql\",\"internal/database/migrations/122_pipeline_presets.sql\",\"internal/database/migrations/123_industry_business_types.sql\",\"internal/database/migrations/124_compliance_frameworks_sync.sql\",\"internal/database/migrations/125_epa.sql\",\"internal/database/migrations/126_epa_client_contact_asset.sql\",\"internal/database/migrations/127_agent_releases.sql\",\"internal/database/migrations/128_agent_releases_v11_seed.sql\",\"internal/database/migrations/129_agent_releases_go_v110.sql\",\"internal/database/migrations/129_custom_actions.sql\",\"internal/database/migrations/131_agent_releases_go_v111.sql\",\"internal/database/migrations/132_contacts_nexie_epa_enabled.sql\",\"internal/database/migrations/133_nexuspulse.sql\",\"internal/database/migrations/134_nexuspulse_reports.sql\",\"internal/database/migrations/135_nexuspulse_rebrand_codes.sql\",\"internal/database/migrations/136_product_qbo_accounts.sql\",\"internal/database/migrations/137_product_cost.sql\",\"internal/database/migrations/138_product_subcontractor.sql\",\"internal/database/migrations/139_product_image.sql\",\"internal/database/migrations/140_product_sku.sql\",\"internal/database/migrations/141_product_supplier.sql\",\"internal/database/migrations/142_bid_engine.sql\",\"internal/database/migrations/143_bid_spec_compliance.sql\",\"internal/database/migrations/144_product_labor_hours.sql\",\"internal/database/migrations/145_bid_card_types.sql\",\"internal/database/migrations/146_bid_revisions.sql\",\"internal/database/migrations/147_bid_sections.sql\",\"internal/database/migrations/148_bid_trade_scope.sql\",\"internal/database/migrations/149_section_scope_link.sql\",\"internal/database/migrations/150_bid_address_fields.sql\",\"internal/database/migrations/151_spec_doc_ai_cost.sql\",\"internal/database/migrations/152_site_surveys.sql\",\"internal/database/migrations/153_crm_company_type.sql\",\"internal/database/migrations/154_survey_checklist_seeds.sql\",\"internal/database/migrations/155_orchestrator_survey_bid_presets.sql\",\"internal/database/migrations/156_timer_survey_support.sql\",\"internal/database/migrations/157_survey_photo_analysis.sql\",\"internal/database/migrations/158_orchestrator_photo_analysis_preset.sql\",\"internal/database/migrations/159_survey_poc_contact.sql\",\"internal/database/migrations/160_survey_audio.sql\",\"internal/database/migrations/161_drawings.sql\",\"internal/database/migrations/162_photo_thumbnails.sql\",\"internal/database/migrations/163_assessment_work_type.sql\",\"internal/database/migrations/164_work_orders.sql\",\"internal/database/migrations/165_full_lifecycle_presets.sql\",\"internal/database/migrations/166_dispatch.sql\",\"internal/database/migrations/167_employee.sql\",\"internal/database/migrations/168_seed_employees.sql\",\"internal/database/migrations/169_recorder_sessions_segments.sql\",\"internal/database/migrations/170_role_rates.sql\",\"internal/database/migrations/171_time_entry_costs.sql\",\"internal/database/migrations/172_role_rates_billing_link.sql\",\"internal/database/migrations/173_cmmc_l2_module.sql\",\"internal/database/migrations/174_cmmc_company_branding.sql\",\"internal/database/migrations/175_recorder_consent.sql\",\"internal/database/migrations/176_infra_devices.sql\",\"internal/database/migrations/177_agents_compliance_data.sql\",\"internal/database/migrations/178_cmmc_ingested_docs.sql\",\"internal/database/migrations/179_compliance_composer.sql\",\"internal/database/migrations/180_sentinel_devices.sql\",\"internal/database/migrations/181_sentinel_schema_isolation.sql\",\"internal/database/migrations/182_sentinel_audit.sql\",\"internal/database/migrations/183_agents_sentinel_hmac_key.sql\",\"internal/database/migrations/183_risk_disposition.sql\",\"internal/database/migrations/184_ncsr_assessments.sql\",\"internal/database/migrations/185_recorder_consent_cancelled.sql\",\"internal/database/migrations/185_risk_acceptances.sql\",\"internal/database/migrations/186_live_review_engine.sql\",\"internal/database/migrations/187_review_token_revocation.sql\",\"internal/database/migrations/188_ncsr_plan_jobs.sql\",\"internal/database/migrations/189_review_decision_writeback.sql\",\"internal/database/migrations/189_tickets_pinned.sql\",\"internal/database/migrations/190_backfill_action_decline_from_area.sql\",\"internal/database/migrations/190_tickets_source_enum.sql\",\"internal/database/migrations/191_backfill_action_decision_from_area.sql\",\"internal/database/migrations/192_brief_v2_fields.sql\",\"internal/database/migrations/193_ticket_followers.sql\",\"internal/database/migrations/194_review_multi_reviewer.sql\",\"internal/database/migrations/195_mcp_oauth_columns.sql\",\"internal/database/migrations/196_distributor_ingest.sql\",\"internal/database/migrations/196_helpdesk_config_tables.sql\",\"internal/database/migrations/197_tickets_fk_indexes.sql\",\"internal/database/migrations/200_contract_lock_state.sql\",\"internal/database/migrations/201_contract_line_exclusions.sql\",\"internal/database/migrations/202_contract_change_requests.sql\",\"internal/database/migrations/203_contract_line_audit.sql\",\"internal/database/migrations/204_nexiq_foundation.sql\",\"internal/database/migrations/205_pbr_command_control_fields.sql\",\"internal/database/migrations/206_bid_drawing_reconciliation.sql\",\"internal/database/migrations/207_bid_mode.sql\",\"internal/database/migrations/208_quote_totals_trigger.sql\",\"internal/database/migrations/209_invoice_totals_trigger.sql\",\"internal/database/migrations/210_lifecycle_events.sql\",\"internal/database/migrations/210_quote_line_client_selection.sql\",\"internal/database/migrations/211_quote_totals_effective.sql\",\"internal/database/migrations/212_phase3_purchase_orders_procurement.sql\",\"internal/database/migrations/213_po_remediation_queue.sql\",\"internal/database/migrations/214_vendor_distributor_auto_dispatch.sql\",\"internal/database/migrations/215_purchase_order_audit.sql\",\"internal/database/migrations/216_contract_line_procurement.sql\",\"internal/database/migrations/217_deprecate_project_purchase_orders.sql\",\"internal/database/migrations/218_bid_ai_scope_summary.sql\",\"internal/database/migrations/219_bid_nexie_last_error.sql\",\"internal/database/migrations/220_drawings_page_selection.sql\",\"internal/database/migrations/221_peek_summary.sql\",\"internal/database/migrations/222_bid_line_part_number.sql\",\"internal/database/migrations/223_bid_addendum_changes.sql\",\"internal/database/migrations/224_drawing_supersession.sql\",\"internal/database/migrations/225_addendum_diff_status.sql\",\"internal/database/migrations/226_addendum_audit_trail.sql\",\"internal/database/migrations/227_addendum_decision_audit.sql\",\"internal/database/migrations/228_addendum_scope_exclusion.sql\",\"internal/database/migrations/229_backfill_orphan_recon_line_items.sql\",\"internal/database/migrations/230_bid_line_scope_flags.sql\",\"internal/database/migrations/231_bid_trade_profiles.sql\",\"internal/database/migrations/232_bid_outcomes.sql\",\"internal/database/migrations/233_drawings_extracted_text.sql\",\"internal/database/migrations/234_bid_audit_acks.sql\",\"internal/database/migrations/235_tag_tables_tenant_id.sql\",\"internal/database/migrations/236_campaign_recipients_tenant_id.sql\"]},{\"name\":\"internal \u2014 devices\",\"slug\":\"internal-devices\",\"files\":[\"internal/devices/fortigate.go\",\"internal/devices/handler.go\"]},{\"name\":\"internal \u2014 dispatch\",\"slug\":\"internal-dispatch\",\"files\":[\"internal/dispatch/graph_calendar.go\",\"internal/dispatch/handler.go\",\"internal/dispatch/types.go\"]},{\"name\":\"internal \u2014 distributor\",\"slug\":\"internal-distributor\",\"files\":[\"internal/distributor/catalog_query.go\",\"internal/distributor/contract_query.go\",\"internal/distributor/handler.go\",\"internal/distributor/provisioner.go\",\"internal/distributor/scheduler.go\",\"internal/distributor/status.go\",\"internal/distributor/subs_query.go\",\"internal/distributor/sync.go\",\"internal/distributor/types.go\",\"internal/distributor/wrapper.go\"]},{\"name\":\"internal \u2014 drawing\",\"slug\":\"internal-drawing\",\"files\":[\"internal/drawing/handler.go\",\"internal/drawing/isometric.go\",\"internal/drawing/takeoff.go\",\"internal/drawing/types.go\",\"internal/drawing/vision.go\"]},{\"name\":\"internal \u2014 emailsec\",\"slug\":\"internal-emailsec\",\"files\":[\"internal/emailsec/graph.go\",\"internal/emailsec/handler.go\",\"internal/emailsec/headers.go\",\"internal/emailsec/llm_triage.go\",\"internal/emailsec/pipeline.go\",\"internal/emailsec/scoring.go\"]},{\"name\":\"internal \u2014 employee\",\"slug\":\"internal-employee\",\"files\":[\"internal/employee/handler.go\",\"internal/employee/types.go\"]},{\"name\":\"internal \u2014 epa\",\"slug\":\"internal-epa\",\"files\":[\"internal/epa/handler.go\",\"internal/epa/store.go\",\"internal/epa/types.go\"]},{\"name\":\"internal \u2014 financial\",\"slug\":\"internal-financial\",\"files\":[\"internal/financial/provider.go\",\"internal/financial/qbo/adapter.go\",\"internal/financial/qbo/provider.go\",\"internal/financial/types.go\"]},{\"name\":\"internal \u2014 health\",\"slug\":\"internal-health\",\"files\":[\"internal/health/disk_darwin.go\",\"internal/health/disk_linux.go\",\"internal/health/disk_windows.go\",\"internal/health/handler.go\"]},{\"name\":\"internal \u2014 helpdesk\",\"slug\":\"internal-helpdesk\",\"files\":[\"internal/helpdesk/capabilities.go\",\"internal/helpdesk/config_audit.go\",\"internal/helpdesk/config_handler.go\",\"internal/helpdesk/config_seed.go\",\"internal/helpdesk/contract.go\",\"internal/helpdesk/email.go\",\"internal/helpdesk/handler.go\",\"internal/helpdesk/handler_v2.go\",\"internal/helpdesk/orchestrator.go\",\"internal/helpdesk/peek_handler.go\",\"internal/helpdesk/peek_hudu.go\",\"internal/helpdesk/peek_store.go\",\"internal/helpdesk/peek_types.go\",\"internal/helpdesk/recorder_session.go\",\"internal/helpdesk/recordings_handler.go\",\"internal/helpdesk/sla.go\",\"internal/helpdesk/store.go\",\"internal/helpdesk/ticket_detail_helpers.go\",\"internal/helpdesk/tickets_prefs.go\",\"internal/helpdesk/tracking.go\",\"internal/helpdesk/types.go\",\"internal/helpdesk/types_itil.go\",\"internal/helpdesk/workflow.go\"]},{\"name\":\"internal \u2014 hudu\",\"slug\":\"internal-hudu\",\"files\":[\"internal/hudu/client.go\",\"internal/hudu/handler.go\",\"internal/hudu/types.go\"]},{\"name\":\"internal \u2014 industry\",\"slug\":\"internal-industry\",\"files\":[\"internal/industry/handler.go\"]},{\"name\":\"internal \u2014 infra\",\"slug\":\"internal-infra\",\"files\":[\"internal/infra/audit.go\",\"internal/infra/handler.go\",\"internal/infra/hudu.go\",\"internal/infra/runbook.go\",\"internal/infra/ssrf_guard.go\",\"internal/infra/types.go\",\"internal/infra/verify.go\"]},{\"name\":\"internal \u2014 itil\",\"slug\":\"internal-itil\",\"files\":[\"internal/itil/itil_handler.go\",\"internal/itil/itil_store.go\",\"internal/itil/itil_types.go\"]},{\"name\":\"internal \u2014 kb\",\"slug\":\"internal-kb\",\"files\":[\"internal/kb/handler.go\",\"internal/kb/types.go\"]},{\"name\":\"internal \u2014 legal\",\"slug\":\"internal-legal\",\"files\":[\"internal/legal/ach_submit_helpers.go\",\"internal/legal/analyze.go\",\"internal/legal/billing_handler.go\",\"internal/legal/billing_types.go\",\"internal/legal/contract_summary_generator.go\",\"internal/legal/crypto.go\",\"internal/legal/docx_renderer.go\",\"internal/legal/edit_contract_form_helpers.go\",\"internal/legal/executed_package.go\",\"internal/legal/handler.go\",\"internal/legal/msa_generator.go\",\"internal/legal/order_generator.go\",\"internal/legal/quote_conversion.go\",\"internal/legal/sign_contract_helpers.go\",\"internal/legal/signature_stamper.go\",\"internal/legal/signing_page_helpers.go\",\"internal/legal/sync.go\",\"internal/legal/types.go\"]},{\"name\":\"internal \u2014 lifecycle\",\"slug\":\"internal-lifecycle\",\"files\":[\"internal/lifecycle/lifecycle.go\"]},{\"name\":\"internal \u2014 mcpclient\",\"slug\":\"internal-mcpclient\",\"files\":[\"internal/mcpclient/client.go\"]},{\"name\":\"internal \u2014 mcpvendors\",\"slug\":\"internal-mcpvendors\",\"files\":[\"internal/mcpvendors/factory.go\",\"internal/mcpvendors/pax8/pax8.go\",\"internal/mcpvendors/resolver.go\"]},{\"name\":\"internal \u2014 metasploit\",\"slug\":\"internal-metasploit\",\"files\":[\"internal/metasploit/client.go\",\"internal/metasploit/handler.go\",\"internal/metasploit/types.go\"]},{\"name\":\"internal \u2014 middleware\",\"slug\":\"internal-middleware\",\"files\":[\"internal/middleware/csrf.go\",\"internal/middleware/middleware.go\"]},{\"name\":\"internal \u2014 mobile\",\"slug\":\"internal-mobile\",\"files\":[\"internal/mobile/handler.go\"]},{\"name\":\"internal \u2014 nexie\",\"slug\":\"internal-nexie\",\"files\":[\"internal/nexie/handler.go\",\"internal/nexie/query.go\",\"internal/nexie/schema.go\"]},{\"name\":\"internal \u2014 nexiq\",\"slug\":\"internal-nexiq\",\"files\":[\"internal/nexiq/cascade.go\",\"internal/nexiq/cockpit.go\",\"internal/nexiq/events.go\",\"internal/nexiq/events_test.go\",\"internal/nexiq/goals.go\",\"internal/nexiq/gut_check.go\",\"internal/nexiq/handler.go\",\"internal/nexiq/pages.go\",\"internal/nexiq/permissions.go\",\"internal/nexiq/scenarios.go\",\"internal/nexiq/stubs.go\",\"internal/nexiq/workspace.go\"]},{\"name\":\"internal \u2014 notify\",\"slug\":\"internal-notify\",\"files\":[\"internal/notify/service.go\"]},{\"name\":\"internal \u2014 onboarding\",\"slug\":\"internal-onboarding\",\"files\":[\"internal/onboarding/blueprint_injection.go\",\"internal/onboarding/handler.go\",\"internal/onboarding/phases.go\",\"internal/onboarding/types.go\"]},{\"name\":\"internal \u2014 performance\",\"slug\":\"internal-performance\",\"files\":[\"internal/performance/analyzer.go\",\"internal/performance/calculator.go\",\"internal/performance/collector.go\",\"internal/performance/handler.go\",\"internal/performance/qbo_mapper.go\",\"internal/performance/store.go\",\"internal/performance/types.go\"]},{\"name\":\"internal \u2014 po\",\"slug\":\"internal-po\",\"files\":[\"internal/po/handler.go\"]},{\"name\":\"internal \u2014 portal\",\"slug\":\"internal-portal\",\"files\":[\"internal/portal/admin.go\",\"internal/portal/handler.go\",\"internal/portal/middleware.go\",\"internal/portal/nexie.go\",\"internal/portal/nexie_tools.go\",\"internal/portal/pages.go\",\"internal/portal/risk_acceptances.go\",\"internal/portal/store.go\",\"internal/portal/types.go\",\"internal/portal/usage.go\"]},{\"name\":\"internal \u2014 procurement\",\"slug\":\"internal-procurement\",\"files\":[\"internal/procurement/audit.go\",\"internal/procurement/handler.go\",\"internal/procurement/remediation.go\",\"internal/procurement/service.go\",\"internal/procurement/types.go\"]},{\"name\":\"internal \u2014 product\",\"slug\":\"internal-product\",\"files\":[\"internal/product/handler.go\",\"internal/product/project_handler.go\",\"internal/product/types.go\",\"internal/product/types_project.go\"]},{\"name\":\"internal \u2014 project\",\"slug\":\"internal-project\",\"files\":[\"internal/project/costing.go\",\"internal/project/evm.go\",\"internal/project/handler.go\",\"internal/project/safe_columns.go\",\"internal/project/types.go\"]},{\"name\":\"internal \u2014 push\",\"slug\":\"internal-push\",\"files\":[\"internal/push/handler.go\"]},{\"name\":\"internal \u2014 quickbooks\",\"slug\":\"internal-quickbooks\",\"files\":[\"internal/quickbooks/client.go\",\"internal/quickbooks/employees_sync.go\",\"internal/quickbooks/oauth.go\",\"internal/quickbooks/po_adapter.go\",\"internal/quickbooks/push.go\",\"internal/quickbooks/recurring_billing.go\",\"internal/quickbooks/service.go\",\"internal/quickbooks/sync.go\",\"internal/quickbooks/timesheet_push.go\",\"internal/quickbooks/types.go\"]},{\"name\":\"internal \u2014 rbac\",\"slug\":\"internal-rbac\",\"files\":[\"internal/rbac/roles.go\"]},{\"name\":\"internal \u2014 review\",\"slug\":\"internal-review\",\"files\":[\"internal/review/binder.go\",\"internal/review/broker.go\",\"internal/review/handler.go\",\"internal/review/qr.go\",\"internal/review/repository.go\",\"internal/review/security.go\",\"internal/review/seeder.go\",\"internal/review/types.go\"]},{\"name\":\"internal \u2014 risk\",\"slug\":\"internal-risk\",\"files\":[\"internal/risk/handler.go\",\"internal/risk/print.go\",\"internal/risk/registry.go\",\"internal/risk/service.go\",\"internal/risk/types.go\"]},{\"name\":\"internal \u2014 rmm\",\"slug\":\"internal-rmm\",\"files\":[\"internal/rmm/agent_api.go\",\"internal/rmm/agent_update.go\",\"internal/rmm/crypto/tls.go\",\"internal/rmm/device_api.go\",\"internal/rmm/enroll.go\",\"internal/rmm/handler.go\",\"internal/rmm/install.go\",\"internal/rmm/installers/linux.sh\",\"internal/rmm/installers/macos.sh\",\"internal/rmm/installers/nexusos-agent.wxs.tmpl\",\"internal/rmm/installers/windows.ps1\",\"internal/rmm/msi.go\",\"internal/rmm/protocol/compliance.go\",\"internal/rmm/protocol/inventory.go\",\"internal/rmm/protocol/types.go\",\"internal/rmm/recorder.go\",\"internal/rmm/recordings.go\",\"internal/rmm/remote/desktop.go\",\"internal/rmm/remote/session.go\",\"internal/rmm/remote/tunnel.go\",\"internal/rmm/sentinel.go\",\"internal/rmm/sentinel_socks_hub.go\",\"internal/rmm/session_token.go\",\"internal/rmm/types.go\",\"internal/rmm/update_api.go\"]},{\"name\":\"internal \u2014 sentinel\",\"slug\":\"internal-sentinel\",\"files\":[\"internal/sentinel/capability.go\",\"internal/sentinel/secrets.go\",\"internal/sentinel/types.go\"]},{\"name\":\"internal \u2014 settings\",\"slug\":\"internal-settings\",\"files\":[\"internal/settings/appearance_handler.go\",\"internal/settings/tab_order_handler.go\",\"internal/settings/tab_visibility_handler.go\"]},{\"name\":\"internal \u2014 siem\",\"slug\":\"internal-siem\",\"files\":[\"internal/siem/handler.go\",\"internal/siem/pcap.go\",\"internal/siem/syslog.go\",\"internal/siem/types.go\"]},{\"name\":\"internal \u2014 survey\",\"slug\":\"internal-survey\",\"files\":[\"internal/survey/audio.go\",\"internal/survey/handler.go\",\"internal/survey/types.go\",\"internal/survey/vision.go\",\"internal/survey/voice.go\"]},{\"name\":\"internal \u2014 tenant\",\"slug\":\"internal-tenant\",\"files\":[\"internal/tenant/handler.go\",\"internal/tenant/mcp_oauth.go\"]},{\"name\":\"internal \u2014 timer\",\"slug\":\"internal-timer\",\"files\":[\"internal/timer/billing_context_helpers.go\",\"internal/timer/escalate_helpers.go\",\"internal/timer/handler.go\",\"internal/timer/list_timers_helpers.go\",\"internal/timer/stop_helpers.go\"]},{\"name\":\"internal \u2014 timesheet\",\"slug\":\"internal-timesheet\",\"files\":[\"internal/timesheet/handler.go\",\"internal/timesheet/store.go\",\"internal/timesheet/types.go\"]},{\"name\":\"internal \u2014 ui\",\"slug\":\"internal-ui\",\"files\":[\"internal/ui/preferences.go\",\"internal/ui/render.go\",\"internal/ui/render_v2_test.go\",\"internal/ui/static/css/epa.css\",\"internal/ui/static/css/helpdesk-glass.css\",\"internal/ui/static/css/helpdesk-peek.css\",\"internal/ui/static/css/nexus.css\",\"internal/ui/static/css/theme-mixer.css\",\"internal/ui/static/icons/placeholder.txt\",\"internal/ui/static/js/app.js\",\"internal/ui/static/js/csrf.js\",\"internal/ui/static/js/sigpad.js\",\"internal/ui/static/js/sw.js\",\"internal/ui/static/manifest-mobile.json\",\"internal/ui/static/manifest.json\",\"internal/ui/templates/assessment/detail.html\",\"internal/ui/templates/assessment/form.html\",\"internal/ui/templates/assessment/list.html\",\"internal/ui/templates/auth/invite.html\",\"internal/ui/templates/auth/login.html\",\"internal/ui/templates/bidding/audit.html\",\"internal/ui/templates/bidding/bid_form.html\",\"internal/ui/templates/bidding/calibration.html\",\"internal/ui/templates/bidding/cost_engine.html\",\"internal/ui/templates/bidding/detail.html\",\"internal/ui/templates/bidding/drawing_selection.html\",\"internal/ui/templates/bidding/list.html\",\"internal/ui/templates/bidding/print_detail.html\",\"internal/ui/templates/bidding/print_shadow.html\",\"internal/ui/templates/bidding/settings.html\",\"internal/ui/templates/bidding/takeoff.html\",\"internal/ui/templates/bidding/vision_audit.html\",\"internal/ui/templates/billing/aging.html\",\"internal/ui/templates/billing/invoice_detail.html\",\"internal/ui/templates/billing/invoice_edit.html\",\"internal/ui/templates/billing/invoice_form.html\",\"internal/ui/templates/billing/invoice_print.html\",\"internal/ui/templates/billing/invoices.html\",\"internal/ui/templates/billing/quote_approve.html\",\"internal/ui/templates/billing/quote_detail.html\",\"internal/ui/templates/billing/quote_edit.html\",\"internal/ui/templates/billing/quote_form.html\",\"internal/ui/templates/billing/quote_print.html\",\"internal/ui/templates/billing/quotes.html\",\"internal/ui/templates/billing/transcript_import.html\",\"internal/ui/templates/cmmc/assessments.html\",\"internal/ui/templates/cmmc/connect.html\",\"internal/ui/templates/cmmc/control_detail.html\",\"internal/ui/templates/cmmc/controls.html\",\"internal/ui/templates/cmmc/cui_scope.html\",\"internal/ui/templates/cmmc/dashboard.html\",\"internal/ui/templates/cmmc/dispositions.html\",\"internal/ui/templates/cmmc/evidence_guide.html\",\"internal/ui/templates/cmmc/poam.html\",\"internal/ui/templates/cmmc/select_company.html\",\"internal/ui/templates/cmmc/ssp.html\",\"internal/ui/templates/compliance/approve.html\",\"internal/ui/templates/compliance/client_overview.html\",\"internal/ui/templates/compliance/device_detail.html\",\"internal/ui/templates/compliance/documents.html\",\"internal/ui/templates/compliance/frameworks.html\",\"internal/ui/templates/compliance/gap_analysis.html\",\"internal/ui/templates/composer/client_hub.html\",\"internal/ui/templates/composer/controls.html\",\"internal/ui/templates/composer/dashboard.html\",\"internal/ui/templates/composer/dispositions.html\",\"internal/ui/templates/composer/framework_clients.html\",\"internal/ui/templates/composer/hub.html\",\"internal/ui/templates/composer/live_review_setup.html\",\"internal/ui/templates/composer/ncsr/action_plan.html\",\"internal/ui/templates/composer/ncsr/assessment.html\",\"internal/ui/templates/composer/ncsr/crm_compliance.html\",\"internal/ui/templates/composer/ncsr/landing.html\",\"internal/ui/templates/composer/ncsr/reviews.html\",\"internal/ui/templates/composer/poam.html\",\"internal/ui/templates/composer/reviews_history.html\",\"internal/ui/templates/crm/activities.html\",\"internal/ui/templates/crm/activity_form.html\",\"internal/ui/templates/crm/campaign_detail.html\",\"internal/ui/templates/crm/campaign_form.html\",\"internal/ui/templates/crm/campaigns.html\",\"internal/ui/templates/crm/client_detail.html\",\"internal/ui/templates/crm/client_form.html\",\"internal/ui/templates/crm/clients.html\",\"internal/ui/templates/crm/contact_detail.html\",\"internal/ui/templates/crm/contact_form.html\",\"internal/ui/templates/crm/contacts.html\",\"internal/ui/templates/crm/deal_form.html\",\"internal/ui/templates/crm/pipeline.html\",\"internal/ui/templates/cybersec/dashboard.html\",\"internal/ui/templates/cybersec/scan_detail.html\",\"internal/ui/templates/cybersec/scan_form.html\",\"internal/ui/templates/cybersec/scan_library.html\",\"internal/ui/templates/cybersec/scan_report.html\",\"internal/ui/templates/cybersec/scans.html\",\"internal/ui/templates/cybersec/sessions.html\",\"internal/ui/templates/cybersec/siem.html\",\"internal/ui/templates/dashboard/index.html\",\"internal/ui/templates/dispatch/board.html\",\"internal/ui/templates/dispatch/calendar.html\",\"internal/ui/templates/dispatch/kanban.html\",\"internal/ui/templates/dispatch/list.html\",\"internal/ui/templates/dispatch/map.html\",\"internal/ui/templates/dispatch/settings.html\",\"internal/ui/templates/dispatch/tv.html\",\"internal/ui/templates/dispatch/unassigned.html\",\"internal/ui/templates/distributor/catalog.html\",\"internal/ui/templates/distributor/ccr_modal.html\",\"internal/ui/templates/distributor/contract_section.html\",\"internal/ui/templates/distributor/subs_partial.html\",\"internal/ui/templates/drawing/detail.html\",\"internal/ui/templates/drawing/form.html\",\"internal/ui/templates/drawing/list.html\",\"internal/ui/templates/drawing/view3d.html\",\"internal/ui/templates/employee/detail.html\",\"internal/ui/templates/employee/list.html\",\"internal/ui/templates/epa/company_tab.html\",\"internal/ui/templates/epa/dashboard.html\",\"internal/ui/templates/helpdesk/_v2_asset_console_real.html\",\"internal/ui/templates/helpdesk/_v2_card_drawer.html\",\"internal/ui/templates/helpdesk/_v2_kebab_menu.html\",\"internal/ui/templates/helpdesk/_v2_panel_asset.html\",\"internal/ui/templates/helpdesk/_v2_panel_contact.html\",\"internal/ui/templates/helpdesk/_v2_panel_contract.html\",\"internal/ui/templates/helpdesk/_v2_panel_conversation.html\",\"internal/ui/templates/helpdesk/_v2_panel_customer.html\",\"internal/ui/templates/helpdesk/change_detail.html\",\"internal/ui/templates/helpdesk/change_form.html\",\"internal/ui/templates/helpdesk/cmdb_detail.html\",\"internal/ui/templates/helpdesk/cmdb_form.html\",\"internal/ui/templates/helpdesk/contract_detail.html\",\"internal/ui/templates/helpdesk/contract_form.html\",\"internal/ui/templates/helpdesk/contracts.html\",\"internal/ui/templates/helpdesk/email_templates.html\",\"internal/ui/templates/helpdesk/kanban.html\",\"internal/ui/templates/helpdesk/peek.html\",\"internal/ui/templates/helpdesk/problem_detail.html\",\"internal/ui/templates/helpdesk/problem_form.html\",\"internal/ui/templates/helpdesk/service_form.html\",\"internal/ui/templates/helpdesk/services.html\",\"internal/ui/templates/helpdesk/ticket_detail.html\",\"internal/ui/templates/helpdesk/ticket_detail_v2.html\",\"internal/ui/templates/helpdesk/ticket_form.html\",\"internal/ui/templates/helpdesk/ticket_preview.html\",\"internal/ui/templates/helpdesk/tickets.html\",\"internal/ui/templates/helpdesk/tickets_brief.html\",\"internal/ui/templates/industry/detail.html\",\"internal/ui/templates/industry/list.html\",\"internal/ui/templates/infra/dashboard.html\",\"internal/ui/templates/infra/report.html\",\"internal/ui/templates/infra/select_company.html\",\"internal/ui/templates/itil/changes.html\",\"internal/ui/templates/itil/cmdb.html\",\"internal/ui/templates/itil/problems.html\",\"internal/ui/templates/kb/article.html\",\"internal/ui/templates/kb/article_form.html\",\"internal/ui/templates/kb/articles.html\",\"internal/ui/templates/kb/search.html\",\"internal/ui/templates/layouts/base.html\",\"internal/ui/templates/layouts/mobile.html\",\"internal/ui/templates/legal/contract_detail.html\",\"internal/ui/templates/legal/contract_form.html\",\"internal/ui/templates/legal/contract_sign.html\",\"internal/ui/templates/legal/contracts.html\",\"internal/ui/templates/mobile/agent_detail.html\",\"internal/ui/templates/mobile/agents.html\",\"internal/ui/templates/mobile/alerts.html\",\"internal/ui/templates/mobile/clients.html\",\"internal/ui/templates/mobile/dashboard.html\",\"internal/ui/templates/mobile/invoices.html\",\"internal/ui/templates/mobile/project_detail.html\",\"internal/ui/templates/mobile/projects.html\",\"internal/ui/templates/mobile/purchase_order_new.html\",\"internal/ui/templates/mobile/purchase_orders.html\",\"internal/ui/templates/mobile/security.html\",\"internal/ui/templates/mobile/settings.html\",\"internal/ui/templates/mobile/ticket_detail.html\",\"internal/ui/templates/mobile/ticket_new.html\",\"internal/ui/templates/mobile/tickets.html\",\"internal/ui/templates/mobile/time.html\",\"internal/ui/templates/mobile/timesheet.html\",\"internal/ui/templates/nexiq/gut_check.html\",\"internal/ui/templates/nexiq/index.html\",\"internal/ui/templates/nexiq/lever_lab.html\",\"internal/ui/templates/nexiq/pbr_sheets.html\",\"internal/ui/templates/nexiq/plan_cascade.html\",\"internal/ui/templates/nexiq/sales_cockpit.html\",\"internal/ui/templates/nexiq/sales_workspace.html\",\"internal/ui/templates/notifications/center.html\",\"internal/ui/templates/onboarding/detail.html\",\"internal/ui/templates/onboarding/list.html\",\"internal/ui/templates/onboarding/new.html\",\"internal/ui/templates/partials/brief_body.html\",\"internal/ui/templates/partials/compliance_stepper.html\",\"internal/ui/templates/partials/drag_grip.html\",\"internal/ui/templates/partials/photo_analysis_card.html\",\"internal/ui/templates/partials/portal_shell.html\",\"internal/ui/templates/partials/pulse_nav.html\",\"internal/ui/templates/partials/stats.html\",\"internal/ui/templates/performance/counterbalance.html\",\"internal/ui/templates/performance/dashboard.html\",\"internal/ui/templates/performance/enterprise_value.html\",\"internal/ui/templates/performance/input_editor.html\",\"internal/ui/templates/performance/mrr_context.html\",\"internal/ui/templates/performance/report.html\",\"internal/ui/templates/performance/report_print.html\",\"internal/ui/templates/performance/reports.html\",\"internal/ui/templates/performance/salaries.html\",\"internal/ui/templates/performance/section.html\",\"internal/ui/templates/performance/settings.html\",\"internal/ui/templates/performance/tools.html\",\"internal/ui/templates/performance/trends.html\",\"internal/ui/templates/po/form.html\",\"internal/ui/templates/po/list.html\",\"internal/ui/templates/po/print.html\",\"internal/ui/templates/po/quick.html\",\"internal/ui/templates/po/settings.html\",\"internal/ui/templates/portal/contract_detail.html\",\"internal/ui/templates/portal/contracts.html\",\"internal/ui/templates/portal/forbidden.html\",\"internal/ui/templates/portal/invoice_detail.html\",\"internal/ui/templates/portal/invoices.html\",\"internal/ui/templates/portal/kb.html\",\"internal/ui/templates/portal/kb_article.html\",\"internal/ui/templates/portal/landing.html\",\"internal/ui/templates/portal/login.html\",\"internal/ui/templates/portal/profile.html\",\"internal/ui/templates/portal/quote_detail.html\",\"internal/ui/templates/portal/quotes.html\",\"internal/ui/templates/portal/risk_acceptance_sign.html\",\"internal/ui/templates/portal/risk_acceptances.html\",\"internal/ui/templates/portal/support_consent.html\",\"internal/ui/templates/portal/ticket_detail.html\",\"internal/ui/templates/portal/ticket_new.html\",\"internal/ui/templates/portal/tickets.html\",\"internal/ui/templates/procurement/detail.html\",\"internal/ui/templates/procurement/list.html\",\"internal/ui/templates/procurement/remediation.html\",\"internal/ui/templates/products/assignments.html\",\"internal/ui/templates/products/catalog.html\",\"internal/ui/templates/products/product_detail.html\",\"internal/ui/templates/products/product_form.html\",\"internal/ui/templates/products/product_legal_docs_partial.html\",\"internal/ui/templates/projects/board.html\",\"internal/ui/templates/projects/cr_sign.html\",\"internal/ui/templates/projects/detail.html\",\"internal/ui/templates/projects/list.html\",\"internal/ui/templates/projects/project_form.html\",\"internal/ui/templates/projects/tabs/budget.html\",\"internal/ui/templates/projects/tabs/changes.html\",\"internal/ui/templates/projects/tabs/costs.html\",\"internal/ui/templates/projects/tabs/evm.html\",\"internal/ui/templates/projects/tabs/gantt.html\",\"internal/ui/templates/projects/tabs/lessons.html\",\"internal/ui/templates/projects/tabs/notes.html\",\"internal/ui/templates/projects/tabs/overview.html\",\"internal/ui/templates/projects/tabs/profitability.html\",\"internal/ui/templates/projects/tabs/risks.html\",\"internal/ui/templates/projects/tabs/stakeholders.html\",\"internal/ui/templates/projects/tabs/tasks.html\",\"internal/ui/templates/projects/tabs/team.html\",\"internal/ui/templates/review/binder.html\",\"internal/ui/templates/review/host.html\",\"internal/ui/templates/review/index.html\",\"internal/ui/templates/review/live.html\",\"internal/ui/templates/risk/modal.html\",\"internal/ui/templates/risk/register.html\",\"internal/ui/templates/rmm/agent_detail.html\",\"internal/ui/templates/rmm/agents.html\",\"internal/ui/templates/rmm/dashboard.html\",\"internal/ui/templates/rmm/deploy.html\",\"internal/ui/templates/rmm/desktop.html\",\"internal/ui/templates/rmm/recording_detail.html\",\"internal/ui/templates/rmm/recordings.html\",\"internal/ui/templates/rmm/sentinel.html\",\"internal/ui/templates/rmm/terminal.html\",\"internal/ui/templates/settings/ai_usage.html\",\"internal/ui/templates/settings/api_keys.html\",\"internal/ui/templates/settings/appearance.html\",\"internal/ui/templates/settings/audit.html\",\"internal/ui/templates/settings/billing.html\",\"internal/ui/templates/settings/certificates.html\",\"internal/ui/templates/settings/escalation.html\",\"internal/ui/templates/settings/health.html\",\"internal/ui/templates/settings/helpdesk.html\",\"internal/ui/templates/settings/integrations.html\",\"internal/ui/templates/settings/legal.html\",\"internal/ui/templates/settings/mcp.html\",\"internal/ui/templates/settings/metasploit.html\",\"internal/ui/templates/settings/mfa_setup.html\",\"internal/ui/templates/settings/orchestrator.html\",\"internal/ui/templates/settings/portal_roles.html\",\"internal/ui/templates/settings/profile.html\",\"internal/ui/templates/settings/qb_mappings.html\",\"internal/ui/templates/settings/quickbooks.html\",\"internal/ui/templates/settings/sessions.html\",\"internal/ui/templates/settings/tenant.html\",\"internal/ui/templates/settings/users.html\",\"internal/ui/templates/settings/voip.html\",\"internal/ui/templates/survey/audio_recordings.html\",\"internal/ui/templates/survey/departure_gate.html\",\"internal/ui/templates/survey/detail.html\",\"internal/ui/templates/survey/form.html\",\"internal/ui/templates/survey/list.html\",\"internal/ui/templates/survey/photo_analysis.html\",\"internal/ui/templates/survey/photo_analysis_detail.html\",\"internal/ui/templates/survey/photo_analysis_results.html\",\"internal/ui/templates/survey/print.html\",\"internal/ui/templates/survey/settings.html\",\"internal/ui/templates/survey/template_edit.html\",\"internal/ui/templates/survey/walkthrough.html\",\"internal/ui/templates/timesheet/all.html\",\"internal/ui/templates/timesheet/approvals.html\",\"internal/ui/templates/timesheet/detail.html\",\"internal/ui/templates/timesheet/my_timesheet.html\",\"internal/ui/templates/workorder/detail.html\",\"internal/ui/templates/workorder/form.html\",\"internal/ui/templates/workorder/list.html\",\"internal/ui/theme_mixer_test.go\"]},{\"name\":\"internal \u2014 version\",\"slug\":\"internal-version\",\"files\":[\"internal/version/version.go\"]},{\"name\":\"internal \u2014 voip\",\"slug\":\"internal-voip\",\"files\":[\"internal/voip/handler.go\",\"internal/voip/types.go\"]},{\"name\":\"internal \u2014 workorder\",\"slug\":\"internal-workorder\",\"files\":[\"internal/workorder/handler.go\",\"internal/workorder/types.go\"]}]},{\"name\":\"mockups\",\"slug\":\"mockups\",\"files\":[],\"children\":[{\"name\":\"mockups \u2014 mockups\",\"slug\":\"mockups-mockups\",\"files\":[\"mockups/bid_drawing_page_selection.html\",\"mockups/crm_client_compliance_tab.html\",\"mockups/distributor_s1_vendor_settings.html\",\"mockups/distributor_s2_catalog_browser.html\",\"mockups/distributor_s3_client_subscriptions.html\",\"mockups/distributor_s4_contract_distributor_lines.html\",\"mockups/distributor_s5_product_catalog.html\",\"mockups/distributor_s6_invoice_distributor_cost.html\",\"mockups/ncsr_action_plan_post_review.html\",\"mockups/ncsr_assessment_full_page.html\",\"mockups/ncsr_maturity_trajectory.html\",\"mockups/ncsr_post_review_overview.html\",\"mockups/ncsr_reviews_history_tab.html\",\"mockups/ncsr_sectioned_multi_reviewer.html\",\"mockups/pick-pages-hover.html\",\"mockups/pick-pages-mockup.html\",\"mockups/pick-pages-placement.html\",\"mockups/pick-pages-redesign.html\",\"mockups/slideout_mockup.html\"]},{\"name\":\"mockups \u2014 nexusiq\",\"slug\":\"mockups-nexusiq\",\"files\":[\"mockups/nexusiq/ARCHITECTURE.md\",\"mockups/nexusiq/gut_check.html\",\"mockups/nexusiq/index.html\",\"mockups/nexusiq/lever_lab.html\",\"mockups/nexusiq/plan_cascade.html\",\"mockups/nexusiq/proposal_v2_sales_cockpit.html\",\"mockups/nexusiq/proposal_v2_sales_workspace.html\",\"mockups/nexusiq/proposal_v3_pbr_dense_grid.html\",\"mockups/nexusiq/proposal_v3b_pbr_full_sheet.html\",\"mockups/nexusiq/sales_cockpit.html\",\"mockups/nexusiq/sales_workspace.html\"]}]},{\"name\":\"scripts\",\"slug\":\"scripts\",\"files\":[\"scripts/changelog/main.go\",\"scripts/check-tenant-id.sh\",\"scripts/proxmox-deploy.sh\",\"scripts/security_gen/main.go\",\"scripts/seed.go\"]}]};\n\n(function() {\n  var activePage = 'overview';\n\n  document.addEventListener('DOMContentLoaded', function() {\n    mermaid.initialize({ startOnLoad: false, theme: 'neutral', securityLevel: 'loose' });\n    renderMeta();\n    renderNav();\n    document.getElementById('menu-toggle').addEventListener('click', function() {\n      document.getElementById('sidebar').classList.toggle('open');\n    });\n    if (location.hash &amp;&amp; location.hash.length &gt; 1) {\n      activePage = decodeURIComponent(location.hash.slice(1));\n    }\n    navigateTo(activePage);\n  });\n\n  function renderMeta() {\n    if (!META) return;\n    var el = document.getElementById('meta-info');\n    var parts = [];\n    if (META.generatedAt) {\n      parts.push(new Date(META.generatedAt).toLocaleDateString());\n    }\n    if (META.model) parts.push(META.model);\n    if (META.fromCommit) parts.push(META.fromCommit.slice(0, 8));\n    el.textContent = parts.join(' \\u00b7 ');\n  }\n\n  function renderNav() {\n    var container = document.getElementById('nav-tree');\n    var html = '\n';\n    html += 'Overview';\n    html += '';\n    if (TREE.length &gt; 0) {\n      html += '\nModules';\n      html += buildNavTree(TREE);\n    }\n    container.innerHTML = html;\n    container.addEventListener('click', function(e) {\n      var target = e.target;\n      while (target &amp;&amp; !target.dataset.page) { target = target.parentElement; }\n      if (target &amp;&amp; target.dataset.page) {\n        e.preventDefault();\n        navigateTo(target.dataset.page);\n      }\n    });\n  }\n\n  function buildNavTree(nodes) {\n    var html = '';\n    for (var i = 0; i &lt; nodes.length; i++) {\n      var node = nodes[i];\n      html += '\n';\n      html += '' + escH(node.name) + '';\n      if (node.children &amp;&amp; node.children.length &gt; 0) {\n        html += '\n' + buildNavTree(node.children) + '';\n      }\n      html += '';\n    }\n    return html;\n  }\n\n  function escH(s) {\n    var d = document.createElement('div');\n    d.textContent = s;\n    return d.innerHTML;\n  }\n\n  function navigateTo(page) {\n    activePage = page;\n    location.hash = encodeURIComponent(page);\n\n    var items = document.querySelectorAll('.nav-item');\n    for (var i = 0; i &lt; items.length; i++) {\n      if (items[i].dataset.page === page) {\n        items[i].classList.add('active');\n      } else {\n        items[i].classList.remove('active');\n      }\n    }\n\n    var contentEl = document.getElementById('content');\n    var md = PAGES[page];\n\n    if (!md) {\n      contentEl.innerHTML = '\n\nPage not found\n' + escH(page) + '.md does not exist.';\n      return;\n    }\n\n    contentEl.innerHTML = marked.parse(md);\n\n    // Rewrite .md links to hash navigation\n    var links = contentEl.querySelectorAll('a[href]');\n    for (var i = 0; i &lt; links.length; i++) {\n      var href = links[i].getAttribute('href');\n      if (href &amp;&amp; href.endsWith('.md') &amp;&amp; href.indexOf('://') === -1) {\n        var slug = href.replace(/\\.md$/, '');\n        links[i].setAttribute('href', '#' + encodeURIComponent(slug));\n        (function(s) {\n          links[i].addEventListener('click', function(e) {\n            e.preventDefault();\n            navigateTo(s);\n          });\n        })(slug);\n      }\n    }\n\n    // Convert mermaid code blocks into mermaid divs\n    var mermaidBlocks = contentEl.querySelectorAll('pre code.language-mermaid');\n    for (var i = 0; i &lt; mermaidBlocks.length; i++) {\n      var pre = mermaidBlocks[i].parentElement;\n      var div = document.createElement('div');\n      div.className = 'mermaid';\n      div.textContent = mermaidBlocks[i].textContent;\n      pre.parentNode.replaceChild(div, pre);\n    }\n    try { mermaid.run({ querySelector: '.mermaid' }); } catch(e) {}\n\n    window.scrollTo(0, 0);\n    document.getElementById('sidebar').classList.remove('open');\n  }\n})();\n\n\n\n", "creation_timestamp": "2026-05-25T10:26:41.000000Z"}