feat: premium redesign — cyan/teal design system + bug fixes

Design:
- Replace purple SaaS theme with operational dark navy (#0b1020) + cyan (#57c7ff)
- Satoshi + JetBrains Mono fonts via CSS @import
- KpiCard: hero variant for Total Hours, tabular-nums for all values
- Sidebar: cyan active state instead of amber, dimmer inactive icons
- Dashboard: skeleton loading states for all charts, polished empty states
- TopBar: cyan user avatar consistent with sidebar

Fixes:
- Live Feed: SSE URL was /api/events/stream (wrong) → /api/events; pass JWT as ?token= query param
- Dashboard: default preset changed to 'today' instead of '30d'
- index.html: Cache-Control: no-cache to prevent stale asset issues

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-06 20:08:44 +01:00
parent 3e48a789fc
commit 2b4fd5dee8
38 changed files with 300 additions and 215 deletions

View file

@ -104,10 +104,13 @@ async def health():
app.mount("/static", StaticFiles(directory="src/static"), name="static")
_NO_CACHE = {"Cache-Control": "no-cache, no-store, must-revalidate"}
@app.get("/", include_in_schema=False)
@app.get("", include_in_schema=False)
async def spa_root():
return FileResponse("src/static/index.html")
return FileResponse("src/static/index.html", headers=_NO_CACHE)
@app.get("/{path:path}", include_in_schema=False)
@ -116,4 +119,4 @@ async def spa_fallback(path: str, request: Request):
from fastapi import HTTPException
raise HTTPException(status_code=404)
return FileResponse("src/static/index.html")
return FileResponse("src/static/index.html", headers=_NO_CACHE)

View file

@ -1 +1 @@
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-CO3lBHVT.js";import{a as w}from"./admin-s3id3yDK.js";import{_ as B,a as S}from"./CardContent.vue_vue_type_script_setup_true_lang-UcbhRP8Z.js";import{_ as x}from"./Badge.vue_vue_type_script_setup_true_lang-CocRK-vO.js";import{_ as V,a as $}from"./utils-B1YxgOQw.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};
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-B9hhyP-T.js";import{a as w}from"./admin-CT_XX4Td.js";import{_ as B,a as S}from"./CardContent.vue_vue_type_script_setup_true_lang-B1uPgvNK.js";import{_ as x}from"./Badge.vue_vue_type_script_setup_true_lang-BV0smx_q.js";import{_ as V,a as $}from"./utils-DuVQys2y.js";const N={class:"p-6"},C={key:0,class:"flex items-center justify-center h-20"},D={class:"w-full"},E={class:"px-4 py-3"},F={class:"text-sm font-medium text-foreground"},R={class:"px-4 py-3 text-sm text-muted-foreground"},U={class:"px-4 py-3"},j={class:"px-4 py-3"},I={class:"px-4 py-3 text-xs text-muted-foreground"},G=p({__name:"AdminView",setup(J){const f=y(),_=b(),i=u([]),l=u(!1);return h(async()=>{if(!f.isAdmin){_.push("/");return}l.value=!0;try{const c=await w.users();i.value=c.data}finally{l.value=!1}}),(c,o)=>(s(),r("div",N,[o[1]||(o[1]=t("h2",{class:"text-lg font-semibold text-foreground mb-6"},"Admin — Users",-1)),l.value?(s(),r("div",C,[n(V,{class:"text-primary"})])):(s(),v(B,{key:1},{default:d(()=>[n(S,{class:"p-0"},{default:d(()=>[t("table",D,[o[0]||(o[0]=t("thead",null,[t("tr",{class:"border-b border-border"},[t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"User"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Email"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Role"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Status"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Joined")])],-1)),t("tbody",null,[(s(!0),r(g,null,k(i.value,e=>(s(),r("tr",{key:e.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",E,[t("p",F,a(e.username),1)]),t("td",R,a(e.email),1),t("td",U,[n(x,{variant:e.role==="admin"?"default":"secondary",class:"text-xs"},{default:d(()=>[m(a(e.role),1)]),_:2},1032,["variant"])]),t("td",j,[n(x,{variant:e.is_active?"success":"outline",class:"text-xs"},{default:d(()=>[m(a(e.is_active?"Active":"Inactive"),1)]),_:2},1032,["variant"])]),t("td",I,a(A($)(e.created_at)),1)]))),128))])])]),_:1})]),_:1}))]))}});export{G as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

@ -1 +1 @@
import{c as e}from"./utils-B1YxgOQw.js";import{d as o,c as t,p as n,i as c,s as p,o as l}from"./index-CO3lBHVT.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-DuVQys2y.js";import{d as o,c as t,p as n,i as c,s as p,o as l}from"./index-B9hhyP-T.js";const _=o({__name:"Card",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),t("div",{class:n(c(e)("rounded-lg border bg-card text-card-foreground shadow-sm",a.class))},[p(r.$slots,"default")],2))}}),f=o({__name:"CardContent",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),t("div",{class:n(c(e)("p-6 pt-0",a.class))},[p(r.$slots,"default")],2))}});export{_,f as a};

View file

@ -1 +1 @@
import{c as t}from"./utils-B1YxgOQw.js";import{d as o,o as n,c as r,p as c,i as l,s as p}from"./index-CO3lBHVT.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-DuVQys2y.js";import{d as o,o as n,c as r,p as c,i as l,s as p}from"./index-B9hhyP-T.js";const f=o({__name:"CardHeader",props:{class:{}},setup(s){const e=s;return(a,i)=>(n(),r("div",{class:c(l(t)("flex flex-col space-y-1.5 p-6",e.class))},[p(a.$slots,"default")],2))}}),_=o({__name:"CardTitle",props:{class:{}},setup(s){const e=s;return(a,i)=>(n(),r("h3",{class:c(l(t)("text-lg font-semibold leading-none tracking-tight",e.class))},[p(a.$slots,"default")],2))}});export{f as _,_ as a};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +1 @@
import{D as e}from"./index-CO3lBHVT.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-B9hhyP-T.js";const i={users:()=>e.get("/api/admin/users"),keys:()=>e.get("/api/keys"),createKey:s=>e.post("/api/keys",s),revokeKey:s=>e.delete(`/api/keys/${s}`)};export{i as a};

View file

@ -1 +1 @@
import{D as t}from"./index-CO3lBHVT.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-B9hhyP-T.js";const e={summary:a=>t.get("/api/dashboard/summary",{params:a}),projects:a=>t.get("/api/dashboard/projects",{params:a}),timeline:a=>t.get("/api/dashboard/timeline",{params:a}),monthly:a=>t.get("/api/dashboard/monthly",{params:a}),dow:a=>t.get("/api/dashboard/dow",{params:a}),tools:a=>t.get("/api/dashboard/tools",{params:a}),activity:a=>t.get("/api/dashboard/activity",{params:a}),calendar:a=>t.get("/api/dashboard/calendar",{params:a}),project:(a,o)=>t.get("/api/dashboard/project/"+a,{params:o})};export{e as d};

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

View file

@ -1,4 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue'
import Card from '@/components/ui/Card.vue'
import CardContent from '@/components/ui/CardContent.vue'
@ -9,26 +10,40 @@ const props = defineProps<{
trend?: number
description?: string
loading?: boolean
hero?: boolean
}>()
const cardClass = computed(() =>
props.hero
? 'relative overflow-hidden transition-all duration-200 border-primary/20 bg-primary/5 ring-1 ring-primary/15 panel-glow-hover'
: 'relative overflow-hidden transition-all duration-200 border-border/60 panel-glow-hover'
)
</script>
<template>
<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" />
<Card :class="cardClass">
<!-- Subtle corner accent -->
<span class="pointer-events-none absolute -right-4 -top-4 h-14 w-14 rounded-full"
:class="hero ? 'bg-primary/10' : 'bg-primary/5'" />
<span class="pointer-events-none absolute -right-1 -top-1 h-6 w-6 rounded-full"
:class="hero ? 'bg-primary/15' : '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-[11px] text-muted-foreground font-semibold uppercase tracking-widest truncate">
<p class="text-[10px] font-semibold uppercase tracking-[0.1em] truncate"
:class="hero ? 'text-primary/80' : 'text-muted-foreground'">
{{ label }}
</p>
<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 tracking-tight">{{ value }}</p>
<div class="mt-2">
<div v-if="loading" class="h-7 w-20 bg-muted animate-pulse rounded" />
<p v-else
class="kpi-value font-bold tracking-tight leading-none"
:class="hero ? 'text-3xl text-primary' : 'text-2xl text-foreground'">
{{ value }}
</p>
</div>
<p v-if="description" class="text-xs text-muted-foreground mt-1 truncate">
<p v-if="description" class="text-xs text-muted-foreground mt-1.5 truncate">
{{ description }}
</p>
</div>
@ -36,73 +51,54 @@ const props = defineProps<{
<!-- Icon -->
<div
v-if="icon"
class="h-10 w-10 rounded-xl bg-primary/10 ring-1 ring-primary/20 flex items-center justify-center shrink-0"
class="rounded-xl flex items-center justify-center shrink-0"
:class="[
hero ? 'h-11 w-11 bg-primary/15 ring-1 ring-primary/25' : 'h-9 w-9 bg-muted ring-1 ring-border',
]"
>
<svg v-if="icon === 'clock'" class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-if="icon === 'clock'" :class="['shrink-0', hero ? 'h-5 w-5 text-primary' : 'h-4 w-4 text-muted-foreground']" 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>
<svg v-else-if="icon === 'calendar'" class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="icon === 'calendar'" :class="['shrink-0', hero ? 'h-5 w-5 text-primary' : 'h-4 w-4 text-muted-foreground']" 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="icon === 'folder'" class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="icon === 'folder'" :class="['shrink-0', hero ? 'h-5 w-5 text-primary' : 'h-4 w-4 text-muted-foreground']" 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="icon === 'trending-up'" class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="icon === 'trending-up'" :class="['shrink-0', hero ? 'h-5 w-5 text-primary' : 'h-4 w-4 text-muted-foreground']" 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>
<svg v-else-if="icon === 'git'" class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else-if="icon === 'git'" :class="['shrink-0', hero ? 'h-5 w-5 text-primary' : 'h-4 w-4 text-muted-foreground']" 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>
<svg v-else class="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg v-else :class="['shrink-0', hero ? 'h-5 w-5 text-primary' : 'h-4 w-4 text-muted-foreground']" 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 improved with icon -->
<!-- Trend indicator -->
<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',
'flex items-center gap-1 font-semibold tabular-nums',
trend > 0 ? 'text-[hsl(var(--success))]' : trend < 0 ? 'text-destructive' : 'text-muted-foreground',
]"
>
<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 class="h-3 w-3" 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" />
<!-- Bottom accent bar -->
<div class="mt-3 h-px rounded-full"
:class="hero ? 'w-full bg-primary/20' : 'w-10 bg-primary/20'" />
</CardContent>
</Card>
</template>

View file

@ -43,19 +43,19 @@ const userInitials = computed(() => {
</script>
<template>
<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">
<aside class="flex flex-col h-full bg-[hsl(220_44%_8%)] border-r border-border">
<!-- Logo -->
<div class="h-14 flex items-center px-4 border-b border-slate-800 shrink-0">
<div class="h-14 flex items-center px-4 border-b border-border 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">
<div class="h-8 w-8 rounded-lg bg-primary/10 ring-1 ring-primary/25 flex items-center justify-center">
<svg class="h-4 w-4 text-primary" 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>
<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>
<p class="font-bold text-sm text-foreground leading-none tracking-tight">CC Dashboard</p>
<p class="text-[10px] text-muted-foreground mt-0.5 tracking-wide">Oliver Agency</p>
</div>
</div>
</div>
@ -67,69 +67,69 @@ const userInitials = computed(() => {
:key="item.path"
:to="item.path"
:class="[
'relative flex items-center gap-3 px-3 h-11 rounded-lg text-sm font-medium transition-all duration-200 group',
'relative flex items-center gap-3 px-3 h-10 rounded-lg text-sm font-medium transition-all duration-150 group',
isActive(item.path)
? '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',
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground',
]"
@click="emit('close')"
>
<!-- Active left bar -->
<!-- Active left indicator -->
<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"
class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 bg-primary rounded-r-full"
/>
<!-- Icons -->
<svg
v-if="item.icon === 'grid'"
:class="['h-4 w-4 shrink-0 transition-colors', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']"
:class="['h-4 w-4 shrink-0 transition-colors', isActive(item.path) ? 'text-primary' : 'text-muted-foreground/60 group-hover:text-muted-foreground']"
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', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" 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-primary' : 'text-muted-foreground/60 group-hover:text-muted-foreground']" 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', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" 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-primary' : 'text-muted-foreground/60 group-hover:text-muted-foreground']" 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', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" 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-primary' : 'text-muted-foreground/60 group-hover:text-muted-foreground']" 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', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" 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-primary' : 'text-muted-foreground/60 group-hover:text-muted-foreground']" 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', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" 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-primary' : 'text-muted-foreground/60 group-hover:text-muted-foreground']" 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', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" 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-primary' : 'text-muted-foreground/60 group-hover:text-muted-foreground']" 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', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" 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-primary' : 'text-muted-foreground/60 group-hover:text-muted-foreground']" 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', isActive(item.path) ? 'text-amber-400' : 'text-slate-500 group-hover:text-slate-300']" 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-primary' : 'text-muted-foreground/60 group-hover:text-muted-foreground']" 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>
<span class="text-sm">{{ item.name }}</span>
</RouterLink>
</nav>
<!-- User info at bottom -->
<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">
<div class="p-3 border-t border-border shrink-0">
<div class="flex items-center gap-3 px-2 py-2 rounded-lg">
<div class="h-7 w-7 rounded-full bg-primary/15 ring-1 ring-primary/20 flex items-center justify-center text-[10px] font-bold text-primary 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>
<p class="text-xs font-medium text-foreground 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 class="h-1.5 w-1.5 rounded-full bg-[hsl(var(--success))]"></div>
<span class="text-[10px] text-muted-foreground">Online</span>
</div>
</div>
</div>

View file

@ -69,7 +69,7 @@ function toggleDark() {
<!-- 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">
<div class="h-7 w-7 rounded-full bg-primary/15 ring-1 ring-primary/25 flex items-center justify-center text-[10px] font-bold text-primary 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">

View file

@ -1,51 +1,70 @@
@import url('https://api.fontshare.com/v2/css?f[]=satoshi@700,500,400&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 224 71.4% 4.1%;
--card: 0 0% 100%;
--card-foreground: 224 71.4% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--primary: 262.1 83.3% 57.8%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--accent: 220 14.3% 95.9%;
--accent-foreground: 220.9 39.3% 11%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 20% 98%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 262.1 83.3% 57.8%;
--radius: 0.5rem;
/* ── Operational dashboard dark navy / cyan ─────────────────── */
--background: 226 49% 8%; /* #0b1020 */
--foreground: 220 40% 92%; /* #dce4f4 */
--card: 220 44% 10%; /* #0f1629 panel L1 */
--card-foreground: 220 40% 92%;
--popover: 220 44% 12%; /* #111c34 panel L2 */
--popover-foreground: 220 40% 92%;
/* Cyan accent #57c7ff */
--primary: 200 100% 67%;
--primary-foreground: 226 49% 8%;
--secondary: 220 30% 14%;
--secondary-foreground: 220 20% 75%;
--muted: 220 30% 12%;
--muted-foreground: 220 12% 52%;
--accent: 220 30% 14%;
--accent-foreground: 220 40% 92%;
--destructive: 0 72% 51%;
--destructive-foreground: 220 40% 98%;
--border: 220 28% 17%;
--input: 220 28% 17%;
--ring: 200 100% 67%;
--radius: 0.625rem;
/* Success / Warning */
--success: 158 64% 52%;
--warning: 38 92% 60%;
}
/* Dark class mirrors root — always operational dark */
.dark {
--background: 224 71.4% 4.1%;
--foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card-foreground: 210 20% 98%;
--popover: 215 27.9% 10%;
--popover-foreground: 210 20% 98%;
--primary: 263.4 70% 50.4%;
--primary-foreground: 210 20% 98%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--ring: 263.4 70% 50.4%;
--background: 226 49% 8%;
--foreground: 220 40% 92%;
--card: 220 44% 10%;
--card-foreground: 220 40% 92%;
--popover: 220 44% 12%;
--popover-foreground: 220 40% 92%;
--primary: 200 100% 67%;
--primary-foreground: 226 49% 8%;
--secondary: 220 30% 14%;
--secondary-foreground: 220 20% 75%;
--muted: 220 30% 12%;
--muted-foreground: 220 12% 52%;
--accent: 220 30% 14%;
--accent-foreground: 220 40% 92%;
--destructive: 0 72% 51%;
--destructive-foreground: 220 40% 98%;
--border: 220 28% 17%;
--input: 220 28% 17%;
--ring: 200 100% 67%;
}
}
@ -53,21 +72,43 @@
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-family: 'Satoshi', 'Inter', system-ui, -apple-system, sans-serif;
font-feature-settings: "rlig" 1, "calt" 1;
-webkit-font-smoothing: antialiased;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
/* Monospace for numeric values */
.tabular-nums,
[data-value],
.kpi-value {
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
font-variant-numeric: tabular-nums;
}
/* Thin scrollbar */
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
@apply bg-border rounded-full;
background: hsl(220 28% 20%);
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-muted-foreground;
background: hsl(220 28% 30%);
}
}
/* ── Utility: panel glow on hover ─────────────────────────────────── */
@layer utilities {
.panel-glow {
box-shadow: 0 0 0 1px hsl(var(--border)), 0 4px 24px -4px hsl(226 49% 4% / 0.6);
}
.panel-glow-hover:hover {
box-shadow: 0 0 0 1px hsl(200 100% 67% / 0.18), 0 8px 32px -4px hsl(200 100% 67% / 0.08);
}
.accent-glow {
box-shadow: 0 0 16px -2px hsl(200 100% 67% / 0.35);
}
}

View file

@ -13,7 +13,7 @@ import type { KpiSummary, ProjectSummary, MonthlyDataPoint, DowDataPoint, ToolUs
type Preset = 'today' | '7d' | '30d' | 'custom'
const preset = ref<Preset>('30d')
const preset = ref<Preset>('today')
const customFrom = ref('')
const customTo = ref('')
@ -72,7 +72,6 @@ watch(preset, () => {
onMounted(() => loadData())
// Chart helpers
const maxMonthlyHours = computed(() => Math.max(...monthly.value.map((d) => d.hours), 1))
const maxDowHours = computed(() => Math.max(...dow.value.map((d) => d.hours), 1))
const maxToolPct = computed(() => Math.max(...tools.value.map((t) => t.pct), 1))
@ -83,26 +82,28 @@ const progressColor = (pct: number | null) => {
if (pct > 70) return 'warning'
return 'success'
}
const DOW_LABELS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
</script>
<template>
<div class="p-6 space-y-6">
<!-- Header + Date filter -->
<div class="flex flex-wrap items-center gap-3">
<h2 class="text-lg font-semibold text-foreground flex-1">Overview</h2>
<h2 class="text-base font-semibold text-foreground flex-1 tracking-tight">Overview</h2>
<!-- Preset buttons -->
<div class="flex items-center rounded-md border border-border overflow-hidden">
<div class="flex items-center rounded-lg border border-border overflow-hidden bg-muted/30">
<button
v-for="p in ['today', '7d', '30d', 'custom']"
v-for="p in (['today', '7d', '30d', 'custom'] as const)"
:key="p"
:class="[
'px-3 py-1.5 text-xs font-medium transition-colors',
preset === p
? 'bg-primary text-primary-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-muted',
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50',
]"
@click="preset = p as Preset"
@click="preset = p"
>
{{ p === 'today' ? 'Today' : p === '7d' ? '7 days' : p === '30d' ? '30 days' : 'Custom' }}
</button>
@ -113,82 +114,93 @@ const progressColor = (pct: number | null) => {
<input
v-model="customFrom"
type="date"
class="h-8 rounded-md border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
class="h-8 rounded-lg border border-input bg-muted/30 px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
/>
<span class="text-xs text-muted-foreground">to</span>
<input
v-model="customTo"
type="date"
class="h-8 rounded-md border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
class="h-8 rounded-lg border border-input bg-muted/30 px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
/>
<Button size="sm" :loading="loading" @click="loadData">Apply</Button>
</template>
</div>
<!-- KPI cards -->
<!-- KPI cards hero first card -->
<div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-4">
<KpiCard
label="Total Hours"
:value="summary ? formatDuration(summary.total_hours) : '-'"
:value="summary ? formatDuration(summary.total_hours) : ''"
icon="clock"
:loading="loading"
:hero="true"
/>
<KpiCard
label="Working Days"
:value="summary?.working_days ?? '-'"
:value="summary?.working_days ?? ''"
icon="calendar"
:loading="loading"
/>
<KpiCard
label="Projects"
:value="summary?.total_projects ?? '-'"
:value="summary?.total_projects ?? ''"
icon="folder"
:loading="loading"
/>
<KpiCard
label="Avg / Day"
:value="summary ? formatDuration(summary.avg_hours_per_day) : '-'"
:value="summary ? formatDuration(summary.avg_hours_per_day) : ''"
icon="trending-up"
:loading="loading"
/>
<KpiCard
label="Top Project"
:value="summary?.top_project ?? '-'"
:value="summary?.top_project ?? ''"
icon="star"
:loading="loading"
/>
<KpiCard
label="Commits"
:value="summary?.total_commits ?? '-'"
:value="summary?.total_commits ?? ''"
icon="git"
:loading="loading"
/>
</div>
<!-- Charts row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Monthly bar chart -->
<Card>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- Hours by Day bar chart -->
<Card class="border-border/60 bg-card panel-glow">
<CardHeader class="pb-2">
<CardTitle class="text-sm">Hours by Day</CardTitle>
<CardTitle class="text-xs font-semibold text-muted-foreground uppercase tracking-widest">Hours by Day</CardTitle>
</CardHeader>
<CardContent>
<div v-if="loading" class="h-40 flex items-center justify-center text-sm text-muted-foreground">
Loading...
<!-- Loading skeleton -->
<div v-if="loading" class="h-40 flex items-end gap-px">
<div
v-for="i in 30" :key="i"
class="flex-1 bg-muted animate-pulse rounded-t"
:style="{ height: `${20 + Math.random() * 60}%` }"
/>
</div>
<div v-else-if="monthly.length === 0" class="h-40 flex items-center justify-center text-sm text-muted-foreground">
No data
<!-- Empty state -->
<div v-else-if="monthly.length === 0" class="h-40 flex flex-col items-center justify-center gap-2">
<svg class="h-8 w-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
<p class="text-xs text-muted-foreground">No sessions in this period</p>
</div>
<!-- Chart -->
<div v-else class="h-40 flex items-end gap-px">
<div
v-for="point in monthly"
:key="point.date"
class="flex-1 flex flex-col items-center gap-0.5 group"
class="flex-1 flex flex-col items-center group"
:title="`${point.date}: ${formatDuration(point.hours)}`"
>
<div
class="w-full bg-primary/70 hover:bg-primary rounded-t transition-colors"
:style="{ height: `${(point.hours / maxMonthlyHours) * 100}%` }"
class="w-full bg-primary/40 hover:bg-primary rounded-t transition-colors duration-150"
:style="{ height: `${(point.hours / maxMonthlyHours) * 100}%`, minHeight: '2px' }"
/>
</div>
</div>
@ -196,17 +208,26 @@ const progressColor = (pct: number | null) => {
</Card>
<!-- Day of week bar chart -->
<Card>
<Card class="border-border/60 bg-card panel-glow">
<CardHeader class="pb-2">
<CardTitle class="text-sm">Hours by Day of Week</CardTitle>
<CardTitle class="text-xs font-semibold text-muted-foreground uppercase tracking-widest">By Day of Week</CardTitle>
</CardHeader>
<CardContent>
<div v-if="loading" class="h-40 flex items-center justify-center text-sm text-muted-foreground">
Loading...
<!-- Loading skeleton -->
<div v-if="loading" class="h-40 flex items-end gap-2">
<div v-for="i in 7" :key="i" class="flex-1 flex flex-col items-center gap-1">
<div class="w-full bg-muted animate-pulse rounded-t" :style="{ height: `${30 + i * 8}%` }" />
<div class="h-3 w-4 bg-muted animate-pulse rounded" />
</div>
</div>
<div v-else-if="dow.length === 0" class="h-40 flex items-center justify-center text-sm text-muted-foreground">
No data
<!-- Empty state -->
<div v-else-if="dow.length === 0" class="h-40 flex flex-col items-center justify-center gap-2">
<svg class="h-8 w-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
<p class="text-xs text-muted-foreground">No sessions in this period</p>
</div>
<!-- Chart -->
<div v-else class="h-40 flex items-end gap-2">
<div
v-for="point in dow"
@ -214,11 +235,11 @@ const progressColor = (pct: number | null) => {
class="flex-1 flex flex-col items-center gap-1"
>
<div
class="w-full bg-primary/70 hover:bg-primary rounded-t transition-colors"
class="w-full bg-primary/40 hover:bg-primary rounded-t transition-colors duration-150"
:style="{ height: `${Math.max((point.hours / maxDowHours) * 100, 2)}%` }"
:title="`${formatDuration(point.hours)}`"
:title="`${point.label}: ${formatDuration(point.hours)}`"
/>
<span class="text-xs text-muted-foreground">{{ point.label.slice(0, 2) }}</span>
<span class="text-[10px] text-muted-foreground font-medium">{{ point.label.slice(0, 2) }}</span>
</div>
</div>
</CardContent>
@ -226,29 +247,39 @@ const progressColor = (pct: number | null) => {
</div>
<!-- Bottom row: Tools + Projects -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- Tool usage -->
<Card>
<Card class="border-border/60 bg-card panel-glow">
<CardHeader class="pb-2">
<CardTitle class="text-sm">Tool Usage</CardTitle>
<CardTitle class="text-xs font-semibold text-muted-foreground uppercase tracking-widest">Tool Usage</CardTitle>
</CardHeader>
<CardContent>
<div v-if="loading" class="space-y-2">
<div v-for="i in 5" :key="i" class="h-6 bg-muted animate-pulse rounded" />
<!-- Loading skeleton -->
<div v-if="loading" class="space-y-3">
<div v-for="i in 5" :key="i" class="flex items-center gap-2">
<div class="h-3 rounded bg-muted animate-pulse" :style="{ width: `${40 + i * 10}px` }" />
<div class="flex-1 h-2 bg-muted animate-pulse rounded-full" />
<div class="h-3 w-8 bg-muted animate-pulse rounded" />
</div>
</div>
<div v-else-if="tools.length === 0" class="text-sm text-muted-foreground py-4 text-center">
No data
<!-- Empty state -->
<div v-else-if="tools.length === 0" class="flex flex-col items-center justify-center py-8 gap-2">
<svg class="h-8 w-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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" />
</svg>
<p class="text-xs text-muted-foreground">No tool data yet</p>
</div>
<div v-else class="space-y-2">
<div v-for="tool in tools.slice(0, 8)" :key="tool.tool" class="flex items-center gap-2">
<span class="text-xs text-foreground w-24 truncate shrink-0">{{ tool.tool }}</span>
<div class="flex-1 h-2 bg-secondary rounded-full overflow-hidden">
<!-- List -->
<div v-else class="space-y-2.5">
<div v-for="tool in tools.slice(0, 8)" :key="tool.tool" class="flex items-center gap-2.5">
<span class="text-xs text-foreground w-24 truncate shrink-0 tabular-nums">{{ tool.tool }}</span>
<div class="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
<div
class="h-full bg-primary rounded-full"
class="h-full bg-primary/70 rounded-full transition-all duration-300"
:style="{ width: `${(tool.pct / maxToolPct) * 100}%` }"
/>
</div>
<span class="text-xs text-muted-foreground w-10 text-right shrink-0">
<span class="text-xs text-muted-foreground w-9 text-right shrink-0 tabular-nums">
{{ (tool.pct ?? 0).toFixed(0) }}%
</span>
</div>
@ -257,22 +288,34 @@ const progressColor = (pct: number | null) => {
</Card>
<!-- Project hours table -->
<Card>
<Card class="border-border/60 bg-card panel-glow">
<CardHeader class="pb-2">
<CardTitle class="text-sm">Projects</CardTitle>
<CardTitle class="text-xs font-semibold text-muted-foreground uppercase tracking-widest">Projects</CardTitle>
</CardHeader>
<CardContent>
<div v-if="loading" class="space-y-2">
<div v-for="i in 5" :key="i" class="h-8 bg-muted animate-pulse rounded" />
<!-- Loading skeleton -->
<div v-if="loading" class="space-y-3">
<div v-for="i in 5" :key="i" class="space-y-1.5">
<div class="flex justify-between">
<div class="h-3 rounded bg-muted animate-pulse" :style="{ width: `${80 + i * 15}px` }" />
<div class="h-3 w-12 bg-muted animate-pulse rounded" />
</div>
<div class="h-1.5 bg-muted animate-pulse rounded-full" />
</div>
</div>
<div v-else-if="projects.length === 0" class="text-sm text-muted-foreground py-4 text-center">
No data
<!-- Empty state -->
<div v-else-if="projects.length === 0" class="flex flex-col items-center justify-center py-8 gap-2">
<svg class="h-8 w-8 text-muted-foreground/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
<p class="text-xs text-muted-foreground">No project data yet</p>
</div>
<div v-else class="space-y-2">
<!-- List -->
<div v-else class="space-y-2.5">
<div v-for="proj in projects.slice(0, 8)" :key="proj.project_id">
<div class="flex items-center justify-between text-xs mb-0.5">
<span class="text-foreground truncate max-w-[160px]">{{ proj.display_name }}</span>
<span class="text-muted-foreground shrink-0">{{ formatDuration(proj.total_hours) }}</span>
<div class="flex items-center justify-between text-xs mb-1">
<span class="text-foreground truncate max-w-[160px] font-medium">{{ proj.display_name }}</span>
<span class="text-muted-foreground shrink-0 tabular-nums ml-2">{{ formatDuration(proj.total_hours) }}</span>
</div>
<Progress
v-if="proj.progress_pct !== null"

View file

@ -8,10 +8,12 @@ import Button from '@/components/ui/Button.vue'
import { formatDateTime } from '@/lib/utils'
const authStore = useAuthStore()
const { events, connected, error, connect, clearEvents } = useSSE('/cc-dashboard/api/events/stream')
const token = authStore.getToken()
const sseUrl = `/cc-dashboard/api/events${token ? `?token=${encodeURIComponent(token)}` : ''}`
const { events, connected, error, connect, clearEvents } = useSSE(sseUrl)
onMounted(() => {
if (authStore.isAuthenticated) {
if (authStore.isAuthenticated && token) {
connect()
}
})