agent_tracker/CLAUDE.md
nickviljoen 7445c30caf Document UI & brand conventions in CLAUDE.md
Adds a "UI & Brand (2026-05)" section covering the OLIVER design tokens
(yellow #FFCB05, near-black #1A1A1A, Montserrat), signature motifs
(.page-title highlight, .stat-tile, card-header accent), and the two
gotchas to avoid re-introducing (don't override .navbar position in
nav.html; tabs must use button data-bs-target, not a href).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:26:39 +02:00

30 KiB
Raw Permalink Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

AgentHub is a FastAPI-based AI Agent Management System with MongoDB backend. It provides three-tier role-based authentication (admin/readonly_admin/user), agent CRUD operations, client verification workflow, email notifications, user management, and a web interface built with Jinja2 templates and Bootstrap 5.

Common Development Tasks

Running the Application

uvicorn main:app --reload --port 8000

Access at: http://localhost:8000

Installing Dependencies

pip install -r requirements.txt

Environment Setup

Create .env file with:

  • MONGODB_URI: MongoDB connection string (default: mongodb://localhost:27017)
  • MONGODB_DBNAME: Database name (default: agenthub_db)
  • SECRET_KEY: JWT secret key
  • ALGORITHM: JWT algorithm (default: HS256)
  • ACCESS_TOKEN_EXPIRE_MINUTES: Token expiration (default: 60)

Optional: Prompt Audit (Gemini)

  • GOOGLE_API_KEY: Google API key for Gemini (if not set, audit is silently disabled)
  • AUDIT_GEMINI_MODEL: Gemini model name (default: gemini-2.5-pro)
  • AUDIT_CONCURRENCY: Batch size for sequential processing (default: 2)

Optional: Email Notifications (Mailgun)

  • MAILGUN_API_KEY: Mailgun API key (if not set, notifications are silently disabled)
  • MAILGUN_DOMAIN: Mailgun sending domain (e.g. your-domain.mailgun.org)
  • MAILGUN_FROM_EMAIL: Sender address (default: AgentHub <noreply@{MAILGUN_DOMAIN}>)
  • NOTIFICATION_REPLY_TO: Reply-To address attached to every Mailgun send (default: Nick.Viljoen@oliver.agency). Lets recipients reply to a real mailbox instead of the noreply@ sender.
  • TOKEN_USAGE_THRESHOLD: Weekly token count that triggers an alert (default: 100000)
  • NOTIFICATION_COOLDOWN_HOURS: Hours between repeat alerts for the same agent (default: 24)
  • CLIENT_AGENT_NOTIFY_EMAILS: Comma-separated list of emails for client agent notifications
  • WEEKLY_DIGEST_HOUR: Hour (24h format) to send weekly digest on Mondays (default: 7)

Optional: Completion-Reminder Emails (LibreChat completion flow)

  • COMPLETION_REMINDER_COOLDOWN_DAYS: Min days between reminders for the same user (default: 7)
  • COMPLETION_REMINDER_MAX_NUDGES: After this many nudges, stop emailing the user (default: 4)
  • COMPLETION_REMINDER_HOUR: Hour (24h) for the daily reminder cron (default: 8)
  • AGENTHUB_PUBLIC_URL: Public absolute URL used in email links (e.g. https://agenthub.example.com). If unset, links are relative and won't work in email clients.

Default Login Credentials

  • Admin: admin@agenthub.com / admin123
  • Test User: test@example.com / testpass123

Code Architecture

Core Application Structure

main.py: FastAPI application with:

  • JWT cookie-based authentication system
  • HTML routes for web interface
  • REST API endpoints for agent/user management
  • Three-tier role-based access control (admin / readonly_admin / user)
  • Client verification workflow with email notifications
  • Daily agent digest scheduler (APScheduler)
  • Template rendering with Jinja2

Key Authentication Functions:

  • get_current_user_optional(): Cookie-based auth for templates
  • get_current_user_from_cookie(): Required auth for API endpoints
  • require_admin(): Admin-only access control (write operations)
  • require_admin_or_readonly(): Admin + readonly_admin access (read-only dashboard views)

Data Layer

models.py: Pydantic models for:

  • AiAgent: Core agent model with comprehensive fields (includes discipline, rating, client, client_name, studio_name)
  • UsageTimelineEntry: Daily usage data including message_count and token_count
  • UserCreate/UserResponse: User management models (includes role field: user/admin/readonly_admin)
  • UserUpdate: Includes role field for three-tier role management
  • AiAgentCreate/AiAgentResponse: API request/response models (includes total_tokens, prompt_tokens, completion_tokens, discipline, rating, rating_count, client, client_name, studio_name, verification_status, verified_by, verified_date)
  • AgentCollectorCreate: Collector API input model (includes total_tokens, prompt_tokens, completion_tokens, discipline, client, client_name, studio_name)
  • AuditReviewRequest: Audit review input (audit_status: flagged/reviewed/cleared, reviewer_notes)
  • AgentUsageStatsResponse: Usage statistics response (includes total_tokens, prompt_tokens, completion_tokens)

crud.py: Database operations using Motor (async MongoDB driver):

  • User CRUD: authentication, creation, management
  • Agent CRUD: create, read, update, delete with user ownership
  • Advanced features: search, filtering, statistics, pagination
  • All operations use ObjectId for MongoDB document IDs

database.py: MongoDB connection setup with Motor async client

  • Collections: users, agents, agent_usage, token_notifications, agent_ratings, audit_history, completion_reminders
  • ensure_indexes(): Creates compound unique index on agent_ratings(agent_id, user_id), indexes on verification_status, audit_status, and audit_history(agent_id, audit_date)

audit_analyzer.py: Gemini-powered agent classification and audit system:

  • is_gemini_configured(): Returns False if GOOGLE_API_KEY not set (gracefully disabled)
  • analyze_single_agent(): Sends agent instructions to Gemini 2.5 Pro, returns structured JSON with category, discipline, department, client detection, risk level, flags, and recommendations. Includes retry with exponential backoff for rate limits.
  • store_audit_result(): Writes audit results to agent document (audit_status, audit_category, audit_risk_level, etc.) and inserts into audit_history collection
  • apply_classification_fields(): Auto-assigns discipline (from defined list), agent_department (free text inferred from instructions), and client detection (client = "yes", verification_status = "needs_verification") — only overwrites fields that are currently empty/null
  • classify_single_agent(): Convenience function for post-sync automatic classification. Loads agent, analyses, stores result, applies fields.
  • run_audit_batch(): Batch processes agents sequentially with rate-limit-safe pauses. Supports unclassified_only and single_agent_id params. Returns summary with audited/failed/skipped counts.
  • get_all_audit_results(): Returns all agents with audit fields for the Prompt Audit tab
  • update_audit_review(): Admin marks audit as reviewed/cleared with notes
  • Uses google-genai SDK (new API), model configurable via AUDIT_GEMINI_MODEL env var (default: gemini-2.5-pro)

notifications.py: Mailgun email notification system:

  • is_mailgun_configured(): Returns False if env vars not set (gracefully disabled)
  • send_mailgun_email(): POST to Mailgun HTTP API with 10s timeout
  • build_threshold_email(): HTML email template for threshold alerts
  • check_and_notify_threshold(): Checks 7-day token usage against threshold, enforces cooldown via token_notifications collection, sends to admin users
  • send_client_agent_notification(): Sends email when client-facing agent is created (to CLIENT_AGENT_NOTIFY_EMAILS)
  • build_client_agent_email(): HTML email template for client agent notifications
  • send_weekly_agent_digest(): Queries agents created in last 7 days, sends summary to all admin users
  • build_weekly_digest_email(): HTML email template for weekly digest

auth.py: JWT authentication with:

  • bcrypt password hashing
  • JWT token creation/validation using python-jose
  • Configurable token expiration

Frontend Templates

Located in templates/ directory:

  • base.html: Bootstrap 5 base template with navigation
  • nav.html: Dynamic navigation based on user role
  • index.html: Landing page
  • login.html/register.html: Authentication forms
  • agent_register.html: Agent creation form
  • agent_management.html: Agent dashboard with real data
  • search.html: Global search functionality
  • user_management.html: User management interface
  • admin/dashboard.html: Admin statistics, management, and verification workflow

Static Assets

static/style.css: Custom CSS with:

  • CSS variables for consistent theming
  • Gradient backgrounds and modern styling
  • Responsive design for mobile devices
  • Bootstrap 5 customizations

Key Features

Authentication Flow

  • Cookie-based JWT authentication
  • Three-tier role-based access: user, admin, readonly_admin
    • user: Standard access, can manage own agents
    • admin: Full access to admin dashboard, all write operations
    • readonly_admin: Can view admin dashboard but all write actions (edit, delete, approve, create) are hidden
  • role field on user documents; is_admin kept in sync for backward compatibility
  • require_admin_or_readonly() dependency for read-only admin endpoints
  • Automatic redirects based on user role (admin/readonly_admin → /admin, user → /agent-management)
  • Secure logout with token cleanup

Agent Management

  • Full CRUD operations with user ownership
  • Status tracking (Active, Inactive, Development, Deprecated)
  • Rich metadata: tags, userbase, department, contact person
  • Search functionality across multiple fields
  • Filtering by status, audit status (Audited / Not Audited), and discipline
  • Admin can view/manage all agents

Client & Verification System

  • client field on agents: "yes" or "no" (mandatory on registration form)
  • client_name: free text, required when client is "yes"
  • studio_name: optional free text field
  • Registration form order: Name, Description, Purpose, Client (Yes/No), Client Name (conditional), Studio Name, Tool, then Version/Status/etc.
  • When client == "yes": agent auto-tagged with verification_status = "needs_verification"
  • Verification tab on admin dashboard shows pending agents with Approve button
  • PUT /api/admin/agents/{id}/verify — admin-only, sets status to "verified" with verifier info
  • GET /api/admin/agents/pending-verification — returns agents needing verification
  • Verification badges displayed on agent cards (orange "Needs Verification", green "Verified")

Client Agent Email Notification

  • When client == "yes" on agent creation, sends email via Mailgun to CLIENT_AGENT_NOTIFY_EMAILS
  • Subject: "Client Agent Created"
  • Body includes: Agent Name, Description, Purpose, Client Name, Studio Name, Tool, Created By
  • Non-blocking: failure does not break agent creation

Weekly Agent Digest Email

  • Scheduled via APScheduler to run every Monday morning (default 7:00 AM, WEEKLY_DIGEST_HOUR env var)
  • Queries agents created in last 7 days
  • Sends to all active admin users
  • Body includes: Agent Name, Purpose, Description, Created By (email)
  • Subject: "Agents Created in Last Week"
  • Skips sending if no agents created
  • Can be manually triggered via POST /api/admin/digest/send (admin-only)

Discipline & Star Rating

  • discipline field classifies agents into business categories: Strategy, Creative, Oversight including delivery, Optimization, Back Office including operations, Pencil Agents
  • Required on registration form, optional on edit (to support legacy agents)
  • Pencil Agents discipline is auto-assigned to agents with "pencil" in the name when no discipline is set (collector API auto-tag + startup migration)
  • rating field stores the average star rating (1-5) computed from all per-user ratings
  • rating_count field stores the number of individual ratings
  • Per-user rating system: Any authenticated user can rate any agent via PUT /api/agents/{id}/rating
    • Individual ratings stored in agent_ratings collection with compound unique index on (agent_id, user_id)
    • After each rating, the agent's average rating and count are recalculated and stored on the agent document
    • GET /api/agents/{id}/my-rating returns the current user's rating plus the average and count
  • Interactive star rating widget in detail modal shows the user's own rating as filled stars
  • Average rating and count displayed below the stars and on agent card badges
  • Rating removed from edit modals (rating is per-user, not admin-set)
  • Rating framework info modal accessible via info icon next to "Rating:" label
  • Dashboard supports filtering by discipline and sorting by rating
  • Discipline badge (purple) and star rating badge displayed on agent cards
  • Both fields included in CSV export/import
  • Discipline passed through collector API; rating is human-only (not in collector)

Token Usage Tracking

  • total_tokens, prompt_tokens, completion_tokens fields on agents track cumulative LLM token consumption
  • prompt_tokens (input) and completion_tokens (output) provide cost breakdown detail
  • token_count per day in usage timeline entries alongside message_count
  • Token badge displayed on agent cards (gold/coins icon) with prompt/completion breakdown in tooltip
  • Usage modal shows Total Tokens stat with In/Out breakdown alongside messages/conversations/users
  • Dual-axis chart (messages left axis, tokens right axis) when token data exists
  • Sort agents by Total Tokens
  • CSV export includes total_tokens, prompt_tokens, completion_tokens columns

High Usage Email Notifications

  • Entirely optional — silently disabled when Mailgun env vars are not set
  • Triggered from the Agent Collector endpoint (POST /agents); checks 7-day rolling token usage from usage_timeline
  • Alerts when weekly token usage exceeds threshold (default 100,000, configurable via TOKEN_USAGE_THRESHOLD)
  • Non-blocking — notification failure never breaks the collector API
  • Cooldown tracking in MongoDB token_notifications collection (default 24h, configurable)
  • Sends to all active admin users' email addresses

Prompt Audit & Auto-Classification (Gemini)

  • Automated analysis of agent instructions (system prompts) using Google Gemini 2.5 Pro
  • Two trigger modes:
    1. Automatic post-sync: After the collector API (POST /agents) creates/updates an agent with instructions, a background task (asyncio.create_task) auto-classifies it. Non-blocking — sync response is not delayed.
    2. Manual batch: Admin clicks "Run Full Audit" or "Run Unclassified Only" on the Prompt Audit tab. Processes agents sequentially with 4-second pauses between batches to avoid Gemini rate limits.
  • Classification outputs per agent:
    • audit_category: Cat 1 (Internal Sandbox), Cat 1B (High Cost Internal), Cat 2 (Client-Exposed), Cat 3 (Client-Sold)
    • audit_risk_level: low / medium / high / critical
    • audit_discipline: Picks from existing discipline list (Strategy, Creative, Oversight including delivery, Optimization, Back Office including operations, Pencil Agents)
    • audit_department: Free text inferred from instructions (e.g., "Project Management", "Media")
    • audit_is_client_work: Boolean — detects client names, brands, client deliverables in instructions
    • audit_flags: Array of risk flags (client_facing, handles_pii, uses_external_tools, etc.)
    • audit_summary, audit_recommendations, audit_category_reasoning, audit_discipline_reasoning, audit_client_work_reasoning, audit_client_name_detected
  • Auto-assignment: Gemini results auto-populate discipline and agent_department fields on the agent document (only if currently empty, to respect manual edits)
  • Client work auto-detection: When audit_is_client_work = true, auto-sets client = "yes" and verification_status = "needs_verification" — agent appears on Verification tab. Does NOT overwrite manually-set values.
  • Audit statuses: All audited agents start as flagged. Admin can mark as reviewed or cleared via the detail modal with optional notes.
  • Prompt Audit tab on admin dashboard: Summary cards (Audited, Flagged, Reviewed, Cleared, Client Detected, No Instructions), filterable results table, detail modal with full analysis and review controls
  • Rate limit handling: Retry with exponential backoff (10s, 20s, 40s) for 429/quota errors
  • Logging: Uses Python logging module (audit_analyzer logger) for systemd journal visibility
  • Agents without instructions are skipped (counted as "No Instructions")
  • audit_history collection stores historical record of each audit run and review action
  • Gracefully disabled when GOOGLE_API_KEY not set — UI shows config warning, buttons hidden

User Management

  • User registration with validation
  • Admin user creation capabilities
  • Three-tier role system: user, admin, readonly_admin
    • Role dropdown in admin user edit modal (replaces is_admin checkbox)
    • role and is_admin fields kept in sync on update
  • Profile management
  • User statistics and administration

Database Integration

  • MongoDB with proper ObjectId handling
  • Async operations using Motor driver
  • Indexed queries for performance
  • Data aggregation for statistics
  • Collections: users, agents, agent_usage, token_notifications, agent_ratings, audit_history, completion_reminders

Development Guidelines

Database Operations

  • Always use ObjectId for MongoDB document IDs
  • Use Motor async driver methods (await collection.find_one())
  • Handle ObjectId conversion in CRUD operations
  • Implement proper error handling with try/except blocks

Authentication

  • Use cookie-based auth for web interface
  • API endpoints require get_current_user_from_cookie() dependency
  • Write endpoints use require_admin() dependency
  • Read-only admin endpoints use require_admin_or_readonly() dependency
  • Always validate user permissions for data access
  • readonly_admin users can view admin dashboard but UI hides all write-action buttons via admin-write-action CSS class

Template Context

  • Pass current_user to all templates for navigation
  • Handle dict objects (not User model instances) in templates
  • Use proper null checks for optional user data

API Response Models

  • Convert ObjectId to string in API responses
  • Handle optional datetime fields with isoformat()
  • Maintain consistency between Create and Response models

Error Handling

  • Provide meaningful error messages in templates
  • Use proper HTTP status codes in API responses
  • Graceful degradation for missing data

Project Dependencies

Key dependencies from requirements.txt:

  • fastapi: Web framework
  • uvicorn: ASGI server
  • motor: Async MongoDB driver
  • pymongo: MongoDB operations
  • python-jose: JWT token handling
  • passlib: Password hashing
  • bcrypt: Password encryption
  • pydantic: Data validation
  • jinja2: Template engine
  • python-multipart: Form handling
  • requests: HTTP client (used for Mailgun API calls)
  • apscheduler: Task scheduling (weekly digest email)
  • google-genai: Google Gemini API client (used for prompt audit auto-classification)

API Endpoints (New)

Verification

  • GET /api/admin/agents/pending-verification — List agents with verification status (admin + readonly_admin)
  • PUT /api/admin/agents/{agent_id}/verify — Approve/verify an agent (admin only)

Weekly Digest

  • POST /api/admin/digest/send — Manually trigger the weekly agent digest email (admin only)

Prompt Audit

  • POST /api/admin/audit/run — Run Gemini audit batch. Optional JSON body: {agent_id, unclassified_only}. Returns {status, total, audited_count, failed_count, skipped_count, results_summary} (admin only)
  • GET /api/admin/audit/results — Get all agents with audit data + config_status.gemini_configured (admin + readonly_admin)
  • PUT /api/admin/audit/{agent_id}/review — Mark audit as reviewed/cleared with {audit_status, reviewer_notes} (admin only)

Registration Completion Flow (LibreChat-synced agents)

  • GET /api/agents/incomplete — Current user's incomplete agents (admins see all). Used by the agent management page banner. Owner resolution: created_by == user_id OR (created_by == "agent_collector_api" AND agent_contact_person == user_email).
  • GET /agent-complete/{agent_id} — HTML form (parameterised agent_register.html in completion mode). Pre-fills existing fields, required new governance fields blank. Auth: agent owner (by ID or email) or admin.
  • POST /agent-complete/{agent_id} — Submit completion. Sets registration_complete=True, reassigns created_by from "agent_collector_api" to the submitting user's ID.
  • POST /api/admin/completion-reminders/send — Manually trigger the daily reminder digest. Optional ?force=true to bypass cooldown + max-nudges cap (admin only).
  • GET /api/admin/agents/unresolved-owner — Collector-created agents whose agent_contact_person doesn't match an active user (admin only)
  • PUT /api/admin/agents/{agent_id}/reassign-owner — Admin reassigns ownership; body {new_contact_email, new_owner_user_id?}. Resolves user by email if new_owner_user_id omitted (admin only)

Registration Form Redesign (2026-05)

The agent registration form was rebuilt around 7 governance sections per Indext2.html mockup. See PLAN-registration-form-redesign.md for full design rationale.

New schema fields on agents

Top-level (all Optional):

  • business_entity (enum: OLIVER, DARE, Brandtech Group, Pencil, Jellyfish, Adjust, Other) — required on form
  • client_scope (enum: internal, all, specific) — required on form. Maps to legacy client field: specificyes, else→no. Both fields stay in sync to preserve existing verification + Mailgun client-agent-email flows.
  • agent_classification (enum: Utility, Functional, Supervisory, Guardian) — required on form
  • autonomy_level (enum: Human-Led, Hybrid, Autopilot) — required on form
  • ip_ownership (enum: Brandtech IP, Client IP, Shared/TBD) — required on form
  • foundation_model (string) — optional, dropdown with optgroups by provider
  • validated_by, validation_date, evals_method — optional Performance & Testing fields
  • registration_complete (bool) — True for form-submitted agents, False for collector-created. Drives the completion-reminder flow.

Nested objects:

  • safety: { off_switch_confirmed, access_rights_confirmed } — booleans, recorded but don't block submission
  • pii: { handles_pii, legal_ref, data_types, consent_recorded } — Yes/No radio with conditional fields
  • declarations: { governance, accuracy, upkeep } — three required checkboxes

Module-level enum constants live in models.py (BUSINESS_ENTITIES, CLIENT_SCOPES, AGENT_CLASSIFICATIONS, AUTONOMY_LEVELS, IP_OWNERSHIPS, DISCIPLINES).

Form field mapping

  • "Studio / Department" form input → stored as studio_name (not agent_departmentagent_department continues to be auto-populated by Gemini audit)
  • Author / Contact Person — autofilled from logged-in user's email; not a user-editable input
  • Capabilities — 8 fixed checkboxes comma-split values from "Other" free-text input, stored as agent_capabilities: list[str]

Discipline rename

Discipline value Optimization (US) was renamed to Optimisation (UK) in 2026-05. Migration runs on startup (crud.migrate_optimisation_spelling). All template dropdowns + Gemini system prompt updated. Defensive Optimization → Optimisation normalisation in audit_analyzer.store_audit_result and apply_classification_fields.

Required-field enforcement

The /agent-register POST handler enforces required fields server-side via Form(...) defaults plus explicit conditional checks (e.g. client_name required if client_scope == "specific", all 3 declarations required). Existing agents are grandfathered via the startup migration that backfilled registration_complete based on created_by.

Completion flow for LibreChat-synced agents

LibreChat-synced agents enter via the collector with created_by="agent_collector_api" and only a subset of fields populated. They land with registration_complete=False (set in crud.create_agent_from_collector via setdefault). Owners are nudged via daily APScheduler email job (notifications.send_completion_reminders):

  1. Job groups incomplete agents by lowercased agent_contact_person and resolves to active users (case-insensitive email match)
  2. Per-user cooldown (default 7 days) + nudge cap (default 4) tracked in completion_reminders collection
  3. Single digest email per user with one "Complete →" CTA per agent
  4. Submitting the completion form flips registration_complete=True AND reassigns created_by from the marker to the submitting user's ID — they then own it normally for future edits

agent_register.html is parameterised: mode="register" (default) or mode="complete". The completion-mode handler _build_completion_context() in main.py pre-computes capability sets, derived client_scope from legacy client field, and PII/safety pre-fills. Special case: agent_purpose renders blank if it equals agent_description (collector defaults purpose=description for ~94% of agents per prod data analysis).

Gemini audit auto-classification (extended 2026-05)

audit_analyzer.SYSTEM_INSTRUCTION schema extended to also return:

  • agent_classification (Utility/Functional/Supervisory/Guardian) — auto-applied to agent_classification field if empty
  • autonomy_level_hint (Human-Led/Hybrid/Autopilot/null) — auto-applied to autonomy_level if empty
  • agent_classification_reasoning, autonomy_reasoning — stored on agent doc as audit fields (not used as primary values)

Running POST /api/admin/audit/run after deploy auto-fills these on the ~225 agents with instructions, leaving only the truly non-inferable fields (business_entity, ip_ownership, declarations) for the human owner via the completion form.

Filter dimensions

Both agent_management.html and admin/dashboard.html agent views support filtering by:

  • Business Entity, Agent Type, Autonomy Level (new dropdowns)
  • "Compliance risks" quick toggle — shows agents where pii.handles_pii=true OR ip_ownership="Shared/TBD" OR autonomy_level="Autopilot"

CSV roundtrip for backfilling

/api/admin/agents/export/csv includes 21 new columns covering all governance fields. Import accepts them; nested objects (safety, pii, declarations) only build up if at least one cell is populated, so partially-filled CSVs don't clobber existing data with False. Defensive _csv_bool() helper returns None for empty cells.

UI & Brand (2026-05)

The frontend was rebranded to the OLIVER master template (/Users/nickviljoen/Documents/Optical_Projects/Pres Template/OLIVER Master PPT Template.potx). The old muddy-orange #f3ae3e palette and Inter font are gone. See PLAN-ui-refresh.md for design rationale.

Design tokens (static/style.css :root)

Always use these tokens — do not introduce new hex codes:

  • --brand-yellow: #FFCB05 — primary accent (page-title highlight, stat-tile left strip, card-header icons, active states, primary buttons)
  • --brand-yellow-soft: #FFF5C4 — subtle hover backgrounds, dropdown-item hover
  • --brand-orange: #FF5C00 — destructive accent (btn-danger, Microsoft SSO button, logout link)
  • --brand-dark: #1A1A1A — body text, table headers, modal headers, nav text
  • --brand-grey: #626262 — secondary text / muted copy
  • --brand-grey-light: #DEE2E5 — borders, dividers
  • --brand-off-white: #F6F7F7 — page background, table row hover
  • Font: Montserrat 400/500/600/700/800 (loaded from Google Fonts in base.html)

Signature motifs

  • Page title.page-title class. Yellow highlight rectangle behind black text via a linear-gradient (yellow up to 70% of the height, transparent above). Apply to every page <h2>; subtitle goes in <p class="page-subtitle"> below. Don't wrap with a leading icon — the highlight is the visual.
  • Stat tile.stat-tile class. White card with 8px yellow ::before strip on the left, yellow Font Awesome icon, big near-black .stat-tile-value and uppercase .stat-tile-label. Replaced the old orange-gradient .stat-card.
  • Card section accent — every <h5> / <h6> inside .card-header automatically gets a leading yellow icon (CSS targets .card-header h5 > i.fas:first-child) and a 28×3px yellow underline accent (::after). The accent doesn't apply to modal headers or to header titles displaying counts (those go in .card-body).
  • Modal header — solid --brand-dark background, white text, white close button.
  • Active nav-link / nav-tab — 2-3px --brand-yellow bottom border, never a coloured background pill.

Gotchas (don't re-introduce these bugs)

  • Navbar must use position: fixed (set in style.css). Do not add position: relative or position: sticky overrides in templates/nav.html — a previous inline <style> rule there silently overrode the fixed positioning and made the navbar scroll away with the page.
  • Bootstrap tabs must use <button data-bs-target="#x">, not <a href="#x">. The anchor form causes the browser to scroll to the tab pane on click (visible as the page "jumping down"). Bootstrap 5 recommends buttons for this reason.
  • Body has padding-top: 64px to clear the fixed navbar — keep it whenever you change navbar height.
  • No transform: scale(...) on table-row hover — jittery and shifts neighbour rows. Use a background-color change.
  • Chart.js colours in templates/admin/dashboard.html reference the brand palette directly (#FFCB05, #1A1A1A, #FF5C00) because the library can't read CSS vars. Keep them aligned when adding charts.

Where things live

  • static/style.css — all tokens + component styles (palette, navbar, tabs, buttons, forms, tables, cards, stat-tile, page-title, modal, alerts).
  • templates/base.html — Google Fonts link (Montserrat).
  • templates/nav.html — navbar markup + scoped styles for user-avatar / dropdown.
  • templates/admin/dashboard.html — inline <style> block has admin-specific overrides (column widths, audit risk badge colours, tabs); kept in-file because they're not reused elsewhere.
  • Page templates apply .page-title to their main <h2> and .page-subtitle to the description line.