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:
parent
2118187c76
commit
162f4ba822
42 changed files with 362 additions and 209 deletions
63
AGENTS.md
Normal file
63
AGENTS.md
Normal 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 -->
|
||||
58
deploy.sh
58
deploy.sh
|
|
@ -1,40 +1,50 @@
|
|||
#!/usr/bin/env bash
|
||||
# Deploy script for CC Dashboard on optical-dev.oliver.solutions
|
||||
# Run ON the server: ssh optical-dev "cd /opt/cc-dashboard && bash deploy.sh"
|
||||
set -euo pipefail
|
||||
|
||||
STATIC_SRC="/opt/cc-dashboard/src/static"
|
||||
STATIC_DST="/var/www/html/cc-dashboard"
|
||||
COMPOSE="docker compose -f /opt/cc-dashboard/docker-compose.yml"
|
||||
|
||||
log() { echo "[$(date '+%H:%M:%S')] $*"; }
|
||||
die() { echo "[ERROR] $*" >&2; exit 1; }
|
||||
log() { echo "[$(date '+%H:%M:%S')] $*"; }
|
||||
die() { echo "[ERROR] $*" >&2; exit 1; }
|
||||
pass() { echo "[OK] $*"; }
|
||||
|
||||
cd /opt/cc-dashboard || die "Cannot cd to /opt/cc-dashboard"
|
||||
|
||||
# ── 1. Pull latest code ──────────────────────────────────────────────────────
|
||||
log "Pulling latest code..."
|
||||
git pull
|
||||
log "Pulling latest code from origin/main..."
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
pass "Code updated: $(git log --oneline -1)"
|
||||
|
||||
# ── 2. Sync static files to web root ────────────────────────────────────────
|
||||
# Apache serves /cc-dashboard/* from /var/www/html/cc-dashboard/
|
||||
# index.html must be at the root; CSS/JS go into static/ subdir to match
|
||||
# paths in index.html (/cc-dashboard/static/css/..., /cc-dashboard/static/js/...)
|
||||
log "Syncing static files to $STATIC_DST..."
|
||||
sudo mkdir -p "$STATIC_DST/static"
|
||||
sudo cp "$STATIC_SRC/index.html" "$STATIC_DST/index.html"
|
||||
sudo rsync -a --delete --exclude='index.html' "$STATIC_SRC/" "$STATIC_DST/static/"
|
||||
|
||||
# ── 3. Rebuild app image ─────────────────────────────────────────────────────
|
||||
log "Building app image..."
|
||||
# ── 2. Rebuild app image ─────────────────────────────────────────────────────
|
||||
log "Building app image (Python deps + code)..."
|
||||
$COMPOSE build app
|
||||
pass "Image built."
|
||||
|
||||
# ── 4. Restart app (migrations run automatically on container start) ─────────
|
||||
# ── 3. Restart app (alembic upgrade runs automatically in CMD) ───────────────
|
||||
# DB container is NOT touched — data in pgdata volume is preserved.
|
||||
log "Restarting app container..."
|
||||
$COMPOSE up -d --no-deps app
|
||||
pass "Container restarted. Alembic migrations run on container start."
|
||||
|
||||
# ── 5. Wait for healthy + show logs ─────────────────────────────────────────
|
||||
log "Waiting for container to start..."
|
||||
sleep 3
|
||||
$COMPOSE ps app
|
||||
$COMPOSE logs --tail=30 app
|
||||
# ── 4. Wait and verify ───────────────────────────────────────────────────────
|
||||
log "Waiting for app to become healthy..."
|
||||
for i in $(seq 1 15); do
|
||||
if $COMPOSE exec -T app sh -c "curl -fs http://localhost:8800/cc-dashboard/healthz > /dev/null 2>&1"; then
|
||||
pass "Healthcheck OK after ${i}s."
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
if [[ $i -eq 15 ]]; then
|
||||
log "Healthcheck timeout — showing last 40 lines of logs:"
|
||||
$COMPOSE logs --tail=40 app
|
||||
die "App did not start in time."
|
||||
fi
|
||||
done
|
||||
|
||||
log "Deploy complete."
|
||||
# ── 5. Tail recent logs ──────────────────────────────────────────────────────
|
||||
log "Recent app logs:"
|
||||
$COMPOSE logs --tail=20 app
|
||||
|
||||
log "Deploy complete. App at http://optical-dev.oliver.solutions:8800/cc-dashboard/"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
1
src/static/assets/AppLayout-BuZkSaIr.js
Normal file
1
src/static/assets/AppLayout-BuZkSaIr.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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 _};
|
||||
|
|
@ -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 _};
|
||||
|
|
@ -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 _};
|
||||
|
|
@ -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 _};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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};
|
||||
|
|
@ -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
1
src/static/assets/DashboardView-DcIc21XJ.js
Normal file
1
src/static/assets/DashboardView-DcIc21XJ.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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 _};
|
||||
|
|
@ -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 _};
|
||||
|
|
@ -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};
|
||||
|
|
@ -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};
|
||||
|
|
@ -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
|
|
@ -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 _};
|
||||
|
|
@ -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};
|
||||
|
|
@ -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};
|
||||
|
|
@ -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={"&":"&","<":"<",">":">",'"':""","'":"'"},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={"&":"&","<":"<",">":">",'"':""","'":"'"},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,`
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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};
|
||||
|
|
@ -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};
|
||||
|
|
@ -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
40
src/static/assets/index-ZkX-rg-0.js
Normal file
40
src/static/assets/index-ZkX-rg-0.js
Normal file
File diff suppressed because one or more lines are too long
1
src/static/assets/index-vw6q8aQU.css
Normal file
1
src/static/assets/index-vw6q8aQU.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.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) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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/'
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue