feat: AI assistant widget, design system upgrade via 21st-dev Magic

Backend:
- Add AI assistant service with gap detection, anomaly analysis, Anthropic tool_use streaming
- Add assistant router (chat SSE, history, flags CRUD, session categorization)
- Fix agentic loop: text+tool_use in single assistant message per Anthropic spec
- Migrate logging from stdlib to structlog in assistant modules
- Fix migration 0005: UUID type for ai_flags/assistant_messages FKs

Frontend:
- Fix vite base path → /cc-dashboard/static/ to match FastAPI StaticFiles mount
- Redesign Sidebar: gradient background, amber gradient active state, 44px touch targets, user avatar
- Redesign KpiCard: corner decorations, ring border, trend icon, baseline bar (21st.dev pattern)
- Redesign TopBar: backdrop-blur, sticky, gradient user avatar, sign-out button
- Improve AssistantWidget: fix setInterval leak, aria-labels, proper markdown block parser
- Fix AssistantWidget renderMarkdown: line-by-line parser for correct list/header nesting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-06 19:34:17 +01:00
parent 2118187c76
commit 162f4ba822
42 changed files with 362 additions and 209 deletions

63
AGENTS.md Normal file
View file

@ -0,0 +1,63 @@
# CC Dashboard — Project Entry Point
<!-- SCOPE: root | owner: agent | generated: 2026-04-29 -->
## 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
<!-- END SCOPE: root -->

View file

@ -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/"

View file

@ -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)

View file

@ -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"

View file

@ -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};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
import{c as a}from"./utils-CFgQVuqB.js";import{d as n,o,c as s,p as d,i,v as c}from"./index-DRwzmrLE.js";const f=n({__name:"Badge",props:{variant:{default:"default"},class:{}},setup(r){const e=r;return(t,l)=>(o(),s("span",{class:d(i(a)("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors",{"bg-primary text-primary-foreground":e.variant==="default","bg-secondary text-secondary-foreground":e.variant==="secondary","bg-destructive text-destructive-foreground":e.variant==="destructive","border border-border text-foreground":e.variant==="outline","bg-emerald-500/20 text-emerald-400":e.variant==="success","bg-amber-500/20 text-amber-400":e.variant==="warning"},e.class))},[c(t.$slots,"default")],2))}});export{f as _};

View file

@ -0,0 +1 @@
import{c as a}from"./utils-DWBfPysr.js";import{d as n,o as s,c as o,p as d,i,s as c}from"./index-ZkX-rg-0.js";const f=n({__name:"Badge",props:{variant:{default:"default"},class:{}},setup(r){const e=r;return(t,l)=>(s(),o("span",{class:d(i(a)("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors",{"bg-primary text-primary-foreground":e.variant==="default","bg-secondary text-secondary-foreground":e.variant==="secondary","bg-destructive text-destructive-foreground":e.variant==="destructive","border border-border text-foreground":e.variant==="outline","bg-emerald-500/20 text-emerald-400":e.variant==="success","bg-amber-500/20 text-amber-400":e.variant==="warning"},e.class))},[c(t.$slots,"default")],2))}});export{f as _};

View file

@ -0,0 +1 @@
import{c,_ as l}from"./utils-DWBfPysr.js";import{d as u,c as f,p as m,n as b,j as v,s as g,m as p,o as n}from"./index-ZkX-rg-0.js";const y=["type","disabled"],k=u({__name:"Button",props:{variant:{default:"default"},size:{default:"md"},loading:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},type:{default:"button"},class:{}},emits:["click"],setup(t,{emit:o}){const e=t,a=o,r=p(()=>c("inline-flex items-center justify-center rounded-md font-medium transition-colors","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2","disabled:pointer-events-none disabled:opacity-50",{"bg-primary text-primary-foreground hover:bg-primary/90":e.variant==="default","border border-input bg-background hover:bg-accent hover:text-accent-foreground":e.variant==="outline","hover:bg-accent hover:text-accent-foreground":e.variant==="ghost","bg-destructive text-destructive-foreground hover:bg-destructive/90":e.variant==="destructive","bg-secondary text-secondary-foreground hover:bg-secondary/80":e.variant==="secondary","underline-offset-4 hover:underline text-primary":e.variant==="link","h-8 px-3 text-xs":e.size==="sm","h-10 px-4 py-2 text-sm":e.size==="md","h-11 px-8 text-base":e.size==="lg","h-9 w-9 p-0":e.size==="icon"},e.class));return(i,s)=>(n(),f("button",{class:m(r.value),type:t.type,disabled:t.disabled||t.loading,onClick:s[0]||(s[0]=d=>a("click",d))},[t.loading?(n(),b(l,{key:0,size:"sm",class:"mr-2"})):v("",!0),g(i.$slots,"default")],10,y))}});export{k as _};

View file

@ -1 +0,0 @@
import{c,_ as l}from"./utils-CFgQVuqB.js";import{d as u,c as f,p as m,n as v,j as b,v as g,m as p,o}from"./index-DRwzmrLE.js";const y=["type","disabled"],k=u({__name:"Button",props:{variant:{default:"default"},size:{default:"md"},loading:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},type:{default:"button"},class:{}},emits:["click"],setup(t,{emit:s}){const e=t,a=s,r=p(()=>c("inline-flex items-center justify-center rounded-md font-medium transition-colors","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2","disabled:pointer-events-none disabled:opacity-50",{"bg-primary text-primary-foreground hover:bg-primary/90":e.variant==="default","border border-input bg-background hover:bg-accent hover:text-accent-foreground":e.variant==="outline","hover:bg-accent hover:text-accent-foreground":e.variant==="ghost","bg-destructive text-destructive-foreground hover:bg-destructive/90":e.variant==="destructive","bg-secondary text-secondary-foreground hover:bg-secondary/80":e.variant==="secondary","underline-offset-4 hover:underline text-primary":e.variant==="link","h-8 px-3 text-xs":e.size==="sm","h-10 px-4 py-2 text-sm":e.size==="md","h-11 px-8 text-base":e.size==="lg","h-9 w-9 p-0":e.size==="icon"},e.class));return(i,n)=>(o(),f("button",{class:m(r.value),type:t.type,disabled:t.disabled||t.loading,onClick:n[0]||(n[0]=d=>a("click",d))},[t.loading?(o(),v(l,{key:0,size:"sm",class:"mr-2"})):b("",!0),g(i.$slots,"default")],10,y))}});export{k as _};

View file

@ -1 +1 @@
import{c as e}from"./utils-CFgQVuqB.js";import{d as o,c as t,p as n,i as c,v as p,o as l}from"./index-DRwzmrLE.js";const _=o({__name:"Card",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),t("div",{class:n(c(e)("rounded-lg border bg-card text-card-foreground shadow-sm",a.class))},[p(r.$slots,"default")],2))}}),f=o({__name:"CardContent",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),t("div",{class:n(c(e)("p-6 pt-0",a.class))},[p(r.$slots,"default")],2))}});export{_,f as a};
import{c as e}from"./utils-DWBfPysr.js";import{d as o,c as t,p as n,i as c,s as p,o as l}from"./index-ZkX-rg-0.js";const _=o({__name:"Card",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),t("div",{class:n(c(e)("rounded-lg border bg-card text-card-foreground shadow-sm",a.class))},[p(r.$slots,"default")],2))}}),f=o({__name:"CardContent",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),t("div",{class:n(c(e)("p-6 pt-0",a.class))},[p(r.$slots,"default")],2))}});export{_,f as a};

View file

@ -1 +1 @@
import{c as t}from"./utils-CFgQVuqB.js";import{d as o,o as n,c as r,p as c,i as l,v as p}from"./index-DRwzmrLE.js";const f=o({__name:"CardHeader",props:{class:{}},setup(s){const e=s;return(a,i)=>(n(),r("div",{class:c(l(t)("flex flex-col space-y-1.5 p-6",e.class))},[p(a.$slots,"default")],2))}}),_=o({__name:"CardTitle",props:{class:{}},setup(s){const e=s;return(a,i)=>(n(),r("h3",{class:c(l(t)("text-lg font-semibold leading-none tracking-tight",e.class))},[p(a.$slots,"default")],2))}});export{f as _,_ as a};
import{c as t}from"./utils-DWBfPysr.js";import{d as o,o as n,c as r,p as c,i as l,s as p}from"./index-ZkX-rg-0.js";const f=o({__name:"CardHeader",props:{class:{}},setup(s){const e=s;return(a,i)=>(n(),r("div",{class:c(l(t)("flex flex-col space-y-1.5 p-6",e.class))},[p(a.$slots,"default")],2))}}),_=o({__name:"CardTitle",props:{class:{}},setup(s){const e=s;return(a,i)=>(n(),r("h3",{class:c(l(t)("text-lg font-semibold leading-none tracking-tight",e.class))},[p(a.$slots,"default")],2))}});export{f as _,_ as a};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
import{d as y,y as k,E as b,n as h,G as g,e as c,T as x,w as u,o as a,c as n,a as o,v as r,t as m,j as i,p as w}from"./index-DRwzmrLE.js";import{_ as $}from"./Button.vue_vue_type_script_setup_true_lang-DdcUxSmn.js";const C={key:0,class:"fixed inset-0 z-50 flex items-center justify-center p-4"},B=["aria-label"],j={key:0,class:"flex items-center justify-between p-6 pb-4"},E={class:"text-lg font-semibold text-foreground"},z={key:0,class:"text-sm text-muted-foreground mt-1"},L={class:"px-6 pb-4"},M={key:1,class:"flex justify-end gap-2 px-6 pb-6"},V=y({__name:"Dialog",props:{open:{type:Boolean},title:{},description:{},maxWidth:{default:"max-w-lg"}},emits:["close"],setup(e,{emit:f}){const p=e,l=f;function d(t){t.key==="Escape"&&p.open&&l("close")}return k(()=>document.addEventListener("keydown",d)),b(()=>document.removeEventListener("keydown",d)),(t,s)=>(a(),h(g,{to:"body"},[c(x,{"enter-active-class":"transition-opacity duration-200","enter-from-class":"opacity-0","enter-to-class":"opacity-100","leave-active-class":"transition-opacity duration-200","leave-from-class":"opacity-100","leave-to-class":"opacity-0"},{default:u(()=>[e.open?(a(),n("div",C,[o("div",{class:"absolute inset-0 bg-black/60 backdrop-blur-sm",onClick:s[0]||(s[0]=v=>l("close"))}),o("div",{class:w(["relative w-full bg-card border border-border rounded-lg shadow-xl z-10",e.maxWidth]),role:"dialog","aria-modal":!0,"aria-label":e.title},[e.title||t.$slots.header?(a(),n("div",j,[o("div",null,[r(t.$slots,"header",{},()=>[o("h2",E,m(e.title),1),e.description?(a(),n("p",z,m(e.description),1)):i("",!0)])]),c($,{variant:"ghost",size:"icon",class:"shrink-0",onClick:s[1]||(s[1]=v=>l("close"))},{default:u(()=>[...s[2]||(s[2]=[o("svg",{class:"h-4 w-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[o("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)])]),_:1})])):i("",!0),o("div",L,[r(t.$slots,"default")]),t.$slots.footer?(a(),n("div",M,[r(t.$slots,"footer")])):i("",!0)],10,B)])):i("",!0)]),_:3})]))}});export{V as _};
import{d as y,x as k,E as b,n as h,G as x,e as c,T as g,w as u,o as a,c as n,a as o,s as r,t as m,j as i,p as w}from"./index-ZkX-rg-0.js";import{_ as $}from"./Button.vue_vue_type_script_setup_true_lang-CeodyRvV.js";const C={key:0,class:"fixed inset-0 z-50 flex items-center justify-center p-4"},B=["aria-label"],j={key:0,class:"flex items-center justify-between p-6 pb-4"},E={class:"text-lg font-semibold text-foreground"},z={key:0,class:"text-sm text-muted-foreground mt-1"},L={class:"px-6 pb-4"},M={key:1,class:"flex justify-end gap-2 px-6 pb-6"},V=y({__name:"Dialog",props:{open:{type:Boolean},title:{},description:{},maxWidth:{default:"max-w-lg"}},emits:["close"],setup(e,{emit:f}){const p=e,l=f;function d(t){t.key==="Escape"&&p.open&&l("close")}return k(()=>document.addEventListener("keydown",d)),b(()=>document.removeEventListener("keydown",d)),(t,s)=>(a(),h(x,{to:"body"},[c(g,{"enter-active-class":"transition-opacity duration-200","enter-from-class":"opacity-0","enter-to-class":"opacity-100","leave-active-class":"transition-opacity duration-200","leave-from-class":"opacity-100","leave-to-class":"opacity-0"},{default:u(()=>[e.open?(a(),n("div",C,[o("div",{class:"absolute inset-0 bg-black/60 backdrop-blur-sm",onClick:s[0]||(s[0]=v=>l("close"))}),o("div",{class:w(["relative w-full bg-card border border-border rounded-lg shadow-xl z-10",e.maxWidth]),role:"dialog","aria-modal":!0,"aria-label":e.title},[e.title||t.$slots.header?(a(),n("div",j,[o("div",null,[r(t.$slots,"header",{},()=>[o("h2",E,m(e.title),1),e.description?(a(),n("p",z,m(e.description),1)):i("",!0)])]),c($,{variant:"ghost",size:"icon",class:"shrink-0",onClick:s[1]||(s[1]=v=>l("close"))},{default:u(()=>[...s[2]||(s[2]=[o("svg",{class:"h-4 w-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[o("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)])]),_:1})])):i("",!0),o("div",L,[r(t.$slots,"default")]),t.$slots.footer?(a(),n("div",M,[r(t.$slots,"footer")])):i("",!0)],10,B)])):i("",!0)]),_:3})]))}});export{V as _};

View file

@ -1 +1 @@
import{c as i}from"./utils-CFgQVuqB.js";import{d,c as s,p as u,i as m,o as r}from"./index-DRwzmrLE.js";const c=["id","name","type","value","placeholder","disabled","autocomplete","min","max","step"],g=d({__name:"Input",props:{modelValue:{},type:{},placeholder:{},disabled:{type:Boolean},class:{},id:{},name:{},autocomplete:{},min:{},max:{},step:{}},emits:["update:modelValue","change","blur","focus"],setup(e,{emit:a}){const n=e,o=a;return(f,t)=>(r(),s("input",{id:e.id,name:e.name,type:e.type??"text",value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,autocomplete:e.autocomplete,min:e.min,max:e.max,step:e.step,class:u(m(i)("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm","ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium","placeholder:text-muted-foreground","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2","disabled:cursor-not-allowed disabled:opacity-50",n.class)),onInput:t[0]||(t[0]=l=>o("update:modelValue",l.target.value)),onChange:t[1]||(t[1]=l=>o("change",l.target.value)),onBlur:t[2]||(t[2]=l=>o("blur",l)),onFocus:t[3]||(t[3]=l=>o("focus",l))},null,42,c))}});export{g as _};
import{c as i}from"./utils-DWBfPysr.js";import{d,c as s,p as u,i as m,o as r}from"./index-ZkX-rg-0.js";const c=["id","name","type","value","placeholder","disabled","autocomplete","min","max","step"],g=d({__name:"Input",props:{modelValue:{},type:{},placeholder:{},disabled:{type:Boolean},class:{},id:{},name:{},autocomplete:{},min:{},max:{},step:{}},emits:["update:modelValue","change","blur","focus"],setup(e,{emit:a}){const n=e,o=a;return(f,t)=>(r(),s("input",{id:e.id,name:e.name,type:e.type??"text",value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,autocomplete:e.autocomplete,min:e.min,max:e.max,step:e.step,class:u(m(i)("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm","ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium","placeholder:text-muted-foreground","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2","disabled:cursor-not-allowed disabled:opacity-50",n.class)),onInput:t[0]||(t[0]=l=>o("update:modelValue",l.target.value)),onChange:t[1]||(t[1]=l=>o("change",l.target.value)),onBlur:t[2]||(t[2]=l=>o("blur",l)),onFocus:t[3]||(t[3]=l=>o("focus",l))},null,42,c))}});export{g as _};

View file

@ -1 +1 @@
import{a as b}from"./admin-DItGP2ar.js";import{_ as K,a as $}from"./CardContent.vue_vue_type_script_setup_true_lang-Dhtfm4bR.js";import{_ as v}from"./Button.vue_vue_type_script_setup_true_lang-DdcUxSmn.js";import{_ as V}from"./Dialog.vue_vue_type_script_setup_true_lang-B-pK_nBZ.js";import{_ as N}from"./Input.vue_vue_type_script_setup_true_lang-CR2nSoFe.js";import{_ as A,a as k}from"./utils-CFgQVuqB.js";import{d as B,y as L,c as l,a as t,e as r,w as n,r as i,o as a,k as p,F as P,l as j,t as u,i as h,n as F,j as I,K as y}from"./index-DRwzmrLE.js";const D={class:"p-6"},R={class:"flex items-center justify-between mb-6"},z={key:0,class:"flex items-center justify-center h-20"},M={key:1,class:"text-center text-muted-foreground py-8 text-sm"},T={key:2,class:"w-full"},U={class:"px-4 py-3 text-sm text-foreground"},E={class:"px-4 py-3 text-sm font-mono text-muted-foreground"},H={class:"px-4 py-3 text-xs text-muted-foreground"},S={class:"px-4 py-3 text-xs text-muted-foreground"},q={class:"px-4 py-3 text-right"},G={class:"space-y-4"},J={key:0,class:"rounded-md bg-emerald-500/10 border border-emerald-500/30 p-3"},O={class:"text-xs font-mono text-foreground break-all"},Q={key:1,class:"space-y-1.5"},le=B({__name:"KeysView",setup(W){const f=i([]),_=i(!1),c=i(!1),m=i(""),x=i(!1),d=i(null);L(()=>g());async function g(){_.value=!0;try{const o=await b.keys();f.value=o.data}finally{_.value=!1}}async function w(){if(m.value.trim()){x.value=!0;try{const o=await b.createKey({label:m.value});d.value=o.data.key,y.success("API key created"),await g(),m.value=""}catch{y.error("Failed to create key")}finally{x.value=!1}}}async function C(o){if(confirm(`Revoke key "${o.label}"? This cannot be undone.`))try{await b.revokeKey(o.id),y.success("Key revoked"),f.value=f.value.filter(e=>e.id!==o.id)}catch{y.error("Failed to revoke key")}}return(o,e)=>(a(),l("div",D,[t("div",R,[e[5]||(e[5]=t("h2",{class:"text-lg font-semibold text-foreground"},"API Keys",-1)),r(v,{size:"sm",onClick:e[0]||(e[0]=s=>{c.value=!0,d.value=null})},{default:n(()=>[...e[4]||(e[4]=[t("svg",{class:"h-4 w-4 mr-1.5",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 4v16m8-8H4"})],-1),p(" New Key ",-1)])]),_:1})]),r(K,null,{default:n(()=>[r($,{class:"p-0"},{default:n(()=>[_.value?(a(),l("div",z,[r(A,{class:"text-primary"})])):f.value.length===0?(a(),l("div",M," No API keys ")):(a(),l("table",T,[e[7]||(e[7]=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"},"Label"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Prefix"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Created"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Last Used"),t("th",{class:"px-4 py-3"})])],-1)),t("tbody",null,[(a(!0),l(P,null,j(f.value,s=>(a(),l("tr",{key:s.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",U,u(s.label),1),t("td",E,u(s.prefix)+"...",1),t("td",H,u(h(k)(s.created_at)),1),t("td",S,u(s.last_used?h(k)(s.last_used):"Never"),1),t("td",q,[r(v,{variant:"ghost",size:"sm",class:"text-destructive",onClick:X=>C(s)},{default:n(()=>[...e[6]||(e[6]=[p(" Revoke ",-1)])]),_:1},8,["onClick"])])]))),128))])]))]),_:1})]),_:1}),r(V,{open:c.value,title:"Create API Key",onClose:e[3]||(e[3]=s=>c.value=!1)},{footer:n(()=>[r(v,{variant:"outline",onClick:e[2]||(e[2]=s=>c.value=!1)},{default:n(()=>[p(u(d.value?"Done":"Cancel"),1)]),_:1}),d.value?I("",!0):(a(),F(v,{key:0,loading:x.value,onClick:w},{default:n(()=>[...e[10]||(e[10]=[p(" Create ",-1)])]),_:1},8,["loading"]))]),default:n(()=>[t("div",G,[d.value?(a(),l("div",J,[e[8]||(e[8]=t("p",{class:"text-xs text-emerald-400 font-medium mb-1"},"Key created — save it now!",-1)),t("p",O,u(d.value),1)])):(a(),l("div",Q,[e[9]||(e[9]=t("label",{class:"text-sm font-medium text-foreground"},"Label",-1)),r(N,{modelValue:m.value,"onUpdate:modelValue":e[1]||(e[1]=s=>m.value=s),placeholder:"e.g. claude-collector",disabled:x.value},null,8,["modelValue","disabled"])]))])]),_:1},8,["open"])]))}});export{le as default};
import{a as b}from"./admin-D88IFW1N.js";import{_ as K,a as $}from"./CardContent.vue_vue_type_script_setup_true_lang-DGh5KRxz.js";import{_ as v}from"./Button.vue_vue_type_script_setup_true_lang-CeodyRvV.js";import{_ as V}from"./Dialog.vue_vue_type_script_setup_true_lang-BilK24Vk.js";import{_ as N}from"./Input.vue_vue_type_script_setup_true_lang-BzTd5oOE.js";import{_ as A,a as k}from"./utils-DWBfPysr.js";import{d as B,x as L,c as l,a as t,e as r,w as n,r as i,o as a,k as p,F as P,l as j,t as u,i as h,n as F,j as I,K as y}from"./index-ZkX-rg-0.js";const D={class:"p-6"},R={class:"flex items-center justify-between mb-6"},z={key:0,class:"flex items-center justify-center h-20"},M={key:1,class:"text-center text-muted-foreground py-8 text-sm"},T={key:2,class:"w-full"},U={class:"px-4 py-3 text-sm text-foreground"},E={class:"px-4 py-3 text-sm font-mono text-muted-foreground"},H={class:"px-4 py-3 text-xs text-muted-foreground"},S={class:"px-4 py-3 text-xs text-muted-foreground"},q={class:"px-4 py-3 text-right"},G={class:"space-y-4"},J={key:0,class:"rounded-md bg-emerald-500/10 border border-emerald-500/30 p-3"},O={class:"text-xs font-mono text-foreground break-all"},Q={key:1,class:"space-y-1.5"},le=B({__name:"KeysView",setup(W){const f=i([]),_=i(!1),c=i(!1),m=i(""),x=i(!1),d=i(null);L(()=>g());async function g(){_.value=!0;try{const o=await b.keys();f.value=o.data}finally{_.value=!1}}async function w(){if(m.value.trim()){x.value=!0;try{const o=await b.createKey({label:m.value});d.value=o.data.key,y.success("API key created"),await g(),m.value=""}catch{y.error("Failed to create key")}finally{x.value=!1}}}async function C(o){if(confirm(`Revoke key "${o.label}"? This cannot be undone.`))try{await b.revokeKey(o.id),y.success("Key revoked"),f.value=f.value.filter(e=>e.id!==o.id)}catch{y.error("Failed to revoke key")}}return(o,e)=>(a(),l("div",D,[t("div",R,[e[5]||(e[5]=t("h2",{class:"text-lg font-semibold text-foreground"},"API Keys",-1)),r(v,{size:"sm",onClick:e[0]||(e[0]=s=>{c.value=!0,d.value=null})},{default:n(()=>[...e[4]||(e[4]=[t("svg",{class:"h-4 w-4 mr-1.5",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 4v16m8-8H4"})],-1),p(" New Key ",-1)])]),_:1})]),r(K,null,{default:n(()=>[r($,{class:"p-0"},{default:n(()=>[_.value?(a(),l("div",z,[r(A,{class:"text-primary"})])):f.value.length===0?(a(),l("div",M," No API keys ")):(a(),l("table",T,[e[7]||(e[7]=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"},"Label"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Prefix"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Created"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Last Used"),t("th",{class:"px-4 py-3"})])],-1)),t("tbody",null,[(a(!0),l(P,null,j(f.value,s=>(a(),l("tr",{key:s.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",U,u(s.label),1),t("td",E,u(s.prefix)+"...",1),t("td",H,u(h(k)(s.created_at)),1),t("td",S,u(s.last_used?h(k)(s.last_used):"Never"),1),t("td",q,[r(v,{variant:"ghost",size:"sm",class:"text-destructive",onClick:X=>C(s)},{default:n(()=>[...e[6]||(e[6]=[p(" Revoke ",-1)])]),_:1},8,["onClick"])])]))),128))])]))]),_:1})]),_:1}),r(V,{open:c.value,title:"Create API Key",onClose:e[3]||(e[3]=s=>c.value=!1)},{footer:n(()=>[r(v,{variant:"outline",onClick:e[2]||(e[2]=s=>c.value=!1)},{default:n(()=>[p(u(d.value?"Done":"Cancel"),1)]),_:1}),d.value?I("",!0):(a(),F(v,{key:0,loading:x.value,onClick:w},{default:n(()=>[...e[10]||(e[10]=[p(" Create ",-1)])]),_:1},8,["loading"]))]),default:n(()=>[t("div",G,[d.value?(a(),l("div",J,[e[8]||(e[8]=t("p",{class:"text-xs text-emerald-400 font-medium mb-1"},"Key created — save it now!",-1)),t("p",O,u(d.value),1)])):(a(),l("div",Q,[e[9]||(e[9]=t("label",{class:"text-sm font-medium text-foreground"},"Label",-1)),r(N,{modelValue:m.value,"onUpdate:modelValue":e[1]||(e[1]=s=>m.value=s),placeholder:"e.g. claude-collector",disabled:x.value},null,8,["modelValue","disabled"])]))])]),_:1},8,["open"])]))}});export{le as default};

View file

@ -1 +1 @@
import{E as j,r as g,d as L,u as J,y as O,c as d,a as r,p as b,i,t as v,n as T,w as x,j as k,e as E,o as l,k as S,F as V,l as B,m as F}from"./index-DRwzmrLE.js";import{_ as z,a as A}from"./CardContent.vue_vue_type_script_setup_true_lang-Dhtfm4bR.js";import{_ as w}from"./Button.vue_vue_type_script_setup_true_lang-DdcUxSmn.js";import"./utils-CFgQVuqB.js";function D(C){const e=g([]),f=g(!1),o=g(null);let s=null,u=null,m=!1;function p(){if(!m)try{s=new EventSource(C),s.onopen=()=>{f.value=!0,o.value=null},s.onmessage=n=>{try{const h=JSON.parse(n.data);e.value.push({type:"message",data:h}),e.value.length>200&&e.value.shift()}catch{e.value.push({type:"message",data:n.data})}},s.addEventListener("session_start",n=>{try{e.value.push({type:"session_start",data:JSON.parse(n.data)})}catch{e.value.push({type:"session_start",data:n.data})}e.value.length>200&&e.value.shift()}),s.addEventListener("session_end",n=>{try{e.value.push({type:"session_end",data:JSON.parse(n.data)})}catch{e.value.push({type:"session_end",data:n.data})}e.value.length>200&&e.value.shift()}),s.addEventListener("activity",n=>{try{e.value.push({type:"activity",data:JSON.parse(n.data)})}catch{e.value.push({type:"activity",data:n.data})}e.value.length>200&&e.value.shift()}),s.onerror=()=>{f.value=!1,o.value="Connection lost, reconnecting...",s==null||s.close(),s=null,m||(u=setTimeout(()=>p(),5e3))}}catch{o.value="Failed to connect to event stream",m||(u=setTimeout(()=>p(),5e3))}}function _(){m=!0,u&&clearTimeout(u),s==null||s.close(),s=null,f.value=!1}function y(){e.value=[]}return j(()=>{_()}),{events:e,connected:f,error:o,connect:p,disconnect:_,clearEvents:y}}const $={class:"p-6 h-full flex flex-col"},I={class:"flex items-center gap-3 mb-4"},M={class:"flex items-center gap-2"},P={class:"text-xs text-muted-foreground"},R={key:0,class:"mb-4 text-xs text-amber-400 bg-amber-500/10 border border-amber-500/30 rounded px-3 py-2"},U={key:0,class:"flex items-center justify-center h-full text-sm text-muted-foreground"},W={key:1,class:"overflow-y-auto h-full font-mono text-xs"},q={class:"flex-1 min-w-0"},G={class:"flex items-center gap-2 flex-wrap"},H={key:0,class:"text-muted-foreground"},K={class:"text-muted-foreground truncate mt-0.5"},ee=L({__name:"LiveView",setup(C){const e=J(),{events:f,connected:o,error:s,connect:u,clearEvents:m}=D("/cc-dashboard/api/events/stream");O(()=>{e.isAuthenticated&&u()});const p=F(()=>[...f.value].reverse().slice(0,100));function _(t){return t==="session_start"?"text-emerald-400":t==="session_end"?"text-amber-400":t==="activity"?"text-blue-400":"text-muted-foreground"}function y(t){return t==="session_start"?"▶":t==="session_end"?"■":t==="activity"?"●":"○"}function n(t){if(typeof t=="string")return t;if(t&&typeof t=="object"){const a=t;return a.message||a.summary||JSON.stringify(t)}return String(t)}function h(t){if(t&&typeof t=="object"){const a=t;return a.display_name||a.project_id||""}return""}return(t,a)=>(l(),d("div",$,[r("div",I,[a[2]||(a[2]=r("h2",{class:"text-lg font-semibold text-foreground flex-1"},"Live Feed",-1)),r("div",M,[r("div",{class:b(["h-2 w-2 rounded-full",i(o)?"bg-emerald-500 animate-pulse":"bg-red-500"])},null,2),r("span",P,v(i(o)?"Connected":"Disconnected"),1)]),i(o)?k("",!0):(l(),T(w,{key:0,variant:"outline",size:"sm",onClick:i(u)},{default:x(()=>[...a[0]||(a[0]=[S(" Reconnect ",-1)])]),_:1},8,["onClick"])),E(w,{variant:"ghost",size:"sm",onClick:i(m)},{default:x(()=>[...a[1]||(a[1]=[S(" Clear ",-1)])]),_:1},8,["onClick"])]),i(s)&&!i(o)?(l(),d("div",R,v(i(s)),1)):k("",!0),E(z,{class:"flex-1 overflow-hidden"},{default:x(()=>[E(A,{class:"p-0 h-full"},{default:x(()=>[p.value.length===0?(l(),d("div",U,[...a[3]||(a[3]=[r("div",{class:"text-center"},[r("div",{class:"text-2xl mb-2"},"📡"),r("p",null,"Waiting for events..."),r("p",{class:"text-xs mt-1"},"Activity will appear here in real-time")],-1)])])):(l(),d("div",W,[(l(!0),d(V,null,B(p.value,(c,N)=>(l(),d("div",{key:N,class:"flex items-start gap-2 px-4 py-1.5 hover:bg-muted/50 border-b border-border/30"},[r("span",{class:b([_(c.type),"shrink-0 mt-0.5"])},v(y(c.type)),3),r("div",q,[r("div",G,[r("span",{class:b([_(c.type),"font-medium"])},v(c.type),3),h(c.data)?(l(),d("span",H,v(h(c.data)),1)):k("",!0)]),r("p",K,v(n(c.data)),1)])]))),128))]))]),_:1})]),_:1})]))}});export{ee as default};
import{E as j,r as g,d as L,u as J,x as O,c as d,a as r,p as b,i,t as v,n as T,w as x,j as k,e as E,o as l,k as S,F as V,l as B,m as F}from"./index-ZkX-rg-0.js";import{_ as z,a as A}from"./CardContent.vue_vue_type_script_setup_true_lang-DGh5KRxz.js";import{_ as w}from"./Button.vue_vue_type_script_setup_true_lang-CeodyRvV.js";import"./utils-DWBfPysr.js";function D(C){const e=g([]),f=g(!1),o=g(null);let s=null,u=null,m=!1;function p(){if(!m)try{s=new EventSource(C),s.onopen=()=>{f.value=!0,o.value=null},s.onmessage=n=>{try{const h=JSON.parse(n.data);e.value.push({type:"message",data:h}),e.value.length>200&&e.value.shift()}catch{e.value.push({type:"message",data:n.data})}},s.addEventListener("session_start",n=>{try{e.value.push({type:"session_start",data:JSON.parse(n.data)})}catch{e.value.push({type:"session_start",data:n.data})}e.value.length>200&&e.value.shift()}),s.addEventListener("session_end",n=>{try{e.value.push({type:"session_end",data:JSON.parse(n.data)})}catch{e.value.push({type:"session_end",data:n.data})}e.value.length>200&&e.value.shift()}),s.addEventListener("activity",n=>{try{e.value.push({type:"activity",data:JSON.parse(n.data)})}catch{e.value.push({type:"activity",data:n.data})}e.value.length>200&&e.value.shift()}),s.onerror=()=>{f.value=!1,o.value="Connection lost, reconnecting...",s==null||s.close(),s=null,m||(u=setTimeout(()=>p(),5e3))}}catch{o.value="Failed to connect to event stream",m||(u=setTimeout(()=>p(),5e3))}}function _(){m=!0,u&&clearTimeout(u),s==null||s.close(),s=null,f.value=!1}function y(){e.value=[]}return j(()=>{_()}),{events:e,connected:f,error:o,connect:p,disconnect:_,clearEvents:y}}const $={class:"p-6 h-full flex flex-col"},I={class:"flex items-center gap-3 mb-4"},M={class:"flex items-center gap-2"},P={class:"text-xs text-muted-foreground"},R={key:0,class:"mb-4 text-xs text-amber-400 bg-amber-500/10 border border-amber-500/30 rounded px-3 py-2"},U={key:0,class:"flex items-center justify-center h-full text-sm text-muted-foreground"},W={key:1,class:"overflow-y-auto h-full font-mono text-xs"},q={class:"flex-1 min-w-0"},G={class:"flex items-center gap-2 flex-wrap"},H={key:0,class:"text-muted-foreground"},K={class:"text-muted-foreground truncate mt-0.5"},ee=L({__name:"LiveView",setup(C){const e=J(),{events:f,connected:o,error:s,connect:u,clearEvents:m}=D("/cc-dashboard/api/events/stream");O(()=>{e.isAuthenticated&&u()});const p=F(()=>[...f.value].reverse().slice(0,100));function _(t){return t==="session_start"?"text-emerald-400":t==="session_end"?"text-amber-400":t==="activity"?"text-blue-400":"text-muted-foreground"}function y(t){return t==="session_start"?"▶":t==="session_end"?"■":t==="activity"?"●":"○"}function n(t){if(typeof t=="string")return t;if(t&&typeof t=="object"){const a=t;return a.message||a.summary||JSON.stringify(t)}return String(t)}function h(t){if(t&&typeof t=="object"){const a=t;return a.display_name||a.project_id||""}return""}return(t,a)=>(l(),d("div",$,[r("div",I,[a[2]||(a[2]=r("h2",{class:"text-lg font-semibold text-foreground flex-1"},"Live Feed",-1)),r("div",M,[r("div",{class:b(["h-2 w-2 rounded-full",i(o)?"bg-emerald-500 animate-pulse":"bg-red-500"])},null,2),r("span",P,v(i(o)?"Connected":"Disconnected"),1)]),i(o)?k("",!0):(l(),T(w,{key:0,variant:"outline",size:"sm",onClick:i(u)},{default:x(()=>[...a[0]||(a[0]=[S(" Reconnect ",-1)])]),_:1},8,["onClick"])),E(w,{variant:"ghost",size:"sm",onClick:i(m)},{default:x(()=>[...a[1]||(a[1]=[S(" Clear ",-1)])]),_:1},8,["onClick"])]),i(s)&&!i(o)?(l(),d("div",R,v(i(s)),1)):k("",!0),E(z,{class:"flex-1 overflow-hidden"},{default:x(()=>[E(A,{class:"p-0 h-full"},{default:x(()=>[p.value.length===0?(l(),d("div",U,[...a[3]||(a[3]=[r("div",{class:"text-center"},[r("div",{class:"text-2xl mb-2"},"📡"),r("p",null,"Waiting for events..."),r("p",{class:"text-xs mt-1"},"Activity will appear here in real-time")],-1)])])):(l(),d("div",W,[(l(!0),d(V,null,B(p.value,(c,N)=>(l(),d("div",{key:N,class:"flex items-start gap-2 px-4 py-1.5 hover:bg-muted/50 border-b border-border/30"},[r("span",{class:b([_(c.type),"shrink-0 mt-0.5"])},v(y(c.type)),3),r("div",q,[r("div",G,[r("span",{class:b([_(c.type),"font-medium"])},v(c.type),3),h(c.data)?(l(),d("span",H,v(h(c.data)),1)):k("",!0)]),r("p",K,v(n(c.data)),1)])]))),128))]))]),_:1})]),_:1})]))}});export{ee as default};

View file

@ -1 +1 @@
import{d as g,u as b,c as u,a as s,b as _,e as a,w as i,o as m,f as h,g as w,h as y,i as o,t as V,j as k,k as C,r as c}from"./index-DRwzmrLE.js";import{_ as S}from"./Button.vue_vue_type_script_setup_true_lang-DdcUxSmn.js";import{_ as p}from"./Input.vue_vue_type_script_setup_true_lang-CR2nSoFe.js";import{_ as N,a as j}from"./CardContent.vue_vue_type_script_setup_true_lang-Dhtfm4bR.js";import"./utils-CFgQVuqB.js";const B={class:"min-h-screen flex items-center justify-center bg-background p-4"},$={class:"w-full max-w-sm"},q={key:0,class:"rounded-md bg-destructive/10 border border-destructive/30 px-3 py-2 text-sm text-destructive"},z={class:"space-y-1.5"},D={class:"space-y-1.5"},U=g({__name:"LoginView",setup(E){const f=h(),v=w(),t=b(),r=c(""),l=c("");async function x(){try{await t.login(r.value,l.value);const n=v.query.redirect;f.push(n??"/")}catch{}}return(n,e)=>(m(),u("div",B,[s("div",$,[e[5]||(e[5]=_('<div class="text-center mb-8"><div class="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-primary mb-3"><svg class="h-7 w-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg></div><h1 class="text-2xl font-bold text-foreground">CC Dashboard</h1><p class="text-sm text-muted-foreground mt-1">Corporate Planning Hub</p></div>',1)),a(N,null,{default:i(()=>[a(j,{class:"pt-6"},{default:i(()=>[s("form",{class:"space-y-4",onSubmit:y(x,["prevent"])},[o(t).error?(m(),u("div",q,V(o(t).error),1)):k("",!0),s("div",z,[e[2]||(e[2]=s("label",{for:"email",class:"text-sm font-medium text-foreground"},"Email",-1)),a(p,{id:"email",modelValue:r.value,"onUpdate:modelValue":e[0]||(e[0]=d=>r.value=d),type:"email",placeholder:"you@company.com",autocomplete:"email",disabled:o(t).loading,required:""},null,8,["modelValue","disabled"])]),s("div",D,[e[3]||(e[3]=s("label",{for:"password",class:"text-sm font-medium text-foreground"},"Password",-1)),a(p,{id:"password",modelValue:l.value,"onUpdate:modelValue":e[1]||(e[1]=d=>l.value=d),type:"password",placeholder:"••••••••",autocomplete:"current-password",disabled:o(t).loading,required:""},null,8,["modelValue","disabled"])]),a(S,{type:"submit",class:"w-full",loading:o(t).loading},{default:i(()=>[...e[4]||(e[4]=[C(" Sign in ",-1)])]),_:1},8,["loading"])],32)]),_:1})]),_:1})])]))}});export{U as default};
import{d as g,u as b,c as u,a as s,b as _,e as a,w as i,o as m,f as h,g as w,h as y,i as o,t as V,j as k,k as C,r as c}from"./index-ZkX-rg-0.js";import{_ as S}from"./Button.vue_vue_type_script_setup_true_lang-CeodyRvV.js";import{_ as p}from"./Input.vue_vue_type_script_setup_true_lang-BzTd5oOE.js";import{_ as N,a as j}from"./CardContent.vue_vue_type_script_setup_true_lang-DGh5KRxz.js";import"./utils-DWBfPysr.js";const B={class:"min-h-screen flex items-center justify-center bg-background p-4"},$={class:"w-full max-w-sm"},q={key:0,class:"rounded-md bg-destructive/10 border border-destructive/30 px-3 py-2 text-sm text-destructive"},z={class:"space-y-1.5"},D={class:"space-y-1.5"},U=g({__name:"LoginView",setup(E){const f=h(),v=w(),t=b(),r=c(""),l=c("");async function x(){try{await t.login(r.value,l.value);const n=v.query.redirect;f.push(n??"/")}catch{}}return(n,e)=>(m(),u("div",B,[s("div",$,[e[5]||(e[5]=_('<div class="text-center mb-8"><div class="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-primary mb-3"><svg class="h-7 w-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg></div><h1 class="text-2xl font-bold text-foreground">CC Dashboard</h1><p class="text-sm text-muted-foreground mt-1">Corporate Planning Hub</p></div>',1)),a(N,null,{default:i(()=>[a(j,{class:"pt-6"},{default:i(()=>[s("form",{class:"space-y-4",onSubmit:y(x,["prevent"])},[o(t).error?(m(),u("div",q,V(o(t).error),1)):k("",!0),s("div",z,[e[2]||(e[2]=s("label",{for:"email",class:"text-sm font-medium text-foreground"},"Email",-1)),a(p,{id:"email",modelValue:r.value,"onUpdate:modelValue":e[0]||(e[0]=d=>r.value=d),type:"email",placeholder:"you@company.com",autocomplete:"email",disabled:o(t).loading,required:""},null,8,["modelValue","disabled"])]),s("div",D,[e[3]||(e[3]=s("label",{for:"password",class:"text-sm font-medium text-foreground"},"Password",-1)),a(p,{id:"password",modelValue:l.value,"onUpdate:modelValue":e[1]||(e[1]=d=>l.value=d),type:"password",placeholder:"••••••••",autocomplete:"current-password",disabled:o(t).loading,required:""},null,8,["modelValue","disabled"])]),a(S,{type:"submit",class:"w-full",loading:o(t).loading},{default:i(()=>[...e[4]||(e[4]=[C(" Sign in ",-1)])]),_:1},8,["loading"])],32)]),_:1})]),_:1})])]))}});export{U as default};

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
import{c as r}from"./utils-CFgQVuqB.js";import{d as s,o as n,c as t,p as l,i as c,a as d,s as u}from"./index-DRwzmrLE.js";const h=s({__name:"Progress",props:{value:{},max:{default:100},class:{},color:{default:"default"}},setup(a){const e=a,o=()=>Math.min(100,Math.max(0,e.value/e.max*100));return(i,m)=>(n(),t("div",{class:l(c(r)("relative h-2 w-full overflow-hidden rounded-full bg-secondary",e.class))},[d("div",{class:l(["h-full rounded-full transition-all duration-300",{"bg-primary":a.color==="default","bg-emerald-500":a.color==="success","bg-amber-500":a.color==="warning","bg-red-500":a.color==="danger"}]),style:u({width:`${o()}%`})},null,6)],2))}});export{h as _};
import{c as r}from"./utils-DWBfPysr.js";import{d as s,o as n,c as t,p as l,i as c,a as d,A as u}from"./index-ZkX-rg-0.js";const h=s({__name:"Progress",props:{value:{},max:{default:100},class:{},color:{default:"default"}},setup(a){const e=a,o=()=>Math.min(100,Math.max(0,e.value/e.max*100));return(i,m)=>(n(),t("div",{class:l(c(r)("relative h-2 w-full overflow-hidden rounded-full bg-secondary",e.class))},[d("div",{class:l(["h-full rounded-full transition-all duration-300",{"bg-primary":a.color==="default","bg-emerald-500":a.color==="success","bg-amber-500":a.color==="warning","bg-red-500":a.color==="danger"}]),style:u({width:`${o()}%`})},null,6)],2))}});export{h as _};

View file

@ -1 +1 @@
import{d as $,y as N,c as t,e as l,F as u,a as o,t as n,j as c,i as _,w as r,g as D,r as b,o as e,k as m,l as f,s as k}from"./index-DRwzmrLE.js";import{d as T}from"./dashboard-Btm3piK7.js";import{_ as x,a as p}from"./CardContent.vue_vue_type_script_setup_true_lang-Dhtfm4bR.js";import{_ as h,a as v}from"./CardTitle.vue_vue_type_script_setup_true_lang-BDTVk4qm.js";import{f as y,_ as V,b as F}from"./utils-CFgQVuqB.js";const B={class:"p-6"},C={key:0,class:"flex items-center justify-center h-40"},R={class:"mb-6"},S={class:"flex items-start justify-between gap-4 flex-wrap"},z={class:"text-xl font-bold text-foreground"},A={class:"flex items-center gap-3 mt-1 flex-wrap"},M={key:0,class:"text-sm text-muted-foreground"},P={key:1,class:"text-xs bg-muted text-muted-foreground px-2 py-1 rounded"},E=["href"],H={class:"text-right"},I={class:"text-2xl font-bold text-foreground"},L={class:"h-32 flex items-end gap-px"},U=["title"],q={class:"grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"},G={key:0,class:"text-sm text-muted-foreground"},J={key:1,class:"space-y-1.5"},K=["title"],O={class:"text-foreground shrink-0 ml-2"},Q={key:0,class:"text-sm text-muted-foreground"},W={key:1,class:"space-y-2"},X={class:"text-xs text-foreground w-24 truncate shrink-0"},Y={class:"flex-1 h-2 bg-secondary rounded-full overflow-hidden"},Z={class:"text-xs text-muted-foreground w-8 text-right shrink-0"},tt={key:0,class:"text-sm text-muted-foreground"},et={key:1,class:"space-y-2"},st={class:"flex-1 min-w-0"},ot={class:"text-xs text-foreground"},at={key:0,class:"text-xs text-muted-foreground mt-0.5 line-clamp-2"},lt={class:"text-right shrink-0"},rt={class:"text-xs font-medium text-foreground"},dt={class:"text-xs text-muted-foreground"},nt={key:2,class:"text-center text-muted-foreground py-12"},pt=$({__name:"ProjectDetailView",setup(it){const w=D().params.id,a=b(null),g=b(!1);N(async()=>{g.value=!0;try{const i=await T.project(w);a.value=i.data}finally{g.value=!1}});const j=()=>{var i;return Math.max(...((i=a.value)==null?void 0:i.timeline.map(d=>d.hours))??[1],1)};return(i,d)=>(e(),t("div",B,[g.value?(e(),t("div",C,[l(V,{size:"lg",class:"text-primary"})])):a.value?(e(),t(u,{key:1},[o("div",R,[o("div",S,[o("div",null,[o("h2",z,n(a.value.display_name),1),o("div",A,[a.value.client?(e(),t("span",M,n(a.value.client),1)):c("",!0),a.value.job_number?(e(),t("span",P,n(a.value.job_number),1)):c("",!0),a.value.repo_url?(e(),t("a",{key:2,href:a.value.repo_url,target:"_blank",class:"text-xs text-primary hover:underline"}," Repository → ",8,E)):c("",!0)])]),o("div",H,[o("p",I,n(_(y)(a.value.total_hours)),1),d[0]||(d[0]=o("p",{class:"text-xs text-muted-foreground"},"total hours",-1))])])]),l(x,{class:"mb-6"},{default:r(()=>[l(h,{class:"pb-2"},{default:r(()=>[l(v,{class:"text-sm"},{default:r(()=>[...d[1]||(d[1]=[m("Daily Activity",-1)])]),_:1})]),_:1}),l(p,null,{default:r(()=>[o("div",L,[(e(!0),t(u,null,f(a.value.timeline,s=>(e(),t("div",{key:s.date,class:"flex-1 bg-primary/70 hover:bg-primary rounded-t transition-colors",style:k({height:`${s.hours/j()*100}%`}),title:`${s.date}: ${_(y)(s.hours)}`},null,12,U))),128))])]),_:1})]),_:1}),o("div",q,[l(x,null,{default:r(()=>[l(h,{class:"pb-2"},{default:r(()=>[l(v,{class:"text-sm"},{default:r(()=>[...d[2]||(d[2]=[m("Top Files",-1)])]),_:1})]),_:1}),l(p,null,{default:r(()=>[a.value.top_files.length?(e(),t("div",J,[(e(!0),t(u,null,f(a.value.top_files.slice(0,10),s=>(e(),t("div",{key:s.path,class:"flex items-center justify-between text-xs"},[o("span",{class:"text-muted-foreground truncate max-w-[200px]",title:s.path},n(s.path.split("/").pop()),9,K),o("span",O,n(s.count)+"×",1)]))),128))])):(e(),t("div",G,"No data"))]),_:1})]),_:1}),l(x,null,{default:r(()=>[l(h,{class:"pb-2"},{default:r(()=>[l(v,{class:"text-sm"},{default:r(()=>[...d[3]||(d[3]=[m("Tool Usage",-1)])]),_:1})]),_:1}),l(p,null,{default:r(()=>[a.value.top_tools.length?(e(),t("div",W,[(e(!0),t(u,null,f(a.value.top_tools.slice(0,8),s=>(e(),t("div",{key:s.tool,class:"flex items-center gap-2"},[o("span",X,n(s.tool),1),o("div",Y,[o("div",{class:"h-full bg-primary rounded-full",style:k({width:`${s.pct}%`})},null,4)]),o("span",Z,n(s.pct.toFixed(0))+"% ",1)]))),128))])):(e(),t("div",Q,"No data"))]),_:1})]),_:1})]),l(x,null,{default:r(()=>[l(h,{class:"pb-2"},{default:r(()=>[l(v,{class:"text-sm"},{default:r(()=>[...d[4]||(d[4]=[m("Recent Sessions",-1)])]),_:1})]),_:1}),l(p,null,{default:r(()=>[a.value.sessions.length?(e(),t("div",et,[(e(!0),t(u,null,f(a.value.sessions.slice(0,50),s=>(e(),t("div",{key:s.id,class:"flex items-start gap-3 py-2 border-b border-border last:border-0"},[o("div",st,[o("p",ot,n(_(F)(s.start_at)),1),s.summary?(e(),t("p",at,n(s.summary),1)):c("",!0)]),o("div",lt,[o("p",rt,n(_(y)(s.duration_hours)),1),o("p",dt,n(s.commit_count)+" commits ",1)])]))),128))])):(e(),t("div",tt,"No sessions"))]),_:1})]),_:1})],64)):(e(),t("div",nt," Project not found "))]))}});export{pt as default};
import{d as $,x as N,c as t,e as l,F as u,a as o,t as n,j as c,i as _,w as r,g as D,r as b,o as e,k as m,l as x,A as k}from"./index-ZkX-rg-0.js";import{d as T}from"./dashboard-C1cvUjRU.js";import{_ as f,a as p}from"./CardContent.vue_vue_type_script_setup_true_lang-DGh5KRxz.js";import{_ as h,a as v}from"./CardTitle.vue_vue_type_script_setup_true_lang-CpwJW48B.js";import{f as y,_ as V,b as F}from"./utils-DWBfPysr.js";const A={class:"p-6"},B={key:0,class:"flex items-center justify-center h-40"},C={class:"mb-6"},R={class:"flex items-start justify-between gap-4 flex-wrap"},S={class:"text-xl font-bold text-foreground"},z={class:"flex items-center gap-3 mt-1 flex-wrap"},M={key:0,class:"text-sm text-muted-foreground"},P={key:1,class:"text-xs bg-muted text-muted-foreground px-2 py-1 rounded"},E=["href"],H={class:"text-right"},I={class:"text-2xl font-bold text-foreground"},L={class:"h-32 flex items-end gap-px"},U=["title"],q={class:"grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"},G={key:0,class:"text-sm text-muted-foreground"},J={key:1,class:"space-y-1.5"},K=["title"],O={class:"text-foreground shrink-0 ml-2"},Q={key:0,class:"text-sm text-muted-foreground"},W={key:1,class:"space-y-2"},X={class:"text-xs text-foreground w-24 truncate shrink-0"},Y={class:"flex-1 h-2 bg-secondary rounded-full overflow-hidden"},Z={class:"text-xs text-muted-foreground w-8 text-right shrink-0"},tt={key:0,class:"text-sm text-muted-foreground"},et={key:1,class:"space-y-2"},st={class:"flex-1 min-w-0"},ot={class:"text-xs text-foreground"},at={key:0,class:"text-xs text-muted-foreground mt-0.5 line-clamp-2"},lt={class:"text-right shrink-0"},rt={class:"text-xs font-medium text-foreground"},dt={class:"text-xs text-muted-foreground"},nt={key:2,class:"text-center text-muted-foreground py-12"},pt=$({__name:"ProjectDetailView",setup(it){const w=D().params.id,a=b(null),g=b(!1);N(async()=>{g.value=!0;try{const i=await T.project(w);a.value=i.data}finally{g.value=!1}});const j=()=>{var i;return Math.max(...((i=a.value)==null?void 0:i.timeline.map(d=>d.hours))??[1],1)};return(i,d)=>(e(),t("div",A,[g.value?(e(),t("div",B,[l(V,{size:"lg",class:"text-primary"})])):a.value?(e(),t(u,{key:1},[o("div",C,[o("div",R,[o("div",null,[o("h2",S,n(a.value.display_name),1),o("div",z,[a.value.client?(e(),t("span",M,n(a.value.client),1)):c("",!0),a.value.job_number?(e(),t("span",P,n(a.value.job_number),1)):c("",!0),a.value.repo_url?(e(),t("a",{key:2,href:a.value.repo_url,target:"_blank",class:"text-xs text-primary hover:underline"}," Repository → ",8,E)):c("",!0)])]),o("div",H,[o("p",I,n(_(y)(a.value.total_hours)),1),d[0]||(d[0]=o("p",{class:"text-xs text-muted-foreground"},"total hours",-1))])])]),l(f,{class:"mb-6"},{default:r(()=>[l(h,{class:"pb-2"},{default:r(()=>[l(v,{class:"text-sm"},{default:r(()=>[...d[1]||(d[1]=[m("Daily Activity",-1)])]),_:1})]),_:1}),l(p,null,{default:r(()=>[o("div",L,[(e(!0),t(u,null,x(a.value.timeline,s=>(e(),t("div",{key:s.date,class:"flex-1 bg-primary/70 hover:bg-primary rounded-t transition-colors",style:k({height:`${s.hours/j()*100}%`}),title:`${s.date}: ${_(y)(s.hours)}`},null,12,U))),128))])]),_:1})]),_:1}),o("div",q,[l(f,null,{default:r(()=>[l(h,{class:"pb-2"},{default:r(()=>[l(v,{class:"text-sm"},{default:r(()=>[...d[2]||(d[2]=[m("Top Files",-1)])]),_:1})]),_:1}),l(p,null,{default:r(()=>[a.value.top_files.length?(e(),t("div",J,[(e(!0),t(u,null,x(a.value.top_files.slice(0,10),s=>(e(),t("div",{key:s.path,class:"flex items-center justify-between text-xs"},[o("span",{class:"text-muted-foreground truncate max-w-[200px]",title:s.path},n(s.path.split("/").pop()),9,K),o("span",O,n(s.count)+"×",1)]))),128))])):(e(),t("div",G,"No data"))]),_:1})]),_:1}),l(f,null,{default:r(()=>[l(h,{class:"pb-2"},{default:r(()=>[l(v,{class:"text-sm"},{default:r(()=>[...d[3]||(d[3]=[m("Tool Usage",-1)])]),_:1})]),_:1}),l(p,null,{default:r(()=>[a.value.top_tools.length?(e(),t("div",W,[(e(!0),t(u,null,x(a.value.top_tools.slice(0,8),s=>(e(),t("div",{key:s.tool,class:"flex items-center gap-2"},[o("span",X,n(s.tool),1),o("div",Y,[o("div",{class:"h-full bg-primary rounded-full",style:k({width:`${s.pct}%`})},null,4)]),o("span",Z,n(s.pct.toFixed(0))+"% ",1)]))),128))])):(e(),t("div",Q,"No data"))]),_:1})]),_:1})]),l(f,null,{default:r(()=>[l(h,{class:"pb-2"},{default:r(()=>[l(v,{class:"text-sm"},{default:r(()=>[...d[4]||(d[4]=[m("Recent Sessions",-1)])]),_:1})]),_:1}),l(p,null,{default:r(()=>[a.value.sessions.length?(e(),t("div",et,[(e(!0),t(u,null,x(a.value.sessions.slice(0,50),s=>(e(),t("div",{key:s.id,class:"flex items-start gap-3 py-2 border-b border-border last:border-0"},[o("div",st,[o("p",ot,n(_(F)(s.start_at)),1),s.summary?(e(),t("p",at,n(s.summary),1)):c("",!0)]),o("div",lt,[o("p",rt,n(_(y)(s.duration_hours)),1),o("p",dt,n(s.commit_count)+" commits ",1)])]))),128))])):(e(),t("div",tt,"No sessions"))]),_:1})]),_:1})],64)):(e(),t("div",nt," Project not found "))]))}});export{pt as default};

View file

@ -1 +1 @@
import{d as p,y as g,c as r,a as s,e as d,F as v,l as y,r as _,o,n as h,w as f,t as a,j as i,i as u,p as b,f as k}from"./index-DRwzmrLE.js";import{d as w}from"./dashboard-Btm3piK7.js";import{a as C,_ as $}from"./CardContent.vue_vue_type_script_setup_true_lang-Dhtfm4bR.js";import{_ as B}from"./Progress.vue_vue_type_script_setup_true_lang-B3vZXDIA.js";import{_ as N,f as V,a as D}from"./utils-CFgQVuqB.js";const F={class:"p-6"},j={key:0,class:"flex items-center justify-center h-40"},z={key:1,class:"text-center text-muted-foreground py-12"},L={key:2,class:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"},P={class:"flex items-start justify-between gap-2 mb-3"},S={class:"min-w-0"},A={class:"font-semibold text-sm text-foreground truncate"},E={key:0,class:"text-xs text-muted-foreground truncate"},M={key:0,class:"text-xs bg-muted text-muted-foreground px-1.5 py-0.5 rounded shrink-0"},R={class:"space-y-1.5"},T={class:"flex items-center justify-between text-xs"},q={class:"font-medium text-foreground"},G={class:"flex items-center justify-between text-xs"},H={class:"text-foreground"},I={key:0,class:"flex items-center justify-between text-xs"},J={class:"text-foreground"},K={key:0,class:"mt-3"},O={class:"flex items-center justify-between text-xs mb-1"},st=p({__name:"ProjectsView",setup(Q){const m=k(),l=_([]),c=_(!1);g(async()=>{c.value=!0;try{const n=await w.projects({});l.value=n.data.sort((e,t)=>t.total_hours-e.total_hours)}finally{c.value=!1}});const x=n=>n?n>90?"danger":n>70?"warning":"success":"default";return(n,e)=>(o(),r("div",F,[e[4]||(e[4]=s("h2",{class:"text-lg font-semibold text-foreground mb-6"},"Projects",-1)),c.value?(o(),r("div",j,[d(N,{size:"lg",class:"text-primary"})])):l.value.length===0?(o(),r("div",z," No projects found ")):(o(),r("div",L,[(o(!0),r(v,null,y(l.value,t=>(o(),h($,{key:t.project_id,class:"cursor-pointer hover:border-primary/50 transition-colors",onClick:U=>u(m).push(`/projects/${t.project_id}`)},{default:f(()=>[d(C,{class:"p-4"},{default:f(()=>[s("div",P,[s("div",S,[s("p",A,a(t.display_name),1),t.client?(o(),r("p",E,a(t.client),1)):i("",!0)]),t.job_number?(o(),r("span",M,a(t.job_number),1)):i("",!0)]),s("div",R,[s("div",T,[e[0]||(e[0]=s("span",{class:"text-muted-foreground"},"Total hours",-1)),s("span",q,a(u(V)(t.total_hours)),1)]),s("div",G,[e[1]||(e[1]=s("span",{class:"text-muted-foreground"},"Sessions",-1)),s("span",H,a(t.session_count),1)]),t.last_active?(o(),r("div",I,[e[2]||(e[2]=s("span",{class:"text-muted-foreground"},"Last active",-1)),s("span",J,a(u(D)(t.last_active)),1)])):i("",!0)]),t.progress_pct!==null?(o(),r("div",K,[s("div",O,[e[3]||(e[3]=s("span",{class:"text-muted-foreground"},"Budget",-1)),s("span",{class:b(t.progress_pct>90?"text-red-400":"text-muted-foreground")},a(t.progress_pct.toFixed(0))+"% ",3)]),d(B,{value:t.progress_pct,color:x(t.progress_pct)},null,8,["value","color"])])):i("",!0)]),_:2},1024)]),_:2},1032,["onClick"]))),128))]))]))}});export{st as default};
import{d as p,x as g,c as r,a as s,e as d,F as v,l as y,r as _,o,n as h,w as f,t as a,j as i,i as u,p as b,f as k}from"./index-ZkX-rg-0.js";import{d as w}from"./dashboard-C1cvUjRU.js";import{a as C,_ as $}from"./CardContent.vue_vue_type_script_setup_true_lang-DGh5KRxz.js";import{_ as B}from"./Progress.vue_vue_type_script_setup_true_lang-CWTZuml2.js";import{_ as N,f as V,a as D}from"./utils-DWBfPysr.js";const F={class:"p-6"},j={key:0,class:"flex items-center justify-center h-40"},z={key:1,class:"text-center text-muted-foreground py-12"},L={key:2,class:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"},P={class:"flex items-start justify-between gap-2 mb-3"},S={class:"min-w-0"},A={class:"font-semibold text-sm text-foreground truncate"},E={key:0,class:"text-xs text-muted-foreground truncate"},M={key:0,class:"text-xs bg-muted text-muted-foreground px-1.5 py-0.5 rounded shrink-0"},R={class:"space-y-1.5"},T={class:"flex items-center justify-between text-xs"},q={class:"font-medium text-foreground"},G={class:"flex items-center justify-between text-xs"},H={class:"text-foreground"},I={key:0,class:"flex items-center justify-between text-xs"},J={class:"text-foreground"},K={key:0,class:"mt-3"},O={class:"flex items-center justify-between text-xs mb-1"},st=p({__name:"ProjectsView",setup(Q){const m=k(),l=_([]),c=_(!1);g(async()=>{c.value=!0;try{const n=await w.projects({});l.value=n.data.sort((e,t)=>t.total_hours-e.total_hours)}finally{c.value=!1}});const x=n=>n?n>90?"danger":n>70?"warning":"success":"default";return(n,e)=>(o(),r("div",F,[e[4]||(e[4]=s("h2",{class:"text-lg font-semibold text-foreground mb-6"},"Projects",-1)),c.value?(o(),r("div",j,[d(N,{size:"lg",class:"text-primary"})])):l.value.length===0?(o(),r("div",z," No projects found ")):(o(),r("div",L,[(o(!0),r(v,null,y(l.value,t=>(o(),h($,{key:t.project_id,class:"cursor-pointer hover:border-primary/50 transition-colors",onClick:U=>u(m).push(`/projects/${t.project_id}`)},{default:f(()=>[d(C,{class:"p-4"},{default:f(()=>[s("div",P,[s("div",S,[s("p",A,a(t.display_name),1),t.client?(o(),r("p",E,a(t.client),1)):i("",!0)]),t.job_number?(o(),r("span",M,a(t.job_number),1)):i("",!0)]),s("div",R,[s("div",T,[e[0]||(e[0]=s("span",{class:"text-muted-foreground"},"Total hours",-1)),s("span",q,a(u(V)(t.total_hours)),1)]),s("div",G,[e[1]||(e[1]=s("span",{class:"text-muted-foreground"},"Sessions",-1)),s("span",H,a(t.session_count),1)]),t.last_active?(o(),r("div",I,[e[2]||(e[2]=s("span",{class:"text-muted-foreground"},"Last active",-1)),s("span",J,a(u(D)(t.last_active)),1)])):i("",!0)]),t.progress_pct!==null?(o(),r("div",K,[s("div",O,[e[3]||(e[3]=s("span",{class:"text-muted-foreground"},"Budget",-1)),s("span",{class:b(t.progress_pct>90?"text-red-400":"text-muted-foreground")},a(t.progress_pct.toFixed(0))+"% ",3)]),d(B,{value:t.progress_pct,color:x(t.progress_pct)},null,8,["value","color"])])):i("",!0)]),_:2},1024)]),_:2},1032,["onClick"]))),128))]))]))}});export{st as default};

View file

@ -1,4 +1,4 @@
var Ce=Object.defineProperty;var ae=a=>{throw TypeError(a)};var Ee=(a,t,e)=>t in a?Ce(a,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[t]=e;var k=(a,t,e)=>Ee(a,typeof t!="symbol"?t+"":t,e),Le=(a,t,e)=>t.has(a)||ae("Cannot "+e);var ce=(a,t,e)=>t.has(a)?ae("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(a):t.set(a,e);var Z=(a,t,e)=>(Le(a,t,"access private method"),e);import{D as V,d as Be,y as qe,c as z,a as x,p as U,e as P,w as I,F as Ze,l as Pe,r as A,o as $,k as G,n as pe,t as W,i as De,j as he,K as ue,_ as Me}from"./index-DRwzmrLE.js";import{a as Qe,_ as je}from"./CardContent.vue_vue_type_script_setup_true_lang-Dhtfm4bR.js";import{_ as fe}from"./Badge.vue_vue_type_script_setup_true_lang-CKuGC1QO.js";import{_ as Ne}from"./Button.vue_vue_type_script_setup_true_lang-DdcUxSmn.js";import{_ as Oe,a as He,i as Fe}from"./utils-CFgQVuqB.js";const ge={list:()=>V.get("/api/reports"),get:a=>V.get(`/api/reports/${a}`),generate:a=>V.post("/api/reports/generate",a)};function J(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}let S=J();function we(a){S=a}const ye=/[&<>"']/,Ve=new RegExp(ye.source,"g"),$e=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,Ue=new RegExp($e.source,"g"),Ge={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},de=a=>Ge[a];function m(a,t){if(t){if(ye.test(a))return a.replace(Ve,de)}else if($e.test(a))return a.replace(Ue,de);return a}const We=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;function Xe(a){return a.replace(We,(t,e)=>(e=e.toLowerCase(),e==="colon"?":":e.charAt(0)==="#"?e.charAt(1)==="x"?String.fromCharCode(parseInt(e.substring(2),16)):String.fromCharCode(+e.substring(1)):""))}const Ke=/(^|[^\[])\^/g;function d(a,t){let e=typeof a=="string"?a:a.source;t=t||"";const n={replace:(i,r)=>{let s=typeof r=="string"?r:r.source;return s=s.replace(Ke,"$1"),e=e.replace(i,s),n},getRegex:()=>new RegExp(e,t)};return n}function ke(a){try{a=encodeURI(a).replace(/%25/g,"%")}catch{return null}return a}const E={exec:()=>null};function xe(a,t){const e=a.replace(/\|/g,(r,s,l)=>{let o=!1,u=s;for(;--u>=0&&l[u]==="\\";)o=!o;return o?"|":" |"}),n=e.split(/ \|/);let i=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length<t;)n.push("");for(;i<n.length;i++)n[i]=n[i].trim().replace(/\\\|/g,"|");return n}function D(a,t,e){const n=a.length;if(n===0)return"";let i=0;for(;i<n&&a.charAt(n-i-1)===t;)i++;return a.slice(0,n-i)}function Je(a,t){if(a.indexOf(t[1])===-1)return-1;let e=0;for(let n=0;n<a.length;n++)if(a[n]==="\\")n++;else if(a[n]===t[0])e++;else if(a[n]===t[1]&&(e--,e<0))return n;return-1}function me(a,t,e,n){const i=t.href,r=t.title?m(t.title):null,s=a[1].replace(/\\([\[\]])/g,"$1");if(a[0].charAt(0)!=="!"){n.state.inLink=!0;const l={type:"link",raw:e,href:i,title:r,text:s,tokens:n.inlineTokens(s)};return n.state.inLink=!1,l}return{type:"image",raw:e,href:i,title:r,text:m(s)}}function Ye(a,t){const e=a.match(/^(\s+)(?:```)/);if(e===null)return t;const n=e[1];return t.split(`
var Ce=Object.defineProperty;var ae=a=>{throw TypeError(a)};var Ee=(a,t,e)=>t in a?Ce(a,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[t]=e;var k=(a,t,e)=>Ee(a,typeof t!="symbol"?t+"":t,e),Le=(a,t,e)=>t.has(a)||ae("Cannot "+e);var ce=(a,t,e)=>t.has(a)?ae("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(a):t.set(a,e);var Z=(a,t,e)=>(Le(a,t,"access private method"),e);import{D as V,d as Be,x as qe,c as z,a as x,p as U,e as P,w as I,F as Ze,l as Pe,r as A,o as $,k as G,n as pe,t as W,i as De,j as he,K as ue,_ as Me}from"./index-ZkX-rg-0.js";import{a as Qe,_ as je}from"./CardContent.vue_vue_type_script_setup_true_lang-DGh5KRxz.js";import{_ as fe}from"./Badge.vue_vue_type_script_setup_true_lang-Cs8z3MtN.js";import{_ as Ne}from"./Button.vue_vue_type_script_setup_true_lang-CeodyRvV.js";import{_ as Oe,a as He,i as Fe}from"./utils-DWBfPysr.js";const ge={list:()=>V.get("/api/reports"),get:a=>V.get(`/api/reports/${a}`),generate:a=>V.post("/api/reports/generate",a)};function J(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}let S=J();function we(a){S=a}const ye=/[&<>"']/,Ve=new RegExp(ye.source,"g"),$e=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,Ue=new RegExp($e.source,"g"),Ge={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},de=a=>Ge[a];function m(a,t){if(t){if(ye.test(a))return a.replace(Ve,de)}else if($e.test(a))return a.replace(Ue,de);return a}const We=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;function Xe(a){return a.replace(We,(t,e)=>(e=e.toLowerCase(),e==="colon"?":":e.charAt(0)==="#"?e.charAt(1)==="x"?String.fromCharCode(parseInt(e.substring(2),16)):String.fromCharCode(+e.substring(1)):""))}const Ke=/(^|[^\[])\^/g;function d(a,t){let e=typeof a=="string"?a:a.source;t=t||"";const n={replace:(i,r)=>{let s=typeof r=="string"?r:r.source;return s=s.replace(Ke,"$1"),e=e.replace(i,s),n},getRegex:()=>new RegExp(e,t)};return n}function ke(a){try{a=encodeURI(a).replace(/%25/g,"%")}catch{return null}return a}const E={exec:()=>null};function xe(a,t){const e=a.replace(/\|/g,(r,s,l)=>{let o=!1,u=s;for(;--u>=0&&l[u]==="\\";)o=!o;return o?"|":" |"}),n=e.split(/ \|/);let i=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length<t;)n.push("");for(;i<n.length;i++)n[i]=n[i].trim().replace(/\\\|/g,"|");return n}function D(a,t,e){const n=a.length;if(n===0)return"";let i=0;for(;i<n&&a.charAt(n-i-1)===t;)i++;return a.slice(0,n-i)}function Je(a,t){if(a.indexOf(t[1])===-1)return-1;let e=0;for(let n=0;n<a.length;n++)if(a[n]==="\\")n++;else if(a[n]===t[0])e++;else if(a[n]===t[1]&&(e--,e<0))return n;return-1}function me(a,t,e,n){const i=t.href,r=t.title?m(t.title):null,s=a[1].replace(/\\([\[\]])/g,"$1");if(a[0].charAt(0)!=="!"){n.state.inLink=!0;const l={type:"link",raw:e,href:i,title:r,text:s,tokens:n.inlineTokens(s)};return n.state.inLink=!1,l}return{type:"image",raw:e,href:i,title:r,text:m(s)}}function Ye(a,t){const e=a.match(/^(\s+)(?:```)/);if(e===null)return t;const n=e[1];return t.split(`
`).map(i=>{const r=i.match(/^\s+/);if(r===null)return i;const[s]=r;return s.length>=n.length?i.slice(n.length):i}).join(`
`)}class Q{constructor(t){k(this,"options");k(this,"rules");k(this,"lexer");this.options=t||S}space(t){const e=this.rules.block.newline.exec(t);if(e&&e[0].length>0)return{type:"space",raw:e[0]}}code(t){const e=this.rules.block.code.exec(t);if(e){const n=e[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:e[0],codeBlockStyle:"indented",text:this.options.pedantic?n:D(n,`
`)}}}fences(t){const e=this.rules.block.fences.exec(t);if(e){const n=e[0],i=Ye(n,e[3]||"");return{type:"code",raw:n,lang:e[2]?e[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):e[2],text:i}}}heading(t){const e=this.rules.block.heading.exec(t);if(e){let n=e[2].trim();if(/#$/.test(n)){const i=D(n,"#");(this.options.pedantic||!i||/ $/.test(i))&&(n=i.trim())}return{type:"heading",raw:e[0],depth:e[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(t){const e=this.rules.block.hr.exec(t);if(e)return{type:"hr",raw:e[0]}}blockquote(t){const e=this.rules.block.blockquote.exec(t);if(e){let n=e[0].replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,`

View file

@ -1 +1 @@
import{D as e}from"./index-DRwzmrLE.js";const i={users:()=>e.get("/api/admin/users"),keys:()=>e.get("/api/keys"),createKey:s=>e.post("/api/keys",s),revokeKey:s=>e.delete(`/api/keys/${s}`)};export{i as a};
import{D as e}from"./index-ZkX-rg-0.js";const i={users:()=>e.get("/api/admin/users"),keys:()=>e.get("/api/keys"),createKey:s=>e.post("/api/keys",s),revokeKey:s=>e.delete(`/api/keys/${s}`)};export{i as a};

View file

@ -1 +1 @@
import{D as t}from"./index-DRwzmrLE.js";const e={summary:a=>t.get("/api/dashboard/summary",{params:a}),projects:a=>t.get("/api/dashboard/projects",{params:a}),timeline:a=>t.get("/api/dashboard/timeline",{params:a}),monthly:a=>t.get("/api/dashboard/monthly",{params:a}),dow:a=>t.get("/api/dashboard/dow",{params:a}),tools:a=>t.get("/api/dashboard/tools",{params:a}),activity:a=>t.get("/api/dashboard/activity",{params:a}),calendar:a=>t.get("/api/dashboard/calendar",{params:a}),project:(a,o)=>t.get("/api/dashboard/project/"+a,{params:o})};export{e as d};
import{D as t}from"./index-ZkX-rg-0.js";const e={summary:a=>t.get("/api/dashboard/summary",{params:a}),projects:a=>t.get("/api/dashboard/projects",{params:a}),timeline:a=>t.get("/api/dashboard/timeline",{params:a}),monthly:a=>t.get("/api/dashboard/monthly",{params:a}),dow:a=>t.get("/api/dashboard/dow",{params:a}),tools:a=>t.get("/api/dashboard/tools",{params:a}),activity:a=>t.get("/api/dashboard/activity",{params:a}),calendar:a=>t.get("/api/dashboard/calendar",{params:a}),project:(a,o)=>t.get("/api/dashboard/project/"+a,{params:o})};export{e as d};

View file

@ -1 +1 @@
import{D as s,B as I,r as o}from"./index-DRwzmrLE.js";const i={getIntegration:()=>s.get("/api/devops/integration"),saveIntegration:e=>s.put("/api/devops/integration",e),deleteIntegration:()=>s.delete("/api/devops/integration"),sync:()=>s.post("/api/devops/sync"),workItems:e=>s.get("/api/devops/work-items",{params:e?{state:e}:void 0})},m=I("devops",()=>{const e=o(null),r=o([]),l=o(!1),n=o(!1),c=o(null);async function u(){n.value=!0;try{const t=await i.getIntegration();e.value=t.data}catch{e.value=null}finally{n.value=!1}}async function d(t){const a=await i.saveIntegration(t);e.value=a.data}async function g(){await i.deleteIntegration(),e.value=null}async function f(){var t,a;l.value=!0,c.value=null;try{await i.sync(),await u()}catch(v){const p=v;throw c.value=((a=(t=p.response)==null?void 0:t.data)==null?void 0:a.detail)??p.message??"Sync failed",v}finally{l.value=!1}}async function y(t){n.value=!0;try{const a=await i.workItems(t);r.value=a.data}catch{r.value=[]}finally{n.value=!1}}return{integration:e,workItems:r,syncing:l,loading:n,error:c,fetchIntegration:u,saveIntegration:d,deleteIntegration:g,sync:f,fetchWorkItems:y}});export{m as u};
import{D as s,B as I,r as o}from"./index-ZkX-rg-0.js";const i={getIntegration:()=>s.get("/api/devops/integration"),saveIntegration:e=>s.put("/api/devops/integration",e),deleteIntegration:()=>s.delete("/api/devops/integration"),sync:()=>s.post("/api/devops/sync"),workItems:e=>s.get("/api/devops/work-items",{params:e?{state:e}:void 0})},m=I("devops",()=>{const e=o(null),r=o([]),l=o(!1),n=o(!1),c=o(null);async function u(){n.value=!0;try{const t=await i.getIntegration();e.value=t.data}catch{e.value=null}finally{n.value=!1}}async function d(t){const a=await i.saveIntegration(t);e.value=a.data}async function g(){await i.deleteIntegration(),e.value=null}async function f(){var t,a;l.value=!0,c.value=null;try{await i.sync(),await u()}catch(v){const p=v;throw c.value=((a=(t=p.response)==null?void 0:t.data)==null?void 0:a.detail)??p.message??"Sync failed",v}finally{l.value=!1}}async function y(t){n.value=!0;try{const a=await i.workItems(t);r.value=a.data}catch{r.value=[]}finally{n.value=!1}}return{integration:e,workItems:r,syncing:l,loading:n,error:c,fetchIntegration:u,saveIntegration:d,deleteIntegration:g,sync:f,fetchWorkItems:y}});export{m as u};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/cc-dashboard/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CC Dashboard</title>
<script type="module" crossorigin src="/cc-dashboard/assets/index-DRwzmrLE.js"></script>
<link rel="stylesheet" crossorigin href="/cc-dashboard/assets/index-C0ue-SwZ.css">
<script type="module" crossorigin src="/cc-dashboard/static/assets/index-ZkX-rg-0.js"></script>
<link rel="stylesheet" crossorigin href="/cc-dashboard/static/assets/index-vw6q8aQU.css">
</head>
<body>
<div id="app"></div>

View file

@ -4,6 +4,7 @@
v-if="!isOpen"
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"
aria-label="Open AI Assistant"
@click="open"
>
<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">
@ -48,12 +49,12 @@
</svg>
{{ flagCount }} issue{{ flagCount > 1 ? 's' : '' }}
</button>
<button class="p-1.5 text-gray-400 hover:text-white transition-colors" title="Clear history" @click="clearHistory">
<button class="p-1.5 text-gray-400 hover:text-white transition-colors" title="Clear history" aria-label="Clear chat history" @click="clearHistory">
<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">
<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" />
</svg>
</button>
<button class="p-1.5 text-gray-400 hover:text-white transition-colors" @click="isOpen = false">
<button class="p-1.5 text-gray-400 hover:text-white transition-colors" aria-label="Close assistant" @click="isOpen = false">
<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">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
@ -164,7 +165,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, watch } from 'vue'
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { useAuthStore } from '@/stores/auth'
interface Message {
@ -207,21 +208,43 @@ function toolLabel(tool: string): string {
}
function renderMarkdown(text: string): string {
// Simple markdown to HTML: bold, code, headers, lists, line breaks
return text
const escaped = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/### (.+)/g, '<h3 class="text-sm font-semibold text-white mt-2 mb-1">$1</h3>')
.replace(/## (.+)/g, '<h3 class="text-sm font-semibold text-white mt-2 mb-1">$1</h3>')
.replace(/# (.+)/g, '<h3 class="font-semibold text-white mt-2 mb-1">$1</h3>')
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-white">$1</strong>')
.replace(/`(.+?)`/g, '<code class="rounded bg-gray-700 px-1 py-0.5 text-amber-400 text-xs font-mono">$1</code>')
.replace(/^- (.+)/gm, '<li class="ml-3 list-disc">$1</li>')
.replace(/^(\d+)\. (.+)/gm, '<li class="ml-3 list-decimal">$2</li>')
.replace(/<\/li>\n<li/g, '</li><li')
.replace(/\n\n/g, '</p><p class="mt-1">')
.replace(/\n/g, '<br/>')
// Process line by line for correct block-level structure
const lines = escaped.split('\n')
const out: string[] = []
let inList = false
for (const raw of lines) {
const line = raw
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-white font-semibold">$1</strong>')
.replace(/`(.+?)`/g, '<code class="rounded bg-gray-700 px-1 py-0.5 text-amber-400 text-xs font-mono">$1</code>')
if (/^###? (.+)/.test(line)) {
if (inList) { out.push('</ul>'); inList = false }
out.push(`<p class="text-xs font-semibold text-white mt-2 mb-0.5">${line.replace(/^###? /, '')}</p>`)
} else if (/^# (.+)/.test(line)) {
if (inList) { out.push('</ul>'); inList = false }
out.push(`<p class="text-sm font-bold text-white mt-2 mb-1">${line.replace(/^# /, '')}</p>`)
} else if (/^- (.+)/.test(line)) {
if (!inList) { out.push('<ul class="list-disc ml-4 space-y-0.5 my-1">'); inList = true }
out.push(`<li>${line.replace(/^- /, '')}</li>`)
} else if (/^\d+\. (.+)/.test(line)) {
if (!inList) { out.push('<ol class="list-decimal ml-4 space-y-0.5 my-1">'); inList = true }
out.push(`<li>${line.replace(/^\d+\. /, '')}</li>`)
} else if (line.trim() === '') {
if (inList) { out.push('</ul>'); inList = false }
out.push('<div class="mt-1.5"></div>')
} else {
if (inList) { out.push('</ul>'); inList = false }
out.push(`<p>${line}</p>`)
}
}
if (inList) out.push('</ul>')
return out.join('')
}
async function loadHistory(): Promise<void> {
@ -392,10 +415,15 @@ async function open(): Promise<void> {
scrollBottom()
}
let flagPollTimer: ReturnType<typeof setInterval> | undefined
onMounted(() => {
loadFlagCount()
// Refresh flag count every 5 minutes
setInterval(loadFlagCount, 5 * 60 * 1000)
flagPollTimer = setInterval(loadFlagCount, 5 * 60 * 1000)
})
onUnmounted(() => {
if (flagPollTimer !== undefined) clearInterval(flagPollTimer)
})
watch(isOpen, (val) => {

View file

@ -2,7 +2,7 @@
import Card from '@/components/ui/Card.vue'
import CardContent from '@/components/ui/CardContent.vue'
defineProps<{
const props = defineProps<{
label: string
value: string | number
icon?: string
@ -13,16 +13,20 @@ defineProps<{
</script>
<template>
<Card class="relative overflow-hidden">
<Card class="relative overflow-hidden transition-all duration-200 hover:shadow-md hover:-translate-y-px">
<!-- Corner decoration circles (from 21st.dev KPI pattern) -->
<span class="pointer-events-none absolute -right-6 -top-6 h-16 w-16 rounded-full bg-primary/5" />
<span class="pointer-events-none absolute -right-2 -top-2 h-8 w-8 rounded-full bg-primary/8" />
<CardContent class="p-5">
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0">
<p class="text-xs text-muted-foreground font-medium uppercase tracking-wide truncate">
<p class="text-[11px] text-muted-foreground font-semibold uppercase tracking-widest truncate">
{{ label }}
</p>
<div class="mt-1">
<div class="mt-1.5">
<div v-if="loading" class="h-8 w-24 bg-muted animate-pulse rounded" />
<p v-else class="text-2xl font-bold text-foreground">{{ value }}</p>
<p v-else class="text-2xl font-bold text-foreground tracking-tight">{{ value }}</p>
</div>
<p v-if="description" class="text-xs text-muted-foreground mt-1 truncate">
{{ description }}
@ -32,65 +36,73 @@ defineProps<{
<!-- Icon -->
<div
v-if="icon"
class="h-9 w-9 rounded-lg bg-primary/10 flex items-center justify-center shrink-0"
class="h-10 w-10 rounded-xl bg-primary/10 ring-1 ring-primary/20 flex items-center justify-center shrink-0"
>
<!-- Clock -->
<svg v-if="icon === 'clock'" class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<!-- Calendar -->
<svg v-else-if="icon === 'calendar'" class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<!-- Folder -->
<svg v-else-if="icon === 'folder'" class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7a2 2 0 012-2h3.586a1 1 0 01.707.293l1.414 1.414A1 1 0 0011.414 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
</svg>
<!-- Trending up -->
<svg v-else-if="icon === 'trending-up'" class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
<!-- Git commit -->
<svg v-else-if="icon === 'git'" class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="2" />
<path stroke-linecap="round" stroke-width="2" d="M2 12h6M16 12h6" />
</svg>
<!-- Star -->
<svg v-else class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
</div>
</div>
<!-- Trend indicator -->
<div v-if="trend !== undefined" class="mt-3 flex items-center gap-1 text-xs">
<svg
:class="trend >= 0 ? 'text-emerald-400' : 'text-red-400'"
class="h-3 w-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
<!-- Trend indicator improved with icon -->
<div v-if="trend !== undefined" class="mt-3 flex items-center gap-1.5 text-xs">
<div
:class="[
'flex items-center gap-1 font-semibold',
trend > 0 ? 'text-emerald-500' : trend < 0 ? 'text-red-400' : 'text-muted-foreground',
]"
>
<path
v-if="trend >= 0"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 10l7-7m0 0l7 7m-7-7v18"
/>
<path
v-else
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
</svg>
<span :class="trend >= 0 ? 'text-emerald-400' : 'text-red-400'">
{{ Math.abs(trend) }}%
</span>
<svg
class="h-3.5 w-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
v-if="trend > 0"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2.5"
d="M5 10l7-7m0 0l7 7m-7-7v18"
/>
<path
v-else-if="trend < 0"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2.5"
d="M19 14l-7 7m0 0l-7-7m7 7V3"
/>
<path
v-else
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2.5"
d="M5 12h14"
/>
</svg>
{{ trend > 0 ? '+' : '' }}{{ Math.abs(trend) }}%
</div>
<span class="text-muted-foreground">vs last period</span>
</div>
<!-- Baseline accent bar -->
<div class="mt-3 h-0.5 w-12 rounded-full bg-primary/30" />
</CardContent>
</Card>
</template>

View file

@ -35,79 +35,103 @@ function isActive(path: string): boolean {
if (path === '/') return route.path === '/'
return route.path.startsWith(path)
}
const userInitials = computed(() => {
const name = authStore.user?.username ?? authStore.user?.email ?? '?'
return name.slice(0, 2).toUpperCase()
})
</script>
<template>
<aside class="flex flex-col h-full bg-slate-900 dark:bg-slate-900 border-r border-border">
<aside class="flex flex-col h-full bg-gradient-to-b from-slate-900 via-slate-900 to-slate-950 border-r border-slate-800">
<!-- Logo -->
<div class="h-14 flex items-center px-4 border-b border-border shrink-0">
<div class="flex items-center gap-2">
<div class="h-7 w-7 rounded-md bg-primary flex items-center justify-center">
<div class="h-14 flex items-center px-4 border-b border-slate-800 shrink-0">
<div class="flex items-center gap-3">
<div class="h-8 w-8 rounded-lg bg-gradient-to-br from-amber-400 to-amber-600 flex items-center justify-center shadow-lg shadow-amber-900/30">
<svg class="h-4 w-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<span class="font-bold text-sm text-foreground">CC Dashboard</span>
<div>
<p class="font-bold text-sm text-white leading-none">CC Dashboard</p>
<p class="text-[10px] text-slate-500 mt-0.5">Oliver Agency</p>
</div>
</div>
</div>
<!-- Navigation -->
<nav class="flex-1 px-2 py-4 space-y-1 overflow-y-auto">
<nav class="flex-1 px-2 py-3 space-y-0.5 overflow-y-auto">
<RouterLink
v-for="item in visibleItems"
:key="item.path"
:to="item.path"
:class="[
'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-colors',
'relative flex items-center gap-3 px-3 h-11 rounded-lg text-sm font-medium transition-all duration-200 group',
isActive(item.path)
? 'bg-primary/20 text-primary'
: 'text-slate-400 hover:bg-slate-800 hover:text-slate-100',
? 'bg-gradient-to-r from-amber-500/20 to-amber-600/10 text-amber-400 shadow-sm border border-amber-500/20'
: 'text-slate-400 hover:bg-slate-800/60 hover:text-slate-100',
]"
@click="emit('close')"
>
<!-- Active left bar -->
<span
v-if="isActive(item.path)"
class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-5 bg-amber-400 rounded-r-full"
/>
<!-- Icons -->
<svg
v-if="item.icon === 'grid'"
class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"
:class="['h-4 w-4 shrink-0 transition-colors', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']"
fill="none" stroke="currentColor" viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
<svg v-else-if="item.icon === 'calendar'" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="item.icon === 'calendar'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<svg v-else-if="item.icon === 'check-square'" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="item.icon === 'check-square'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
<svg v-else-if="item.icon === 'folder'" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="item.icon === 'folder'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7a2 2 0 012-2h3.586a1 1 0 01.707.293l1.414 1.414A1 1 0 0011.414 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
</svg>
<svg v-else-if="item.icon === 'activity'" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="item.icon === 'activity'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<svg v-else-if="item.icon === 'file-text'" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="item.icon === 'file-text'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<svg v-else-if="item.icon === 'key'" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="item.icon === 'key'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
<svg v-else-if="item.icon === 'settings'" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="item.icon === 'settings'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<svg v-else-if="item.icon === 'shield'" class="h-4 w-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="item.icon === 'shield'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span>{{ item.name }}</span>
</RouterLink>
</nav>
<!-- User info at bottom -->
<div class="p-4 border-t border-border shrink-0">
<div class="flex items-center gap-2 text-xs text-slate-400">
<div class="h-2 w-2 rounded-full bg-emerald-500"></div>
<span class="truncate">{{ authStore.user?.username ?? authStore.user?.email }}</span>
<div class="p-3 border-t border-slate-800 shrink-0">
<div class="flex items-center gap-3 px-2 py-2 rounded-lg hover:bg-slate-800/50 transition-colors">
<div 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">
{{ userInitials }}
</div>
<div class="flex-1 min-w-0">
<p class="text-xs font-medium text-slate-300 truncate">{{ authStore.user?.username ?? authStore.user?.email }}</p>
<div class="flex items-center gap-1 mt-0.5">
<div class="h-1.5 w-1.5 rounded-full bg-emerald-500"></div>
<span class="text-[10px] text-slate-500">Online</span>
</div>
</div>
</div>
</div>
</aside>

View file

@ -1,7 +1,6 @@
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'
import { useRouter } from 'vue-router'
import Avatar from '@/components/ui/Avatar.vue'
import Button from '@/components/ui/Button.vue'
import { toast } from 'vue-sonner'
@ -31,36 +30,60 @@ function toggleDark() {
</script>
<template>
<header class="h-14 border-b border-border bg-card flex items-center px-4 gap-4 shrink-0">
<header class="h-14 border-b border-border bg-card/95 backdrop-blur-sm flex items-center px-4 gap-3 shrink-0 sticky top-0 z-10">
<!-- Mobile hamburger -->
<Button variant="ghost" size="icon" class="lg:hidden" @click="emit('toggleSidebar')">
<button
class="lg:hidden flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
aria-label="Toggle sidebar"
@click="emit('toggleSidebar')"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</Button>
</button>
<!-- Page title -->
<h1 class="text-base font-semibold text-foreground flex-1">{{ title ?? 'CC Dashboard' }}</h1>
<div class="flex-1">
<h1 class="text-sm font-semibold text-foreground">{{ title ?? 'CC Dashboard' }}</h1>
</div>
<!-- Actions slot -->
<slot name="actions" />
<!-- Dark mode toggle -->
<Button variant="ghost" size="icon" @click="toggleDark">
<button
class="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
aria-label="Toggle dark mode"
@click="toggleDark"
>
<svg class="h-4 w-4 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<svg class="h-4 w-4 dark:hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
</Button>
</button>
<!-- User menu -->
<div class="flex items-center gap-2">
<Avatar :name="authStore.user?.username ?? authStore.user?.email" size="sm" />
<Button variant="ghost" size="sm" class="text-xs text-muted-foreground" @click="handleLogout">
<!-- Divider -->
<div class="h-6 w-px bg-border" />
<!-- User section -->
<div class="flex items-center gap-2.5">
<div 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">
{{ (authStore.user?.username ?? authStore.user?.email ?? '?').slice(0, 2).toUpperCase() }}
</div>
<span class="hidden sm:block text-xs font-medium text-foreground max-w-[120px] truncate">
{{ authStore.user?.username ?? authStore.user?.email }}
</span>
<button
class="flex h-7 items-center gap-1 rounded-md px-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
@click="handleLogout"
>
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Sign out
</Button>
</button>
</div>
</header>
</template>

View file

@ -27,5 +27,6 @@ export default defineConfig({
}
}
},
base: '/cc-dashboard/'
// Base for static assets — must match FastAPI StaticFiles mount at /cc-dashboard/static/
base: '/cc-dashboard/static/'
})