diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d9fbdfa --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,63 @@ +# CC Dashboard — Project Entry Point + + + +## What Is This Project + +FastAPI + PostgreSQL personal productivity dashboard that ingests Claude Code session data (via a stop hook), aggregates it into daily stats, and displays KPIs, timeline, project breakdown, tool usage, and session activity through a single-page web UI with JWT authentication. + +**Client:** Oliver Internal (Vadym Samoilenko — personal tooling) +**Server:** unknown +**URL:** unknown (base path `/cc-dashboard`) + +--- + +## Quick Navigation + +| Need | Go to | +|------|-------| +| FastAPI app entry point + routers | `src/main.py` | +| App config and env var schema | `src/config.py` | +| SQLAlchemy models (Project, Session, DailyStat) | `src/models/` | +| Pydantic response schemas | `src/schemas/` | +| Auth router (login, refresh, JWT) | `src/routers/auth.py` | +| Dashboard aggregation endpoints | `src/routers/dashboard.py` | +| Data ingest endpoint (session stop hook) | `src/routers/ingest.py` | +| Project management endpoints | `src/routers/projects.py` | +| API key management | `src/routers/keys.py` | +| Admin routes | `src/routers/admin.py` | +| Event endpoints | `src/routers/events.py` | +| Database init and session factory | `src/database.py` | +| SPA frontend (static HTML/JS/CSS) | `src/static/index.html` | +| Docker compose (app + postgres:16) | `docker-compose.yml` | +| Environment config template | `.env.example` | +| Python dependencies | `requirements.txt` | + +--- + +## Dev Commands + +| Action | Command | +|--------|---------| +| Start full stack (Docker) | `docker compose up -d` | +| Rebuild after code changes | `docker compose up -d --build` | +| Stop stack | `docker compose down` | +| View logs | `docker compose logs -f app` | +| Run migrations (Alembic) | `docker compose exec app alembic upgrade head` | +| Run locally without Docker | `uvicorn src.main:app --host 0.0.0.0 --port 8800 --reload` | +| Copy env | `cp .env.example .env` (fill `DB_PASSWORD`, `SECRET_KEY`) | + +--- + +## Key Constraints + +- **NO SSH to server** without explicit user instruction +- **`SECRET_KEY`** must be at least 32 random chars — never use the placeholder +- **`DB_PASSWORD`** must be changed from `change_me_strong_password` before deployment +- **JWT tokens**: access = 30 min, refresh = 7 days (configurable in `.env`) +- **Base path is `/cc-dashboard`** — all routes and SPA fallback are scoped to this prefix +- **Static files hot-reload** without rebuild: `src/static/` is bind-mounted in Docker +- **Overlap-union dedup**: session hours use interval union per day to prevent double-counting parallel sessions (see `_union_hours` in `dashboard.py`) +- **PostgreSQL 16 required** — uses `to_char` and `extract('dow')` PG-specific functions + + diff --git a/deploy.sh b/deploy.sh index a9bcd2b..fc8516b 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,40 +1,50 @@ #!/usr/bin/env bash +# Deploy script for CC Dashboard on optical-dev.oliver.solutions +# Run ON the server: ssh optical-dev "cd /opt/cc-dashboard && bash deploy.sh" set -euo pipefail -STATIC_SRC="/opt/cc-dashboard/src/static" -STATIC_DST="/var/www/html/cc-dashboard" COMPOSE="docker compose -f /opt/cc-dashboard/docker-compose.yml" -log() { echo "[$(date '+%H:%M:%S')] $*"; } -die() { echo "[ERROR] $*" >&2; exit 1; } +log() { echo "[$(date '+%H:%M:%S')] $*"; } +die() { echo "[ERROR] $*" >&2; exit 1; } +pass() { echo "[OK] $*"; } cd /opt/cc-dashboard || die "Cannot cd to /opt/cc-dashboard" # ── 1. Pull latest code ────────────────────────────────────────────────────── -log "Pulling latest code..." -git pull +log "Pulling latest code from origin/main..." +git fetch origin +git reset --hard origin/main +pass "Code updated: $(git log --oneline -1)" -# ── 2. Sync static files to web root ──────────────────────────────────────── -# Apache serves /cc-dashboard/* from /var/www/html/cc-dashboard/ -# index.html must be at the root; CSS/JS go into static/ subdir to match -# paths in index.html (/cc-dashboard/static/css/..., /cc-dashboard/static/js/...) -log "Syncing static files to $STATIC_DST..." -sudo mkdir -p "$STATIC_DST/static" -sudo cp "$STATIC_SRC/index.html" "$STATIC_DST/index.html" -sudo rsync -a --delete --exclude='index.html' "$STATIC_SRC/" "$STATIC_DST/static/" - -# ── 3. Rebuild app image ───────────────────────────────────────────────────── -log "Building app image..." +# ── 2. Rebuild app image ───────────────────────────────────────────────────── +log "Building app image (Python deps + code)..." $COMPOSE build app +pass "Image built." -# ── 4. Restart app (migrations run automatically on container start) ───────── +# ── 3. Restart app (alembic upgrade runs automatically in CMD) ─────────────── +# DB container is NOT touched — data in pgdata volume is preserved. log "Restarting app container..." $COMPOSE up -d --no-deps app +pass "Container restarted. Alembic migrations run on container start." -# ── 5. Wait for healthy + show logs ───────────────────────────────────────── -log "Waiting for container to start..." -sleep 3 -$COMPOSE ps app -$COMPOSE logs --tail=30 app +# ── 4. Wait and verify ─────────────────────────────────────────────────────── +log "Waiting for app to become healthy..." +for i in $(seq 1 15); do + if $COMPOSE exec -T app sh -c "curl -fs http://localhost:8800/cc-dashboard/healthz > /dev/null 2>&1"; then + pass "Healthcheck OK after ${i}s." + break + fi + sleep 1 + if [[ $i -eq 15 ]]; then + log "Healthcheck timeout — showing last 40 lines of logs:" + $COMPOSE logs --tail=40 app + die "App did not start in time." + fi +done -log "Deploy complete." +# ── 5. Tail recent logs ────────────────────────────────────────────────────── +log "Recent app logs:" +$COMPOSE logs --tail=20 app + +log "Deploy complete. App at http://optical-dev.oliver.solutions:8800/cc-dashboard/" diff --git a/src/routers/assistant.py b/src/routers/assistant.py index 333edb7..63aefd7 100644 --- a/src/routers/assistant.py +++ b/src/routers/assistant.py @@ -1,7 +1,7 @@ """AI assistant router — streaming chat + flag management + session categorization.""" -import logging -from datetime import date, datetime, timezone +from datetime import date, datetime, timedelta, timezone +import structlog from fastapi import APIRouter, Depends, HTTPException, Response from fastapi.responses import StreamingResponse from sqlalchemy import select @@ -18,7 +18,7 @@ from src.schemas import ( ) from src.services.assistant import chat_stream, detect_day_anomalies, persist_flags -log = logging.getLogger(__name__) +log = structlog.get_logger() router = APIRouter(prefix="/api/assistant", tags=["assistant"]) @@ -78,7 +78,6 @@ async def list_flags( resolved: bool = False, db: AsyncSession = Depends(get_db), ) -> list[AiFlag]: - from datetime import timedelta since = datetime.now(timezone.utc).date() - timedelta(days=days_back) q = ( select(AiFlag) diff --git a/src/services/assistant.py b/src/services/assistant.py index 3777ab2..55c3999 100644 --- a/src/services/assistant.py +++ b/src/services/assistant.py @@ -2,11 +2,11 @@ from __future__ import annotations import json -import logging from collections import defaultdict from datetime import date, datetime, timedelta, timezone from typing import Any, AsyncIterator +import structlog from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -14,7 +14,7 @@ from src.config import settings from src.models import AiFlag, AssistantMessage, ManualEntry, Project, Session, Task, User from src.services.aggregator import _union_hours -log = logging.getLogger(__name__) +log = structlog.get_logger() # ── Gap detection constants ─────────────────────────────────────────────────── GAP_THRESHOLD_MINUTES = 30 # gaps between sessions longer than this may be unlogged work @@ -72,7 +72,7 @@ async def detect_day_anomalies( "entity_id": s.id, }) - if wall > 0 and s.active_hours / wall < RATIO_THRESHOLD: + if wall > 0 and (s.active_hours or 0) / wall < RATIO_THRESHOLD: anomalies.append({ "kind": "uncategorized", "description": ( @@ -336,7 +336,6 @@ async def execute_tool( return {"date": d.isoformat(), "anomalies": anomalies, "count": len(anomalies)} if tool_name == "create_manual_entry": - from src.models import ManualEntry entry = ManualEntry( user_id=user.id, project_id=tool_input.get("project_id"), @@ -468,21 +467,22 @@ async def chat_stream( messages=messages, ) - # Collect text from this round + # Collect text from this round; build proper assistant message for next round round_text = "" - has_tool_use = False + tool_results: list[dict] = [] # tool_result blocks for the user turn + assistant_content: list[dict] = [] # full content for the assistant turn for block in response.content: if block.type == "text": round_text += block.text full_response_text += block.text - # Stream text chunk + assistant_content.append({"type": "text", "text": block.text}) yield f"data: {json.dumps({'type': 'text', 'text': block.text})}\n\n" elif block.type == "tool_use": - has_tool_use = True tool_name = block.name tool_input = block.input + assistant_content.append({"type": "tool_use", "id": block.id, "name": tool_name, "input": tool_input}) yield f"data: {json.dumps({'type': 'tool_start', 'tool': tool_name})}\n\n" @@ -490,30 +490,22 @@ async def chat_stream( result = await execute_tool(tool_name, tool_input, user, db) await db.flush() except Exception as exc: - log.warning("assistant.tool_error", extra={"tool": tool_name, "error": str(exc)}) + log.warning("assistant.tool_error", tool=tool_name, error=str(exc)) result = {"error": str(exc)} tool_calls_log.append({"tool": tool_name, "input": tool_input, "result": result}) + tool_results.append({"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}) yield f"data: {json.dumps({'type': 'tool_result', 'tool': tool_name, 'result': result})}\n\n" - # Add assistant tool_use + tool_result to messages for next round - messages.append({ - "role": "assistant", - "content": [{"type": "tool_use", "id": block.id, "name": tool_name, "input": tool_input}], - }) - messages.append({ - "role": "user", - "content": [{"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}], - }) - - if not has_tool_use: - # No more tool calls — done + if not tool_results: + # No tool calls this round — conversation is done break - if round_text: - # Between-round text already streamed, add to messages - messages.append({"role": "assistant", "content": round_text}) + # Add the full assistant turn (text + all tool_use blocks) as one message + messages.append({"role": "assistant", "content": assistant_content}) + # Collect all tool_results into one user turn (Anthropic requirement) + messages.append({"role": "user", "content": tool_results}) # Persist assistant response if full_response_text or tool_calls_log: @@ -528,7 +520,7 @@ async def chat_stream( except Exception as exc: await db.rollback() - log.error("assistant.chat_error", extra={"user_id": user.id, "error": str(exc)}) + log.error("assistant.chat_error", user_id=user.id, error=str(exc)) yield f"data: {json.dumps({'type': 'error', 'text': 'Assistant error — please try again.'})}\n\n" finally: yield "data: [DONE]\n\n" diff --git a/src/static/assets/AdminView-DrzhLCxy.js b/src/static/assets/AdminView-CpcQgA1O.js similarity index 74% rename from src/static/assets/AdminView-DrzhLCxy.js rename to src/static/assets/AdminView-CpcQgA1O.js index 82bfc26..5826dd2 100644 --- a/src/static/assets/AdminView-DrzhLCxy.js +++ b/src/static/assets/AdminView-CpcQgA1O.js @@ -1 +1 @@ -import{d as p,u as y,y as h,c as r,a as t,e as n,n as v,w as d,f as b,r as u,o as s,F as g,l as k,t as a,k as m,i as A}from"./index-DRwzmrLE.js";import{a as w}from"./admin-DItGP2ar.js";import{_ as B,a as S}from"./CardContent.vue_vue_type_script_setup_true_lang-Dhtfm4bR.js";import{_ as f}from"./Badge.vue_vue_type_script_setup_true_lang-CKuGC1QO.js";import{_ as V,a as $}from"./utils-CFgQVuqB.js";const N={class:"p-6"},C={key:0,class:"flex items-center justify-center h-20"},D={class:"w-full"},E={class:"px-4 py-3"},F={class:"text-sm font-medium text-foreground"},R={class:"px-4 py-3 text-sm text-muted-foreground"},U={class:"px-4 py-3"},j={class:"px-4 py-3"},I={class:"px-4 py-3 text-xs text-muted-foreground"},G=p({__name:"AdminView",setup(J){const x=y(),_=b(),i=u([]),l=u(!1);return h(async()=>{if(!x.isAdmin){_.push("/");return}l.value=!0;try{const c=await w.users();i.value=c.data}finally{l.value=!1}}),(c,o)=>(s(),r("div",N,[o[1]||(o[1]=t("h2",{class:"text-lg font-semibold text-foreground mb-6"},"Admin — Users",-1)),l.value?(s(),r("div",C,[n(V,{class:"text-primary"})])):(s(),v(B,{key:1},{default:d(()=>[n(S,{class:"p-0"},{default:d(()=>[t("table",D,[o[0]||(o[0]=t("thead",null,[t("tr",{class:"border-b border-border"},[t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"User"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Email"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Role"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Status"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Joined")])],-1)),t("tbody",null,[(s(!0),r(g,null,k(i.value,e=>(s(),r("tr",{key:e.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",E,[t("p",F,a(e.username),1)]),t("td",R,a(e.email),1),t("td",U,[n(f,{variant:e.role==="admin"?"default":"secondary",class:"text-xs"},{default:d(()=>[m(a(e.role),1)]),_:2},1032,["variant"])]),t("td",j,[n(f,{variant:e.is_active?"success":"outline",class:"text-xs"},{default:d(()=>[m(a(e.is_active?"Active":"Inactive"),1)]),_:2},1032,["variant"])]),t("td",I,a(A($)(e.created_at)),1)]))),128))])])]),_:1})]),_:1}))]))}});export{G as default}; +import{d as p,u as y,x as h,c as r,a as t,e as n,n as v,w as d,f as b,r as u,o as s,F as g,l as k,t as a,k as m,i as A}from"./index-ZkX-rg-0.js";import{a as w}from"./admin-D88IFW1N.js";import{_ as B,a as S}from"./CardContent.vue_vue_type_script_setup_true_lang-DGh5KRxz.js";import{_ as x}from"./Badge.vue_vue_type_script_setup_true_lang-Cs8z3MtN.js";import{_ as V,a as $}from"./utils-DWBfPysr.js";const N={class:"p-6"},C={key:0,class:"flex items-center justify-center h-20"},D={class:"w-full"},E={class:"px-4 py-3"},F={class:"text-sm font-medium text-foreground"},R={class:"px-4 py-3 text-sm text-muted-foreground"},U={class:"px-4 py-3"},j={class:"px-4 py-3"},I={class:"px-4 py-3 text-xs text-muted-foreground"},G=p({__name:"AdminView",setup(J){const f=y(),_=b(),i=u([]),l=u(!1);return h(async()=>{if(!f.isAdmin){_.push("/");return}l.value=!0;try{const c=await w.users();i.value=c.data}finally{l.value=!1}}),(c,o)=>(s(),r("div",N,[o[1]||(o[1]=t("h2",{class:"text-lg font-semibold text-foreground mb-6"},"Admin — Users",-1)),l.value?(s(),r("div",C,[n(V,{class:"text-primary"})])):(s(),v(B,{key:1},{default:d(()=>[n(S,{class:"p-0"},{default:d(()=>[t("table",D,[o[0]||(o[0]=t("thead",null,[t("tr",{class:"border-b border-border"},[t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"User"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Email"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Role"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Status"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Joined")])],-1)),t("tbody",null,[(s(!0),r(g,null,k(i.value,e=>(s(),r("tr",{key:e.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",E,[t("p",F,a(e.username),1)]),t("td",R,a(e.email),1),t("td",U,[n(x,{variant:e.role==="admin"?"default":"secondary",class:"text-xs"},{default:d(()=>[m(a(e.role),1)]),_:2},1032,["variant"])]),t("td",j,[n(x,{variant:e.is_active?"success":"outline",class:"text-xs"},{default:d(()=>[m(a(e.is_active?"Active":"Inactive"),1)]),_:2},1032,["variant"])]),t("td",I,a(A($)(e.created_at)),1)]))),128))])])]),_:1})]),_:1}))]))}});export{G as default}; diff --git a/src/static/assets/AppLayout-BuZkSaIr.js b/src/static/assets/AppLayout-BuZkSaIr.js new file mode 100644 index 0000000..716e35e --- /dev/null +++ b/src/static/assets/AppLayout-BuZkSaIr.js @@ -0,0 +1 @@ +import{d as j,u as M,c as n,b as V,a as e,F as $,l as A,t as v,i as p,m as y,o as r,n as H,w as _,j as C,p as d,q as z,g as B,s as S,k as T,K as L,f as D,e as f,T as R,r as O}from"./index-ZkX-rg-0.js";const P={class:"flex flex-col h-full bg-gradient-to-b from-slate-900 via-slate-900 to-slate-950 border-r border-slate-800"},I={class:"flex-1 px-2 py-3 space-y-0.5 overflow-y-auto"},N={key:0,class:"absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 bg-amber-400 rounded-r-full"},F={class:"p-3 border-t border-slate-800 shrink-0"},K={class:"flex items-center gap-3 px-2 py-2 rounded-lg hover:bg-slate-800/50 transition-colors"},q={class:"h-7 w-7 rounded-full bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center text-[10px] font-bold text-white shrink-0"},E={class:"flex-1 min-w-0"},U={class:"text-xs font-medium text-slate-300 truncate"},W=j({__name:"Sidebar",emits:["close"],setup(b,{emit:m}){const a=B(),l=M(),x=m,u=[{name:"Dashboard",path:"/",icon:"grid"},{name:"Calendar",path:"/calendar",icon:"calendar"},{name:"Planner",path:"/planner",icon:"check-square"},{name:"Projects",path:"/projects",icon:"folder"},{name:"Live Feed",path:"/live",icon:"activity"},{name:"Reports",path:"/reports",icon:"file-text"},{name:"Keys",path:"/keys",icon:"key"},{name:"Settings",path:"/settings",icon:"settings"},{name:"Admin",path:"/admin",icon:"shield",adminOnly:!0}],k=y(()=>u.filter(c=>!c.adminOnly||l.isAdmin));function s(c){return c==="/"?a.path==="/":a.path.startsWith(c)}const i=y(()=>{var t,h;return(((t=l.user)==null?void 0:t.username)??((h=l.user)==null?void 0:h.email)??"?").slice(0,2).toUpperCase()});return(c,t)=>{var g,w;const h=z("RouterLink");return r(),n("aside",P,[t[11]||(t[11]=V('
CC Dashboard
Oliver Agency
Corporate Planning Hub
Corporate Planning Hub
$1').replace(/^- (.+)/gm,'').replace(/\n/g," ${de.replace(/^###? /,"")} ${de.replace(/^# /,"")} ${de}
")}async function x(){try{const D=await fetch("/cc-dashboard/api/assistant/history?limit=30",{headers:{Authorization:`Bearer ${t.token}`}});if(!D.ok)return;s.value=await D.json()}catch{}}async function g(){try{const D=await fetch("/cc-dashboard/api/assistant/flags?days_back=7&resolved=false",{headers:{Authorization:`Bearer ${t.token}`}});if(!D.ok)return;const E=await D.json();l.value=E.length}catch{}}async function w(){await fetch("/cc-dashboard/api/assistant/history",{method:"DELETE",headers:{Authorization:`Bearer ${t.token}`}}),s.value=[]}function S(){R("Show me all unresolved time-tracking issues from the last 7 days")}function R(D){r.value=D,y()}async function y(){const D=r.value.trim();if(!D||o.value)return;r.value="",$();const E={id:crypto.randomUUID(),role:"user",content:D,created_at:new Date().toISOString()};s.value.push(E),v(),o.value=!0,i.value="",a.value=[];try{const B=await fetch("/cc-dashboard/api/assistant/chat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t.token}`},body:JSON.stringify({message:D})});if(!B.ok||!B.body)throw new Error(`HTTP ${B.status}`);const z=B.body.getReader(),j=new TextDecoder;let te="";for(;;){const{done:ae,value:Se}=await z.read();if(ae)break;te+=j.decode(Se,{stream:!0});const le=te.split(`
-`);te=le.pop()??"";for(const Y of le){if(!Y.startsWith("data: "))continue;const oe=Y.slice(6).trim();if(oe!=="[DONE]")try{const ue=JSON.parse(oe);ue.type==="text"?(i.value+=ue.text,v()):ue.type==="tool_start"?a.value.includes(ue.tool)||a.value.push(ue.tool):ue.type==="tool_result"?a.value=a.value.filter(ye=>ye!==ue.tool):ue.type==="error"&&(i.value=ue.text)}catch{}}}i.value&&s.value.push({id:crypto.randomUUID(),role:"assistant",content:i.value,created_at:new Date().toISOString()}),await g()}catch{s.value.push({id:crypto.randomUUID(),role:"assistant",content:"Failed to get response. Please try again.",created_at:new Date().toISOString()})}finally{o.value=!1,i.value="",a.value=[],v()}}function v(){Hn(()=>{u.value&&(u.value.scrollTop=u.value.scrollHeight)})}function N(D){const E=D.target;E.style.height="auto",E.style.height=`${Math.min(E.scrollHeight,96)}px`}function $(){c.value&&(c.value.style.height="auto")}async function k(){n.value=!0,await x(),Hn(()=>{var D;return(D=c.value)==null?void 0:D.focus()}),v()}return Us(()=>{g(),setInterval(g,5*60*1e3)}),wn(n,D=>{D&&g()}),(D,E)=>(Z(),ce(Re,null,[n.value?st("",!0):(Z(),ce("button",{key:0,class:"fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-amber-400 text-gray-900 shadow-lg hover:bg-amber-300 transition-all duration-200 hover:scale-105 active:scale-95",title:"AI Assistant",onClick:k},[E[2]||(E[2]=G("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-7 w-7",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor","stroke-width":"2"},[G("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"})],-1)),l.value>0?(Z(),ce("span",Vg,Ct(l.value>9?"9+":l.value),1)):st("",!0)])),Te($d,{name:"slide-up"},{default:fn(()=>[n.value?(Z(),ce("div",$g,[G("div",Kg,[E[6]||(E[6]=G("div",{class:"flex items-center gap-2"},[G("div",{class:"flex h-8 w-8 items-center justify-center rounded-full bg-amber-400"},[G("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-4 w-4 text-gray-900",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor","stroke-width":"2"},[G("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"})])]),G("div",null,[G("p",{class:"text-sm font-semibold text-white"},"Time Analyst"),G("p",{class:"text-xs text-gray-400"},"AI assistant")])],-1)),G("div",Qg,[l.value>0?(Z(),ce("button",{key:0,class:"flex items-center gap-1 rounded-full bg-red-900/40 px-2 py-0.5 text-xs text-red-400 hover:bg-red-900/60 transition-colors",title:"View anomalies",onClick:S},[E[3]||(E[3]=G("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-3 w-3",viewBox:"0 0 20 20",fill:"currentColor"},[G("path",{"fill-rule":"evenodd",d:"M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z","clip-rule":"evenodd"})],-1)),Vs(" "+Ct(l.value)+" issue"+Ct(l.value>1?"s":""),1)])):st("",!0),G("button",{class:"p-1.5 text-gray-400 hover:text-white transition-colors",title:"Clear history",onClick:w},[...E[4]||(E[4]=[G("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-4 w-4",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor","stroke-width":"2"},[G("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"})],-1)])]),G("button",{class:"p-1.5 text-gray-400 hover:text-white transition-colors",onClick:E[0]||(E[0]=B=>n.value=!1)},[...E[5]||(E[5]=[G("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-4 w-4",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor","stroke-width":"2"},[G("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M6 18L18 6M6 6l12 12"})],-1)])])])]),G("div",{ref_key:"messagesEl",ref:u,class:"flex-1 overflow-y-auto p-3 space-y-3 min-h-0"},[s.value.length===0&&!o.value?(Z(),ce("div",zg,[E[7]||(E[7]=G("div",{class:"h-12 w-12 rounded-full bg-amber-400/10 flex items-center justify-center mb-3"},[G("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-6 w-6 text-amber-400",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor","stroke-width":"2"},[G("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"})])],-1)),E[8]||(E[8]=G("p",{class:"text-sm font-medium text-gray-300"},"Time Analyst",-1)),E[9]||(E[9]=G("p",{class:"text-xs text-gray-500 mt-1"},"Ask me about your hours, gaps, or missing time entries.",-1)),G("div",Wg,[(Z(),ce(Re,null,ts(f,B=>G("button",{key:B,class:"rounded-full border border-gray-600 px-3 py-1.5 text-xs text-gray-400 hover:border-amber-400 hover:text-amber-400 transition-colors",onClick:z=>R(B)},Ct(B),9,Gg)),64))])])):st("",!0),(Z(!0),ce(Re,null,ts(s.value,B=>(Z(),ce(Re,{key:B.id},[B.role==="user"?(Z(),ce("div",Jg,[G("div",Yg,[G("p",Xg,Ct(B.content),1)])])):(Z(),ce("div",Zg,[G("div",ey,[G("div",{class:"text-sm text-gray-200 prose prose-sm prose-invert max-w-none",innerHTML:p(B.content)},null,8,ty)])]))],64))),128)),o.value||i.value?(Z(),ce("div",ny,[G("div",sy,[a.value.length>0?(Z(),ce("div",ry,[(Z(!0),ce(Re,null,ts(a.value,B=>(Z(),ce("div",{key:B,class:"flex items-center gap-1.5 text-xs text-amber-400"},[E[10]||(E[10]=G("svg",{class:"h-3 w-3 animate-spin",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},[G("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor","stroke-width":"4"}),G("path",{class:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"})],-1)),Vs(" "+Ct(h(B)),1)]))),128))])):st("",!0),i.value?(Z(),ce("div",{key:1,class:"text-sm text-gray-200 prose prose-sm prose-invert max-w-none",innerHTML:p(i.value)},null,8,oy)):(Z(),ce("div",iy,[...E[11]||(E[11]=[G("span",{class:"h-1.5 w-1.5 rounded-full bg-gray-500 animate-bounce",style:{"animation-delay":"0ms"}},null,-1),G("span",{class:"h-1.5 w-1.5 rounded-full bg-gray-500 animate-bounce",style:{"animation-delay":"150ms"}},null,-1),G("span",{class:"h-1.5 w-1.5 rounded-full bg-gray-500 animate-bounce",style:{"animation-delay":"300ms"}},null,-1)])]))])])):st("",!0)],512),G("div",ay,[G("div",ly,[Df(G("textarea",{ref_key:"inputEl",ref:c,"onUpdate:modelValue":E[1]||(E[1]=B=>r.value=B),rows:"1",placeholder:"Ask about your time...",class:"flex-1 resize-none rounded-xl bg-gray-700 border border-gray-600 px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-amber-400 transition-colors max-h-24 overflow-y-auto",disabled:o.value,onKeydown:[Aa(Ra(y,["exact","prevent"]),["enter"]),Aa(Ra(()=>{},["shift","exact"]),["enter"])],onInput:N},null,40,cy),[[dh,r.value]]),G("button",{disabled:!r.value.trim()||o.value,class:"flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-xl bg-amber-400 text-gray-900 transition-all hover:bg-amber-300 disabled:opacity-40 disabled:cursor-not-allowed",onClick:y},[...E[12]||(E[12]=[G("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-4 w-4",viewBox:"0 0 20 20",fill:"currentColor"},[G("path",{d:"M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"})],-1)])],8,uy)]),E[13]||(E[13]=G("p",{class:"mt-1.5 text-center text-xs text-gray-600"},"Enter to send · Shift+Enter for newline",-1))])])):st("",!0)]),_:1})],64))}}),dy=(e,t)=>{const n=e.__vccOpts||e;for(const[s,r]of t)n[s]=r;return n},hy=dy(fy,[["__scopeId","data-v-5edd7614"]]),py=Vn({__name:"App",setup(e){const t=so(),n=me(()=>t.isAuthenticated);return(s,r)=>{const o=Xf("RouterView");return Z(),ce(Re,null,[Te(o),n.value?(Z(),Ut(hy,{key:0})):st("",!0),Te(Ke(em),{position:"top-right","toast-options":{style:{background:"hsl(var(--card))",color:"hsl(var(--card-foreground))",border:"1px solid hsl(var(--border))"}}})],64)}}}),my="modulepreload",gy=function(e){return"/cc-dashboard/"+e},Za={},bt=function(t,n,s){let r=Promise.resolve();if(n&&n.length>0){document.getElementsByTagName("link");const i=document.querySelector("meta[property=csp-nonce]"),a=(i==null?void 0:i.nonce)||(i==null?void 0:i.getAttribute("nonce"));r=Promise.allSettled(n.map(l=>{if(l=gy(l),l in Za)return;Za[l]=!0;const u=l.endsWith(".css"),c=u?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${l}"]${c}`))return;const f=document.createElement("link");if(f.rel=u?"stylesheet":my,u||(f.as="script"),f.crossOrigin="",f.href=l,a&&f.setAttribute("nonce",a),document.head.appendChild(f),u)return new Promise((h,p)=>{f.addEventListener("load",h),f.addEventListener("error",()=>p(new Error(`Unable to preload CSS for ${l}`)))})}))}function o(i){const a=new Event("vite:preloadError",{cancelable:!0});if(a.payload=i,window.dispatchEvent(a),!a.defaultPrevented)throw i}return r.then(i=>{for(const a of i||[])a.status==="rejected"&&o(a.reason);return t().catch(o)})};/*!
- * vue-router v4.6.4
- * (c) 2025 Eduardo San Martin Morote
- * @license MIT
- */const Gn=typeof document<"u";function Au(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function yy(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&Au(e.default)}const be=Object.assign;function Ro(e,t){const n={};for(const s in t){const r=t[s];n[s]=Tt(r)?r.map(e):e(r)}return n}const Fs=()=>{},Tt=Array.isArray;function el(e,t){const n={};for(const s in e)n[s]=s in t?t[s]:e[s];return n}const Ou=/#/g,vy=/&/g,by=/\//g,wy=/=/g,_y=/\?/g,Tu=/\+/g,xy=/%5B/g,Ey=/%5D/g,Pu=/%5E/g,Sy=/%60/g,Du=/%7B/g,Cy=/%7C/g,Nu=/%7D/g,Ry=/%20/g;function Ii(e){return e==null?"":encodeURI(""+e).replace(Cy,"|").replace(xy,"[").replace(Ey,"]")}function Ay(e){return Ii(e).replace(Du,"{").replace(Nu,"}").replace(Pu,"^")}function ni(e){return Ii(e).replace(Tu,"%2B").replace(Ry,"+").replace(Ou,"%23").replace(vy,"%26").replace(Sy,"`").replace(Du,"{").replace(Nu,"}").replace(Pu,"^")}function Oy(e){return ni(e).replace(wy,"%3D")}function Ty(e){return Ii(e).replace(Ou,"%23").replace(_y,"%3F")}function Py(e){return Ty(e).replace(by,"%2F")}function Ws(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}const Dy=/\/$/,Ny=e=>e.replace(Dy,"");function Ao(e,t,n="/"){let s,r={},o="",i="";const a=t.indexOf("#");let l=t.indexOf("?");return l=a>=0&&l>a?-1:l,l>=0&&(s=t.slice(0,l),o=t.slice(l,a>0?a:t.length),r=e(o.slice(1))),a>=0&&(s=s||t.slice(0,a),i=t.slice(a,t.length)),s=My(s??t,n),{fullPath:s+o+i,path:s,query:r,hash:Ws(i)}}function Iy(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function tl(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function Ly(e,t,n){const s=t.matched.length-1,r=n.matched.length-1;return s>-1&&s===r&&hs(t.matched[s],n.matched[r])&&Iu(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function hs(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Iu(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!Fy(e[n],t[n]))return!1;return!0}function Fy(e,t){return Tt(e)?nl(e,t):Tt(t)?nl(t,e):(e==null?void 0:e.valueOf())===(t==null?void 0:t.valueOf())}function nl(e,t){return Tt(t)?e.length===t.length&&e.every((n,s)=>n===t[s]):e.length===1&&e[0]===t}function My(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),s=e.split("/"),r=s[s.length-1];(r===".."||r===".")&&s.push("");let o=n.length-1,i,a;for(i=0;i0)return;if(Os){let t=Os;for(Os=void 0;t;){const n=t.next;t.next=void 0,t.flags&=-9,t=n}}let e;for(;As;){let t=As;for(As=void 0;t;){const n=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(s){e||(e=s)}t=n}}if(e)throw e}function ql(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Vl(e){let t,n=e.depsTail,s=n;for(;s;){const r=s.prevDep;s.version===-1?(s===n&&(n=r),hi(s),Xu(s)):t=s,s.dep.activeLink=s.prevActiveLink,s.prevActiveLink=void 0,s=r}e.deps=t,e.depsTail=n}function Po(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&($l(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function $l(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===Ms)||(e.globalVersion=Ms,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!Po(e))))return;e.flags|=2;const t=e.dep,n=Ce,s=Rt;Ce=e,Rt=!0;try{ql(e);const r=e.fn(e._value);(t.version===0||jt(r,e._value))&&(e.flags|=128,e._value=r,t.version++)}catch(r){throw t.version++,r}finally{Ce=n,Rt=s,Vl(e),e.flags&=-3}}function hi(e,t=!1){const{dep:n,prevSub:s,nextSub:r}=e;if(s&&(s.nextSub=r,e.prevSub=void 0),r&&(r.prevSub=s,e.nextSub=void 0),n.subs===e&&(n.subs=s,!s&&n.computed)){n.computed.flags&=-5;for(let o=n.computed.deps;o;o=o.nextDep)hi(o,!0)}!t&&!--n.sc&&n.map&&n.map.delete(n.key)}function Xu(e){const{prevDep:t,nextDep:n}=e;t&&(t.nextDep=n,e.prevDep=void 0),n&&(n.prevDep=t,e.nextDep=void 0)}let Rt=!0;const Kl=[];function Xt(){Kl.push(Rt),Rt=!1}function Zt(){const e=Kl.pop();Rt=e===void 0?!0:e}function Vi(e){const{cleanup:t}=e;if(e.cleanup=void 0,t){const n=Ce;Ce=void 0;try{t()}finally{Ce=n}}}let Ms=0;class Zu{constructor(t,n){this.sub=t,this.dep=n,this.version=n.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class pi{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(t){if(!Ce||!Rt||Ce===this.computed)return;let n=this.activeLink;if(n===void 0||n.sub!==Ce)n=this.activeLink=new Zu(Ce,this),Ce.deps?(n.prevDep=Ce.depsTail,Ce.depsTail.nextDep=n,Ce.depsTail=n):Ce.deps=Ce.depsTail=n,Ql(n);else if(n.version===-1&&(n.version=this.version,n.nextDep)){const s=n.nextDep;s.prevDep=n.prevDep,n.prevDep&&(n.prevDep.nextDep=s),n.prevDep=Ce.depsTail,n.nextDep=void 0,Ce.depsTail.nextDep=n,Ce.depsTail=n,Ce.deps===n&&(Ce.deps=s)}return n}trigger(t){this.version++,Ms++,this.notify(t)}notify(t){fi();try{for(let n=this.subs;n;n=n.prevSub)n.sub.notify()&&n.sub.dep.notify()}finally{di()}}}function Ql(e){if(e.dep.sc++,e.sub.flags&4){const t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let s=t.deps;s;s=s.nextDep)Ql(s)}const n=e.dep.subs;n!==e&&(e.prevSub=n,n&&(n.nextSub=e)),e.dep.subs=e}}const Er=new WeakMap,Bn=Symbol(""),Do=Symbol(""),ks=Symbol("");function Ge(e,t,n){if(Rt&&Ce){let s=Er.get(e);s||Er.set(e,s=new Map);let r=s.get(n);r||(s.set(n,r=new pi),r.map=s,r.key=n),r.track()}}function Gt(e,t,n,s,r,o){const i=Er.get(e);if(!i){Ms++;return}const a=l=>{l&&l.trigger()};if(fi(),t==="clear")i.forEach(a);else{const l=Z(e),u=l&&Mr(n);if(l&&n==="length"){const c=Number(s);i.forEach((f,h)=>{(h==="length"||h===ks||!gt(h)&&h>=c)&&a(f)})}else switch((n!==void 0||i.has(void 0))&&a(i.get(n)),u&&a(i.get(ks)),t){case"add":l?u&&a(i.get("length")):(a(i.get(Bn)),Xn(e)&&a(i.get(Do)));break;case"delete":l||(a(i.get(Bn)),Xn(e)&&a(i.get(Do)));break;case"set":Xn(e)&&a(i.get(Bn));break}}di()}function ef(e,t){const n=Er.get(e);return n&&n.get(t)}function $n(e){const t=ye(e);return t===e?t:(Ge(t,"iterate",ks),mt(e)?t:t.map(At))}function Ur(e){return Ge(e=ye(e),"iterate",ks),e}function kt(e,t){return en(e)?fs(Yt(e)?At(t):t):At(t)}const tf={__proto__:null,[Symbol.iterator](){return fo(this,Symbol.iterator,e=>kt(this,e))},concat(...e){return $n(this).concat(...e.map(t=>Z(t)?$n(t):t))},entries(){return fo(this,"entries",e=>(e[1]=kt(this,e[1]),e))},every(e,t){return qt(this,"every",e,t,void 0,arguments)},filter(e,t){return qt(this,"filter",e,t,n=>n.map(s=>kt(this,s)),arguments)},find(e,t){return qt(this,"find",e,t,n=>kt(this,n),arguments)},findIndex(e,t){return qt(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return qt(this,"findLast",e,t,n=>kt(this,n),arguments)},findLastIndex(e,t){return qt(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return qt(this,"forEach",e,t,void 0,arguments)},includes(...e){return ho(this,"includes",e)},indexOf(...e){return ho(this,"indexOf",e)},join(e){return $n(this).join(e)},lastIndexOf(...e){return ho(this,"lastIndexOf",e)},map(e,t){return qt(this,"map",e,t,void 0,arguments)},pop(){return gs(this,"pop")},push(...e){return gs(this,"push",e)},reduce(e,...t){return $i(this,"reduce",e,t)},reduceRight(e,...t){return $i(this,"reduceRight",e,t)},shift(){return gs(this,"shift")},some(e,t){return qt(this,"some",e,t,void 0,arguments)},splice(...e){return gs(this,"splice",e)},toReversed(){return $n(this).toReversed()},toSorted(e){return $n(this).toSorted(e)},toSpliced(...e){return $n(this).toSpliced(...e)},unshift(...e){return gs(this,"unshift",e)},values(){return fo(this,"values",e=>kt(this,e))}};function fo(e,t,n){const s=Ur(e),r=s[t]();return s!==e&&!mt(e)&&(r._next=r.next,r.next=()=>{const o=r._next();return o.done||(o.value=n(o.value)),o}),r}const nf=Array.prototype;function qt(e,t,n,s,r,o){const i=Ur(e),a=i!==e&&!mt(e),l=i[t];if(l!==nf[t]){const f=l.apply(e,o);return a?At(f):f}let u=n;i!==e&&(a?u=function(f,h){return n.call(this,kt(e,f),h,e)}:n.length>2&&(u=function(f,h){return n.call(this,f,h,e)}));const c=l.call(i,u,s);return a&&r?r(c):c}function $i(e,t,n,s){const r=Ur(e),o=r!==e&&!mt(e);let i=n,a=!1;r!==e&&(o?(a=s.length===0,i=function(u,c,f){return a&&(a=!1,u=kt(e,u)),n.call(this,u,kt(e,c),f,e)}):n.length>3&&(i=function(u,c,f){return n.call(this,u,c,f,e)}));const l=r[t](i,...s);return a?kt(e,l):l}function ho(e,t,n){const s=ye(e);Ge(s,"iterate",ks);const r=s[t](...n);return(r===-1||r===!1)&&Hr(n[0])?(n[0]=ye(n[0]),s[t](...n)):r}function gs(e,t,n=[]){Xt(),fi();const s=ye(e)[t].apply(e,n);return di(),Zt(),s}const sf=ai("__proto__,__v_isRef,__isVue"),zl=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(gt));function rf(e){gt(e)||(e=String(e));const t=ye(this);return Ge(t,"has",e),t.hasOwnProperty(e)}class Wl{constructor(t=!1,n=!1){this._isReadonly=t,this._isShallow=n}get(t,n,s){if(n==="__v_skip")return t.__v_skip;const r=this._isReadonly,o=this._isShallow;if(n==="__v_isReactive")return!r;if(n==="__v_isReadonly")return r;if(n==="__v_isShallow")return o;if(n==="__v_raw")return s===(r?o?mf:Xl:o?Yl:Jl).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(s)?t:void 0;const i=Z(t);if(!r){let l;if(i&&(l=tf[n]))return l;if(n==="hasOwnProperty")return rf}const a=Reflect.get(t,n,Pe(t)?t:s);if((gt(n)?zl.has(n):sf(n))||(r||Ge(t,"get",n),o))return a;if(Pe(a)){const l=i&&Mr(n)?a:a.value;return r&&xe(l)?Io(l):l}return xe(a)?r?Io(a):Zs(a):a}}class Gl extends Wl{constructor(t=!1){super(!1,t)}set(t,n,s,r){let o=t[n];const i=Z(t)&&Mr(n);if(!this._isShallow){const u=en(o);if(!mt(s)&&!en(s)&&(o=ye(o),s=ye(s)),!i&&Pe(o)&&!Pe(s))return u||(o.value=s),!0}const a=i?Number(n)$1');/^###? (.+)/.test(de)?(W&&(F.push(""),W=!1),F.push(`'),W=!0),F.push(`
"),W=!1),F.push('')):(W&&(F.push(""),W=!1),F.push(`'),W=!0),F.push(`