feat: replace local auth with Azure AD SSO (MSAL PKCE)
- New POST /api/auth/microsoft endpoint validates Azure ID token via JWKS - Removed POST /api/auth/login and /change-password - Added azure_oid + nullable password_hash to users (migration 0007) - Auto-provisions all @oliver.agency accounts on first SSO login - Case-insensitive email matching links existing vadymsamoilenko@ account - DEV_AUTH_BYPASS flag for local development without MSAL - Frontend: MSAL loginPopup replaces email/password form - Added scripts/grant_admin.py for role management Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
dc50dd1d3b
commit
96e6f4ee14
64 changed files with 6296 additions and 175 deletions
10
.env.example
10
.env.example
|
|
@ -10,6 +10,16 @@ BASE_PATH=/cc-dashboard
|
|||
APP_TITLE=CC Dashboard
|
||||
LOG_FORMAT=json
|
||||
|
||||
# Azure AD SSO (Oliver tenant — shared)
|
||||
AZURE_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
AZURE_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
|
||||
ALLOWED_EMAIL_DOMAIN=oliver.agency
|
||||
# Comma-separated emails that auto-receive admin role on first SSO login
|
||||
ADMIN_EMAILS=vadymsamoilenko@oliver.agency
|
||||
# Local dev only — set to true to skip SSO and auto-login as DEV_USER_EMAIL
|
||||
DEV_AUTH_BYPASS=false
|
||||
DEV_USER_EMAIL=vadymsamoilenko@oliver.agency
|
||||
|
||||
# Azure DevOps
|
||||
ADO_ORGANIZATION=your-org
|
||||
ADO_PROJECT=your-project
|
||||
|
|
|
|||
29
alembic/versions/0007_sso_user_columns.py
Normal file
29
alembic/versions/0007_sso_user_columns.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"""Add azure_oid to users and make password_hash nullable
|
||||
|
||||
Revision ID: 0007
|
||||
Revises: 0006
|
||||
Create Date: 2026-05-07
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0007"
|
||||
down_revision = "0006"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column("users", sa.Column("azure_oid", sa.String(36), nullable=True))
|
||||
op.create_unique_constraint("uq_users_azure_oid", "users", ["azure_oid"])
|
||||
op.create_index("ix_users_azure_oid", "users", ["azure_oid"])
|
||||
op.alter_column("users", "password_hash", existing_type=sa.String(255), nullable=True)
|
||||
# Normalize existing emails to lowercase
|
||||
op.execute("UPDATE users SET email = LOWER(email)")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index("ix_users_azure_oid", table_name="users")
|
||||
op.drop_constraint("uq_users_azure_oid", "users", type_="unique")
|
||||
op.drop_column("users", "azure_oid")
|
||||
op.alter_column("users", "password_hash", existing_type=sa.String(255), nullable=False)
|
||||
32
scripts/grant_admin.py
Normal file
32
scripts/grant_admin.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""Grant admin role to a user by email (SSO users).
|
||||
|
||||
Usage:
|
||||
docker compose exec app python scripts/grant_admin.py user@oliver.agency
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from src.database import async_session_factory
|
||||
from src.models import User
|
||||
|
||||
|
||||
async def grant_admin(email: str) -> None:
|
||||
email = email.strip().lower()
|
||||
async with async_session_factory() as db:
|
||||
result = await db.execute(select(User).where(func.lower(User.email) == email))
|
||||
user = result.scalar_one_or_none()
|
||||
if user is None:
|
||||
print(f"User {email!r} not found. They must log in via SSO first.")
|
||||
sys.exit(1)
|
||||
user.role = "admin"
|
||||
await db.commit()
|
||||
print(f"Granted admin to {user.email} (id={user.id})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python scripts/grant_admin.py user@oliver.agency")
|
||||
sys.exit(1)
|
||||
asyncio.run(grant_admin(sys.argv[1]))
|
||||
11
src/auth.py
11
src/auth.py
|
|
@ -90,8 +90,19 @@ async def get_current_user(
|
|||
credentials: Annotated[HTTPAuthorizationCredentials | None, Security(bearer_scheme)],
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> User:
|
||||
from src.config import settings
|
||||
from sqlalchemy import select as sa_select, func
|
||||
|
||||
if not credentials:
|
||||
if settings.DEV_AUTH_BYPASS:
|
||||
result = await db.execute(
|
||||
sa_select(User).where(func.lower(User.email) == settings.DEV_USER_EMAIL.lower())
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
if user and user.is_active:
|
||||
return user
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
|
||||
|
||||
payload = decode_token(credentials.credentials)
|
||||
if payload.get("type") != "access":
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type")
|
||||
|
|
|
|||
|
|
@ -36,6 +36,14 @@ class Settings(BaseSettings):
|
|||
WEEKLY_REPORT_DAY: int = 6 # 0=Mon ... 6=Sun
|
||||
WEEKLY_REPORT_HOUR: int = 21
|
||||
|
||||
# Azure AD SSO
|
||||
AZURE_TENANT_ID: str = ""
|
||||
AZURE_CLIENT_ID: str = ""
|
||||
ALLOWED_EMAIL_DOMAIN: str = "oliver.agency"
|
||||
ADMIN_EMAILS: str = "" # comma-separated lowercase
|
||||
DEV_AUTH_BYPASS: bool = False
|
||||
DEV_USER_EMAIL: str = "vadymsamoilenko@oliver.agency"
|
||||
|
||||
# Logging
|
||||
LOG_FORMAT: str = "console" # "json" or "console"
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ class User(Base):
|
|||
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=new_uuid)
|
||||
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
username: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
password_hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
azure_oid: Mapped[str | None] = mapped_column(String(36), nullable=True, unique=True, index=True)
|
||||
role: Mapped[str] = mapped_column(Enum("admin", "user", name="user_role"), default="user", nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
daily_overhead_hours: Mapped[float] = mapped_column(Float, default=0.0, nullable=False)
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ async def create_user(body: UserCreate, admin: AdminUser, db: AsyncSession = Dep
|
|||
if exists.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
user = User(
|
||||
email=body.email,
|
||||
email=body.email.lower(),
|
||||
username=body.username,
|
||||
password_hash=hash_password(body.password),
|
||||
password_hash=hash_password(body.password) if body.password else None,
|
||||
role=body.role,
|
||||
)
|
||||
db.add(user)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,73 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.auth import (
|
||||
CurrentUser, create_access_token, create_refresh_token,
|
||||
decode_token, hash_password, verify_password,
|
||||
decode_token, hash_password,
|
||||
)
|
||||
from src.config import settings
|
||||
from src.database import get_db
|
||||
from src.models import User
|
||||
from src.schemas import (
|
||||
ChangePasswordRequest, LoginRequest, RefreshRequest,
|
||||
TokenResponse, UserOut,
|
||||
)
|
||||
from src.schemas import MicrosoftLoginRequest, RefreshRequest, TokenResponse, UserOut
|
||||
from src.sso import validate_microsoft_id_token
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
async def login(body: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
result = await db.execute(select(User).where(User.email == body.email))
|
||||
user = result.scalar_one_or_none()
|
||||
if not user or not user.is_active or not verify_password(body.password, user.password_hash):
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
||||
def _admin_set() -> set[str]:
|
||||
return {e.strip().lower() for e in settings.ADMIN_EMAILS.split(",") if e.strip()}
|
||||
|
||||
|
||||
@router.post("/microsoft", response_model=TokenResponse)
|
||||
async def microsoft_sso(body: MicrosoftLoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
claims = validate_microsoft_id_token(body.id_token)
|
||||
|
||||
raw_email = claims.get("preferred_username") or claims.get("email") or ""
|
||||
email = raw_email.lower()
|
||||
oid: str = claims.get("oid", "")
|
||||
name: str = claims.get("name", "")
|
||||
|
||||
if not email or not email.endswith(f"@{settings.ALLOWED_EMAIL_DOMAIN}"):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Domain not allowed")
|
||||
|
||||
# Find by azure_oid first (most stable), fall back to email match
|
||||
user: User | None = None
|
||||
if oid:
|
||||
result = await db.execute(select(User).where(User.azure_oid == oid))
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
result = await db.execute(
|
||||
select(User).where(func.lower(User.email) == email)
|
||||
)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if user is None:
|
||||
# First-time SSO login — auto-provision
|
||||
username = email.split("@")[0]
|
||||
user = User(
|
||||
email=email,
|
||||
username=username,
|
||||
password_hash=None,
|
||||
azure_oid=oid or None,
|
||||
role="admin" if email in _admin_set() else "user",
|
||||
)
|
||||
db.add(user)
|
||||
else:
|
||||
# Link existing account to Azure OID on first SSO login
|
||||
if oid and user.azure_oid is None:
|
||||
user.azure_oid = oid
|
||||
# Promote to admin if listed
|
||||
if email in _admin_set() and user.role != "admin":
|
||||
user.role = "admin"
|
||||
# Normalize email to lowercase (case-insensitive matching)
|
||||
if user.email != email:
|
||||
user.email = email
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
return TokenResponse(
|
||||
access_token=create_access_token(user.id, user.role),
|
||||
refresh_token=create_refresh_token(user.id),
|
||||
|
|
@ -42,19 +88,6 @@ async def refresh(body: RefreshRequest, db: AsyncSession = Depends(get_db)):
|
|||
)
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
async def change_password(
|
||||
body: ChangePasswordRequest,
|
||||
user: CurrentUser,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
if not verify_password(body.current_password, user.password_hash):
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Wrong current password")
|
||||
user.password_hash = hash_password(body.new_password)
|
||||
await db.commit()
|
||||
return {"detail": "Password changed"}
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserOut)
|
||||
async def me(user: CurrentUser):
|
||||
return user
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@ from pydantic import BaseModel, EmailStr, Field
|
|||
|
||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
class MicrosoftLoginRequest(BaseModel):
|
||||
id_token: str
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
|
|
@ -21,11 +20,6 @@ class RefreshRequest(BaseModel):
|
|||
refresh_token: str
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
current_password: str
|
||||
new_password: str = Field(min_length=8)
|
||||
|
||||
|
||||
# ── Users ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
class UserOut(BaseModel):
|
||||
|
|
@ -43,7 +37,7 @@ class UserOut(BaseModel):
|
|||
class UserCreate(BaseModel):
|
||||
email: EmailStr
|
||||
username: str = Field(min_length=2, max_length=100)
|
||||
password: str = Field(min_length=8)
|
||||
password: str | None = Field(default=None, min_length=8)
|
||||
role: str = Field(default="user", pattern="^(admin|user)$")
|
||||
|
||||
|
||||
|
|
|
|||
55
src/sso.py
Normal file
55
src/sso.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""Azure AD SSO — validates Microsoft ID tokens via JWKS."""
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import HTTPException, status
|
||||
from jose import JWTError, jwt
|
||||
|
||||
from src.config import settings
|
||||
|
||||
_jwks_cache: dict[str, Any] = {}
|
||||
_jwks_fetched_at: float = 0.0
|
||||
_JWKS_TTL = 3600 # seconds
|
||||
|
||||
|
||||
def _get_jwks() -> dict:
|
||||
global _jwks_cache, _jwks_fetched_at
|
||||
if time.time() - _jwks_fetched_at < _JWKS_TTL and _jwks_cache:
|
||||
return _jwks_cache
|
||||
url = f"https://login.microsoftonline.com/{settings.AZURE_TENANT_ID}/discovery/v2.0/keys"
|
||||
resp = httpx.get(url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
_jwks_cache = resp.json()
|
||||
_jwks_fetched_at = time.time()
|
||||
return _jwks_cache
|
||||
|
||||
|
||||
def validate_microsoft_id_token(id_token: str) -> dict:
|
||||
"""Validate Azure AD ID token and return claims. Raises 401 on any failure."""
|
||||
if not settings.AZURE_TENANT_ID or not settings.AZURE_CLIENT_ID:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="SSO not configured",
|
||||
)
|
||||
try:
|
||||
jwks = _get_jwks()
|
||||
claims = jwt.decode(
|
||||
id_token,
|
||||
jwks,
|
||||
algorithms=["RS256"],
|
||||
audience=settings.AZURE_CLIENT_ID,
|
||||
issuer=f"https://login.microsoftonline.com/{settings.AZURE_TENANT_ID}/v2.0",
|
||||
options={"verify_at_hash": False},
|
||||
)
|
||||
return claims
|
||||
except JWTError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Invalid Microsoft token: {exc}",
|
||||
)
|
||||
except httpx.HTTPError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Failed to fetch Azure AD keys: {exc}",
|
||||
)
|
||||
|
|
@ -1 +0,0 @@
|
|||
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-yrXqsixb.js";import{a as w}from"./admin-BRKJZipt.js";import{_ as B,a as S}from"./CardContent.vue_vue_type_script_setup_true_lang-BZS0eQer.js";import{_ as x}from"./Badge.vue_vue_type_script_setup_true_lang-18ft6dLh.js";import{_ as V,a as $}from"./utils-D_0J15Md.js";const N={class:"p-6"},C={key:0,class:"flex items-center justify-center h-20"},D={class:"w-full"},E={class:"px-4 py-3"},F={class:"text-sm font-medium text-foreground"},R={class:"px-4 py-3 text-sm text-muted-foreground"},U={class:"px-4 py-3"},j={class:"px-4 py-3"},I={class:"px-4 py-3 text-xs text-muted-foreground"},G=p({__name:"AdminView",setup(J){const f=y(),_=b(),i=u([]),l=u(!1);return h(async()=>{if(!f.isAdmin){_.push("/");return}l.value=!0;try{const c=await w.users();i.value=c.data}finally{l.value=!1}}),(c,o)=>(s(),r("div",N,[o[1]||(o[1]=t("h2",{class:"text-lg font-semibold text-foreground mb-6"},"Admin — Users",-1)),l.value?(s(),r("div",C,[n(V,{class:"text-primary"})])):(s(),v(B,{key:1},{default:d(()=>[n(S,{class:"p-0"},{default:d(()=>[t("table",D,[o[0]||(o[0]=t("thead",null,[t("tr",{class:"border-b border-border"},[t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"User"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Email"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Role"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Status"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Joined")])],-1)),t("tbody",null,[(s(!0),r(g,null,k(i.value,e=>(s(),r("tr",{key:e.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",E,[t("p",F,a(e.username),1)]),t("td",R,a(e.email),1),t("td",U,[n(x,{variant:e.role==="admin"?"default":"secondary",class:"text-xs"},{default:d(()=>[m(a(e.role),1)]),_:2},1032,["variant"])]),t("td",j,[n(x,{variant:e.is_active?"success":"outline",class:"text-xs"},{default:d(()=>[m(a(e.is_active?"Active":"Inactive"),1)]),_:2},1032,["variant"])]),t("td",I,a(A($)(e.created_at)),1)]))),128))])])]),_:1})]),_:1}))]))}});export{G as default};
|
||||
1
src/static/assets/AdminView-DUmZvUGQ.js
Normal file
1
src/static/assets/AdminView-DUmZvUGQ.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{d as _,u as y,v as h,c as r,a as t,e as n,k as v,w as d,f as b,q as m,o as s,F as g,r as k,t as a,p as u,h as A}from"./index-DzSm5_bv.js";import{a as w}from"./admin-DOjSzxjn.js";import{_ as B,a as S}from"./CardContent.vue_vue_type_script_setup_true_lang-B899D1fp.js";import{_ as f}from"./Badge.vue_vue_type_script_setup_true_lang-CaB6FyQ0.js";import{_ as V}from"./Spinner.vue_vue_type_script_setup_true_lang-DxuuceC3.js";import{a as $}from"./utils-7WVCegLb.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"},q={class:"px-4 py-3 text-xs text-muted-foreground"},H=_({__name:"AdminView",setup(I){const x=y(),p=b(),i=m([]),l=m(!1);return h(async()=>{if(!x.isAdmin){p.push("/");return}l.value=!0;try{const c=await w.users();i.value=c.data}finally{l.value=!1}}),(c,o)=>(s(),r("div",N,[o[1]||(o[1]=t("h2",{class:"text-lg font-semibold text-foreground mb-6"},"Admin — Users",-1)),l.value?(s(),r("div",C,[n(V,{class:"text-primary"})])):(s(),v(B,{key:1},{default:d(()=>[n(S,{class:"p-0"},{default:d(()=>[t("table",D,[o[0]||(o[0]=t("thead",null,[t("tr",{class:"border-b border-border"},[t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"User"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Email"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Role"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Status"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Joined")])],-1)),t("tbody",null,[(s(!0),r(g,null,k(i.value,e=>(s(),r("tr",{key:e.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",E,[t("p",F,a(e.username),1)]),t("td",R,a(e.email),1),t("td",U,[n(f,{variant:e.role==="admin"?"default":"secondary",class:"text-xs"},{default:d(()=>[u(a(e.role),1)]),_:2},1032,["variant"])]),t("td",j,[n(f,{variant:e.is_active?"success":"outline",class:"text-xs"},{default:d(()=>[u(a(e.is_active?"Active":"Inactive"),1)]),_:2},1032,["variant"])]),t("td",q,a(A($)(e.created_at)),1)]))),128))])])]),_:1})]),_:1}))]))}});export{H as default};
|
||||
File diff suppressed because one or more lines are too long
1
src/static/assets/AppLayout-LtMoYzU8.js
Normal file
1
src/static/assets/AppLayout-LtMoYzU8.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{c as a}from"./utils-D_0J15Md.js";import{d as n,o as s,c as o,p as d,i,s as c}from"./index-yrXqsixb.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-7WVCegLb.js";import{d as n,o,c as s,n as d,h as i,m as c}from"./index-DzSm5_bv.js";const f=n({__name:"Badge",props:{variant:{default:"default"},class:{}},setup(r){const e=r;return(t,l)=>(o(),s("span",{class:d(i(a)("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors",{"bg-primary text-primary-foreground":e.variant==="default","bg-secondary text-secondary-foreground":e.variant==="secondary","bg-destructive text-destructive-foreground":e.variant==="destructive","border border-border text-foreground":e.variant==="outline","bg-emerald-500/20 text-emerald-400":e.variant==="success","bg-amber-500/20 text-amber-400":e.variant==="warning"},e.class))},[c(t.$slots,"default")],2))}});export{f as _};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{_ as c}from"./Spinner.vue_vue_type_script_setup_true_lang-DxuuceC3.js";import{c as l}from"./utils-7WVCegLb.js";import{d as u,c as m,n as f,k as b,i as v,m as g,j as p,o as n}from"./index-DzSm5_bv.js";const y=["type","disabled"],z=u({__name:"Button",props:{variant:{default:"default"},size:{default:"md"},loading:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},type:{default:"button"},class:{}},emits:["click"],setup(t,{emit:s}){const e=t,a=s,r=p(()=>l("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,o)=>(n(),m("button",{class:f(r.value),type:t.type,disabled:t.disabled||t.loading,onClick:o[0]||(o[0]=d=>a("click",d))},[t.loading?(n(),b(c,{key:0,size:"sm",class:"mr-2"})):v("",!0),g(i.$slots,"default")],10,y))}});export{z as _};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{c,_ as l}from"./utils-D_0J15Md.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-yrXqsixb.js";const y=["type","disabled"],k=u({__name:"Button",props:{variant:{default:"default"},size:{default:"md"},loading:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},type:{default:"button"},class:{}},emits:["click"],setup(t,{emit:o}){const e=t,a=o,r=p(()=>c("inline-flex items-center justify-center rounded-md font-medium transition-colors","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2","disabled:pointer-events-none disabled:opacity-50",{"bg-primary text-primary-foreground hover:bg-primary/90":e.variant==="default","border border-input bg-background hover:bg-accent hover:text-accent-foreground":e.variant==="outline","hover:bg-accent hover:text-accent-foreground":e.variant==="ghost","bg-destructive text-destructive-foreground hover:bg-destructive/90":e.variant==="destructive","bg-secondary text-secondary-foreground hover:bg-secondary/80":e.variant==="secondary","underline-offset-4 hover:underline text-primary":e.variant==="link","h-8 px-3 text-xs":e.size==="sm","h-10 px-4 py-2 text-sm":e.size==="md","h-11 px-8 text-base":e.size==="lg","h-9 w-9 p-0":e.size==="icon"},e.class));return(i,s)=>(n(),f("button",{class:m(r.value),type:t.type,disabled:t.disabled||t.loading,onClick:s[0]||(s[0]=d=>a("click",d))},[t.loading?(n(),b(l,{key:0,size:"sm",class:"mr-2"})):v("",!0),g(i.$slots,"default")],10,y))}});export{k as _};
|
||||
1
src/static/assets/CalendarView-CVfEc5OT.js
Normal file
1
src/static/assets/CalendarView-CVfEc5OT.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{c as e}from"./utils-7WVCegLb.js";import{d as o,c as n,n as t,h as c,m as l,o as p}from"./index-DzSm5_bv.js";const _=o({__name:"Card",props:{class:{}},setup(s){const a=s;return(r,d)=>(p(),n("div",{class:t(c(e)("rounded-lg border bg-card text-card-foreground shadow-sm",a.class))},[l(r.$slots,"default")],2))}}),f=o({__name:"CardContent",props:{class:{}},setup(s){const a=s;return(r,d)=>(p(),n("div",{class:t(c(e)("p-6 pt-0",a.class))},[l(r.$slots,"default")],2))}});export{_,f as a};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{c as e}from"./utils-D_0J15Md.js";import{d as o,c as t,p as n,i as c,s as p,o as l}from"./index-yrXqsixb.js";const _=o({__name:"Card",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),t("div",{class:n(c(e)("rounded-lg border bg-card text-card-foreground shadow-sm",a.class))},[p(r.$slots,"default")],2))}}),f=o({__name:"CardContent",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),t("div",{class:n(c(e)("p-6 pt-0",a.class))},[p(r.$slots,"default")],2))}});export{_,f as a};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{c as t}from"./utils-D_0J15Md.js";import{d as o,o as n,c as r,p as c,i as l,s as p}from"./index-yrXqsixb.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};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{c as t}from"./utils-7WVCegLb.js";import{d as n,o,c as r,n as c,h as l,m as p}from"./index-DzSm5_bv.js";const f=n({__name:"CardHeader",props:{class:{}},setup(s){const e=s;return(a,m)=>(o(),r("div",{class:c(l(t)("flex flex-col space-y-1.5 p-6",e.class))},[p(a.$slots,"default")],2))}}),_=n({__name:"CardTitle",props:{class:{}},setup(s){const e=s;return(a,m)=>(o(),r("h3",{class:c(l(t)("text-lg font-semibold leading-none tracking-tight",e.class))},[p(a.$slots,"default")],2))}});export{f as _,_ as a};
|
||||
File diff suppressed because one or more lines are too long
1
src/static/assets/DashboardView-Cvjfxfcs.js
Normal file
1
src/static/assets/DashboardView-Cvjfxfcs.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{d as S,x as $,c as a,a as s,i as r,n as x,w as l,j as u,e as d,o as n,k as m,t as i,F as _,l as p,p as y,f as I,r as z,m as A,K as v}from"./index-yrXqsixb.js";import{u as j}from"./devops-C_7zqRan.js";import{_ as k,a as h}from"./CardContent.vue_vue_type_script_setup_true_lang-BZS0eQer.js";import{a as D,_ as V}from"./CardTitle.vue_vue_type_script_setup_true_lang-Bs99oJeq.js";import{_ as B}from"./Button.vue_vue_type_script_setup_true_lang-XMbqbqq8.js";import{_ as b}from"./utils-D_0J15Md.js";const L={class:"p-6 space-y-6"},W={class:"flex items-center justify-between gap-4 flex-wrap"},F={class:"flex items-center gap-2"},R={key:0,class:"flex items-center gap-2 text-sm text-muted-foreground"},E={key:1,class:"flex items-center gap-3"},K={class:"text-sm text-foreground"},O={key:0,class:"text-xs text-muted-foreground ml-2"},G={key:2,class:"flex items-center gap-3"},M={key:3,class:"text-xs text-destructive mt-2"},T={class:"flex items-center justify-between gap-3 flex-wrap"},q={class:"flex items-center rounded-lg border border-border overflow-hidden bg-muted/30"},H=["onClick"],J={key:0,class:"flex items-center justify-center py-8"},P={key:1,class:"text-center py-8 text-sm text-muted-foreground"},Q={key:2,class:"space-y-1"},U={class:"text-xs font-mono text-muted-foreground w-10 shrink-0"},X={class:"flex-1 min-w-0"},Y={class:"text-sm text-foreground truncate"},Z={class:"text-xs text-muted-foreground"},tt=["href"],lt=S({__name:"DevopsView",setup(et){const w=I(),t=j(),c=z("All");$(async()=>{await t.fetchIntegration(),t.integration&&await t.fetchWorkItems()});const f=A(()=>c.value==="All"?t.workItems:t.workItems.filter(g=>g.state===c.value));async function C(){try{await t.sync(),v.success("Sync complete"),await t.fetchWorkItems()}catch{v.error(t.error??"Sync failed")}}return(g,e)=>(n(),a("div",L,[s("div",W,[e[2]||(e[2]=s("h2",{class:"text-lg font-semibold text-foreground"},"Azure DevOps",-1)),s("div",F,[r(t).integration?(n(),x(B,{key:0,variant:"outline",size:"sm",loading:r(t).syncing,onClick:C},{default:l(()=>[...e[1]||(e[1]=[m(" Sync Now ",-1)])]),_:1},8,["loading"])):u("",!0)])]),d(k,null,{default:l(()=>[d(h,{class:"pt-4"},{default:l(()=>{var o;return[r(t).loading&&!r(t).integration?(n(),a("div",R,[d(b,{size:"sm"}),e[3]||(e[3]=s("span",null,"Loading...",-1))])):r(t).integration?(n(),a("div",E,[e[6]||(e[6]=s("div",{class:"h-2 w-2 rounded-full bg-[hsl(var(--success))]"},null,-1)),s("span",K,[e[4]||(e[4]=m(" Connected to ",-1)),s("strong",null,i(r(t).integration.organization),1),e[5]||(e[5]=m(" / ",-1)),s("strong",null,i(r(t).integration.project),1)]),r(t).integration.last_synced_at?(n(),a("span",O," Last synced: "+i(new Date(r(t).integration.last_synced_at).toLocaleString()),1)):u("",!0)])):(n(),a("div",G,[e[7]||(e[7]=s("div",{class:"h-2 w-2 rounded-full bg-muted-foreground"},null,-1)),e[8]||(e[8]=s("span",{class:"text-sm text-muted-foreground"},"Not connected.",-1)),s("button",{class:"text-sm text-primary hover:underline",onClick:e[0]||(e[0]=N=>r(w).push("/settings"))}," Go to Settings to connect ")])),(o=r(t).integration)!=null&&o.last_sync_error?(n(),a("p",M," Error: "+i(r(t).integration.last_sync_error),1)):u("",!0)]}),_:1})]),_:1}),r(t).integration?(n(),x(k,{key:0},{default:l(()=>[d(V,{class:"pb-2"},{default:l(()=>[s("div",T,[d(D,{class:"text-sm"},{default:l(()=>[...e[9]||(e[9]=[m("Work Items",-1)])]),_:1}),s("div",q,[(n(),a(_,null,p(["All","Active","Resolved","Closed"],o=>s("button",{key:o,class:y(["px-3 py-1 text-xs font-medium transition-colors",c.value===o?"bg-primary text-primary-foreground":"text-muted-foreground hover:text-foreground hover:bg-muted/50"]),onClick:N=>c.value=o},i(o),11,H)),64))])])]),_:1}),d(h,null,{default:l(()=>[r(t).loading?(n(),a("div",J,[d(b,{size:"md",class:"text-primary"})])):f.value.length===0?(n(),a("div",P," No work items found ")):(n(),a("div",Q,[(n(!0),a(_,null,p(f.value,o=>(n(),a("div",{key:o.id,class:"flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-muted/30 transition-colors"},[s("span",U,"#"+i(o.ado_id),1),s("div",X,[s("p",Y,i(o.title),1),s("p",Z,i(o.type),1)]),s("span",{class:y(["text-xs px-2 py-0.5 rounded-full shrink-0",o.state==="Active"?"bg-blue-500/10 text-blue-400":o.state==="Resolved"?"bg-green-500/10 text-green-400":(o.state==="Closed","bg-muted text-muted-foreground")])},i(o.state),3),o.url?(n(),a("a",{key:0,href:o.url,target:"_blank",class:"text-xs text-primary hover:underline shrink-0"}," Open → ",8,tt)):u("",!0)]))),128))]))]),_:1})]),_:1})):u("",!0)]))}});export{lt as default};
|
||||
1
src/static/assets/DevopsView-L2Z-AJUn.js
Normal file
1
src/static/assets/DevopsView-L2Z-AJUn.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{d as S,v as $,c as a,a as s,h as r,k as x,w as l,i as u,e as d,o as n,p as m,t as i,F as p,r as _,n as y,f as I,q as z,j as A,K as v}from"./index-DzSm5_bv.js";import{u as j}from"./devops-S5lsRUq3.js";import{_ as k,a as h}from"./CardContent.vue_vue_type_script_setup_true_lang-B899D1fp.js";import{a as D,_ as V}from"./CardTitle.vue_vue_type_script_setup_true_lang-ByUGRP-t.js";import{_ as B}from"./Button.vue_vue_type_script_setup_true_lang-D97aKlXO.js";import{_ as b}from"./Spinner.vue_vue_type_script_setup_true_lang-DxuuceC3.js";import"./utils-7WVCegLb.js";const L={class:"p-6 space-y-6"},W={class:"flex items-center justify-between gap-4 flex-wrap"},F={class:"flex items-center gap-2"},R={key:0,class:"flex items-center gap-2 text-sm text-muted-foreground"},E={key:1,class:"flex items-center gap-3"},K={class:"text-sm text-foreground"},O={key:0,class:"text-xs text-muted-foreground ml-2"},q={key:2,class:"flex items-center gap-3"},G={key:3,class:"text-xs text-destructive mt-2"},M={class:"flex items-center justify-between gap-3 flex-wrap"},T={class:"flex items-center rounded-lg border border-border overflow-hidden bg-muted/30"},H=["onClick"],J={key:0,class:"flex items-center justify-center py-8"},P={key:1,class:"text-center py-8 text-sm text-muted-foreground"},Q={key:2,class:"space-y-1"},U={class:"text-xs font-mono text-muted-foreground w-10 shrink-0"},X={class:"flex-1 min-w-0"},Y={class:"text-sm text-foreground truncate"},Z={class:"text-xs text-muted-foreground"},tt=["href"],dt=S({__name:"DevopsView",setup(et){const w=I(),t=j(),c=z("All");$(async()=>{await t.fetchIntegration(),t.integration&&await t.fetchWorkItems()});const f=A(()=>c.value==="All"?t.workItems:t.workItems.filter(g=>g.state===c.value));async function C(){try{await t.sync(),v.success("Sync complete"),await t.fetchWorkItems()}catch{v.error(t.error??"Sync failed")}}return(g,e)=>(n(),a("div",L,[s("div",W,[e[2]||(e[2]=s("h2",{class:"text-lg font-semibold text-foreground"},"Azure DevOps",-1)),s("div",F,[r(t).integration?(n(),x(B,{key:0,variant:"outline",size:"sm",loading:r(t).syncing,onClick:C},{default:l(()=>[...e[1]||(e[1]=[m(" Sync Now ",-1)])]),_:1},8,["loading"])):u("",!0)])]),d(k,null,{default:l(()=>[d(h,{class:"pt-4"},{default:l(()=>{var o;return[r(t).loading&&!r(t).integration?(n(),a("div",R,[d(b,{size:"sm"}),e[3]||(e[3]=s("span",null,"Loading...",-1))])):r(t).integration?(n(),a("div",E,[e[6]||(e[6]=s("div",{class:"h-2 w-2 rounded-full bg-[hsl(var(--success))]"},null,-1)),s("span",K,[e[4]||(e[4]=m(" Connected to ",-1)),s("strong",null,i(r(t).integration.organization),1),e[5]||(e[5]=m(" / ",-1)),s("strong",null,i(r(t).integration.project),1)]),r(t).integration.last_synced_at?(n(),a("span",O," Last synced: "+i(new Date(r(t).integration.last_synced_at).toLocaleString()),1)):u("",!0)])):(n(),a("div",q,[e[7]||(e[7]=s("div",{class:"h-2 w-2 rounded-full bg-muted-foreground"},null,-1)),e[8]||(e[8]=s("span",{class:"text-sm text-muted-foreground"},"Not connected.",-1)),s("button",{class:"text-sm text-primary hover:underline",onClick:e[0]||(e[0]=N=>r(w).push("/settings"))}," Go to Settings to connect ")])),(o=r(t).integration)!=null&&o.last_sync_error?(n(),a("p",G," Error: "+i(r(t).integration.last_sync_error),1)):u("",!0)]}),_:1})]),_:1}),r(t).integration?(n(),x(k,{key:0},{default:l(()=>[d(V,{class:"pb-2"},{default:l(()=>[s("div",M,[d(D,{class:"text-sm"},{default:l(()=>[...e[9]||(e[9]=[m("Work Items",-1)])]),_:1}),s("div",T,[(n(),a(p,null,_(["All","Active","Resolved","Closed"],o=>s("button",{key:o,class:y(["px-3 py-1 text-xs font-medium transition-colors",c.value===o?"bg-primary text-primary-foreground":"text-muted-foreground hover:text-foreground hover:bg-muted/50"]),onClick:N=>c.value=o},i(o),11,H)),64))])])]),_:1}),d(h,null,{default:l(()=>[r(t).loading?(n(),a("div",J,[d(b,{size:"md",class:"text-primary"})])):f.value.length===0?(n(),a("div",P," No work items found ")):(n(),a("div",Q,[(n(!0),a(p,null,_(f.value,o=>(n(),a("div",{key:o.id,class:"flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-muted/30 transition-colors"},[s("span",U,"#"+i(o.ado_id),1),s("div",X,[s("p",Y,i(o.title),1),s("p",Z,i(o.type),1)]),s("span",{class:y(["text-xs px-2 py-0.5 rounded-full shrink-0",o.state==="Active"?"bg-blue-500/10 text-blue-400":o.state==="Resolved"?"bg-green-500/10 text-green-400":(o.state==="Closed","bg-muted text-muted-foreground")])},i(o.state),3),o.url?(n(),a("a",{key:0,href:o.url,target:"_blank",class:"text-xs text-primary hover:underline shrink-0"}," Open → ",8,tt)):u("",!0)]))),128))]))]),_:1})]),_:1})):u("",!0)]))}});export{dt as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
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-yrXqsixb.js";import{_ as $}from"./Button.vue_vue_type_script_setup_true_lang-XMbqbqq8.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 _};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{d as k,v as y,E as b,k as h,G as g,e as c,T as x,w as m,o as a,c as n,a as o,m as r,t as u,i,n as w}from"./index-DzSm5_bv.js";import{_ as $}from"./Button.vue_vue_type_script_setup_true_lang-D97aKlXO.js";const C={key:0,class:"fixed inset-0 z-50 flex items-center justify-center p-4"},B=["aria-label"],E={key:0,class:"flex items-center justify-between p-6 pb-4"},j={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=k({__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 y(()=>document.addEventListener("keydown",d)),b(()=>document.removeEventListener("keydown",d)),(t,s)=>(a(),h(g,{to:"body"},[c(x,{"enter-active-class":"transition-opacity duration-200","enter-from-class":"opacity-0","enter-to-class":"opacity-100","leave-active-class":"transition-opacity duration-200","leave-from-class":"opacity-100","leave-to-class":"opacity-0"},{default:m(()=>[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",E,[o("div",null,[r(t.$slots,"header",{},()=>[o("h2",j,u(e.title),1),e.description?(a(),n("p",z,u(e.description),1)):i("",!0)])]),c($,{variant:"ghost",size:"icon",class:"shrink-0",onClick:s[1]||(s[1]=v=>l("close"))},{default:m(()=>[...s[2]||(s[2]=[o("svg",{class:"h-4 w-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[o("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)])]),_:1})])):i("",!0),o("div",L,[r(t.$slots,"default")]),t.$slots.footer?(a(),n("div",M,[r(t.$slots,"footer")])):i("",!0)],10,B)])):i("",!0)]),_:3})]))}});export{V as _};
|
||||
|
|
@ -1 +1 @@
|
|||
import{c as i}from"./utils-D_0J15Md.js";import{d,c as s,p as u,i as m,o as r}from"./index-yrXqsixb.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-7WVCegLb.js";import{d,c as s,n as u,h as m,o as r}from"./index-DzSm5_bv.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:n}){const a=e,o=n;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",a.class)),onInput:t[0]||(t[0]=l=>o("update:modelValue",l.target.value)),onChange:t[1]||(t[1]=l=>o("change",l.target.value)),onBlur:t[2]||(t[2]=l=>o("blur",l)),onFocus:t[3]||(t[3]=l=>o("focus",l))},null,42,c))}});export{g as _};
|
||||
1
src/static/assets/KeysView-Buk66uDj.js
Normal file
1
src/static/assets/KeysView-Buk66uDj.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{a as b}from"./admin-DOjSzxjn.js";import{_ as K,a as $}from"./CardContent.vue_vue_type_script_setup_true_lang-B899D1fp.js";import{_ as v}from"./Button.vue_vue_type_script_setup_true_lang-D97aKlXO.js";import{_ as V}from"./Dialog.vue_vue_type_script_setup_true_lang-Bpehdtti.js";import{_ as N}from"./Input.vue_vue_type_script_setup_true_lang-DX_izdWK.js";import{_ as A}from"./Spinner.vue_vue_type_script_setup_true_lang-DxuuceC3.js";import{d as B,v as L,c as l,a as t,e as r,w as n,q as i,o as a,p,F as P,r as F,t as u,h as k,k as I,i as j,K as y}from"./index-DzSm5_bv.js";import{a as h}from"./utils-7WVCegLb.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"},q={class:"px-4 py-3 text-sm font-mono text-muted-foreground"},E={class:"px-4 py-3 text-xs text-muted-foreground"},H={class:"px-4 py-3 text-xs text-muted-foreground"},S={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"},re=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,F(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",q,u(s.prefix)+"...",1),t("td",E,u(k(h)(s.created_at)),1),t("td",H,u(s.last_used?k(h)(s.last_used):"Never"),1),t("td",S,[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?j("",!0):(a(),I(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{re as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{a as b}from"./admin-BRKJZipt.js";import{_ as K,a as $}from"./CardContent.vue_vue_type_script_setup_true_lang-BZS0eQer.js";import{_ as v}from"./Button.vue_vue_type_script_setup_true_lang-XMbqbqq8.js";import{_ as V}from"./Dialog.vue_vue_type_script_setup_true_lang-Bjx8yW8V.js";import{_ as N}from"./Input.vue_vue_type_script_setup_true_lang-Bo0JoDsF.js";import{_ as A,a as k}from"./utils-D_0J15Md.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-yrXqsixb.js";const D={class:"p-6"},R={class:"flex items-center justify-between mb-6"},z={key:0,class:"flex items-center justify-center h-20"},M={key:1,class:"text-center text-muted-foreground py-8 text-sm"},T={key:2,class:"w-full"},U={class:"px-4 py-3 text-sm text-foreground"},E={class:"px-4 py-3 text-sm font-mono text-muted-foreground"},H={class:"px-4 py-3 text-xs text-muted-foreground"},S={class:"px-4 py-3 text-xs text-muted-foreground"},q={class:"px-4 py-3 text-right"},G={class:"space-y-4"},J={key:0,class:"rounded-md bg-emerald-500/10 border border-emerald-500/30 p-3"},O={class:"text-xs font-mono text-foreground break-all"},Q={key:1,class:"space-y-1.5"},le=B({__name:"KeysView",setup(W){const f=i([]),_=i(!1),c=i(!1),m=i(""),x=i(!1),d=i(null);L(()=>g());async function g(){_.value=!0;try{const o=await b.keys();f.value=o.data}finally{_.value=!1}}async function w(){if(m.value.trim()){x.value=!0;try{const o=await b.createKey({label:m.value});d.value=o.data.key,y.success("API key created"),await g(),m.value=""}catch{y.error("Failed to create key")}finally{x.value=!1}}}async function C(o){if(confirm(`Revoke key "${o.label}"? This cannot be undone.`))try{await b.revokeKey(o.id),y.success("Key revoked"),f.value=f.value.filter(e=>e.id!==o.id)}catch{y.error("Failed to revoke key")}}return(o,e)=>(a(),l("div",D,[t("div",R,[e[5]||(e[5]=t("h2",{class:"text-lg font-semibold text-foreground"},"API Keys",-1)),r(v,{size:"sm",onClick:e[0]||(e[0]=s=>{c.value=!0,d.value=null})},{default:n(()=>[...e[4]||(e[4]=[t("svg",{class:"h-4 w-4 mr-1.5",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 4v16m8-8H4"})],-1),p(" New Key ",-1)])]),_:1})]),r(K,null,{default:n(()=>[r($,{class:"p-0"},{default:n(()=>[_.value?(a(),l("div",z,[r(A,{class:"text-primary"})])):f.value.length===0?(a(),l("div",M," No API keys ")):(a(),l("table",T,[e[7]||(e[7]=t("thead",null,[t("tr",{class:"border-b border-border"},[t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Label"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Prefix"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Created"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Last Used"),t("th",{class:"px-4 py-3"})])],-1)),t("tbody",null,[(a(!0),l(P,null,j(f.value,s=>(a(),l("tr",{key:s.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",U,u(s.label),1),t("td",E,u(s.prefix)+"...",1),t("td",H,u(h(k)(s.created_at)),1),t("td",S,u(s.last_used?h(k)(s.last_used):"Never"),1),t("td",q,[r(v,{variant:"ghost",size:"sm",class:"text-destructive",onClick:X=>C(s)},{default:n(()=>[...e[6]||(e[6]=[p(" Revoke ",-1)])]),_:1},8,["onClick"])])]))),128))])]))]),_:1})]),_:1}),r(V,{open:c.value,title:"Create API Key",onClose:e[3]||(e[3]=s=>c.value=!1)},{footer:n(()=>[r(v,{variant:"outline",onClick:e[2]||(e[2]=s=>c.value=!1)},{default:n(()=>[p(u(d.value?"Done":"Cancel"),1)]),_:1}),d.value?I("",!0):(a(),F(v,{key:0,loading:x.value,onClick:w},{default:n(()=>[...e[10]||(e[10]=[p(" Create ",-1)])]),_:1},8,["loading"]))]),default:n(()=>[t("div",G,[d.value?(a(),l("div",J,[e[8]||(e[8]=t("p",{class:"text-xs text-emerald-400 font-medium mb-1"},"Key created — save it now!",-1)),t("p",O,u(d.value),1)])):(a(),l("div",Q,[e[9]||(e[9]=t("label",{class:"text-sm font-medium text-foreground"},"Label",-1)),r(N,{modelValue:m.value,"onUpdate:modelValue":e[1]||(e[1]=s=>m.value=s),placeholder:"e.g. claude-collector",disabled:x.value},null,8,["modelValue","disabled"])]))])]),_:1},8,["open"])]))}});export{le as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
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-yrXqsixb.js";import{_ as A,a as D}from"./CardContent.vue_vue_type_script_setup_true_lang-BZS0eQer.js";import{_ as N}from"./Button.vue_vue_type_script_setup_true_lang-XMbqbqq8.js";import"./utils-D_0J15Md.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};
|
||||
1
src/static/assets/LiveView-Df9pKcnA.js
Normal file
1
src/static/assets/LiveView-Df9pKcnA.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{E as T,q as y,d as J,u as O,v as V,c as f,a as o,n as b,h as l,t as v,k as $,w as x,i as k,e as C,o as c,p as w,F as B,r as F,j as z}from"./index-DzSm5_bv.js";import{_ as A,a as D}from"./CardContent.vue_vue_type_script_setup_true_lang-B899D1fp.js";import{_ as N}from"./Button.vue_vue_type_script_setup_true_lang-D97aKlXO.js";import"./utils-7WVCegLb.js";import"./Spinner.vue_vue_type_script_setup_true_lang-DxuuceC3.js";function U(E){const e=y([]),i=y(!1),m=y(null);let s=null,r=null,u=!1;function p(){if(!u)try{s=new EventSource(E),s.onopen=()=>{i.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=()=>{i.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,i.value=!1}function h(){e.value=[]}return T(()=>{_()}),{events:e,connected:i,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"},q={class:"flex items-center gap-2"},M={class:"text-xs text-muted-foreground"},P={key:0,class:"mb-4 text-xs text-amber-400 bg-amber-500/10 border border-amber-500/30 rounded px-3 py-2"},W={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"},ne=J({__name:"LiveView",setup(E){const e=O(),i=e.getToken(),m=`/cc-dashboard/api/events${i?`?token=${encodeURIComponent(i)}`:""}`,{events:s,connected:r,error:u,connect:p,clearEvents:_}=U(m);V(()=>{e.isAuthenticated&&i&&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",q,[o("div",{class:b(["h-2 w-2 rounded-full",l(r)?"bg-emerald-500 animate-pulse":"bg-red-500"])},null,2),o("span",M,v(l(r)?"Connected":"Disconnected"),1)]),l(r)?k("",!0):(c(),$(N,{key:0,variant:"outline",size:"sm",onClick:l(p)},{default:x(()=>[...a[0]||(a[0]=[w(" Reconnect ",-1)])]),_:1},8,["onClick"])),C(N,{variant:"ghost",size:"sm",onClick:l(_)},{default:x(()=>[...a[1]||(a[1]=[w(" Clear ",-1)])]),_:1},8,["onClick"])]),l(u)&&!l(r)?(c(),f("div",P,v(l(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",W,[...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{ne as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
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-yrXqsixb.js";import{_ as S}from"./Button.vue_vue_type_script_setup_true_lang-XMbqbqq8.js";import{_ as p}from"./Input.vue_vue_type_script_setup_true_lang-Bo0JoDsF.js";import{_ as N,a as j}from"./CardContent.vue_vue_type_script_setup_true_lang-BZS0eQer.js";import"./utils-D_0J15Md.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};
|
||||
1
src/static/assets/LoginView-DzMVaLbC.js
Normal file
1
src/static/assets/LoginView-DzMVaLbC.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{d as h,u as f,c as o,a as t,b as m,e as a,w as d,o as r,f as g,g as p,h as i,t as x,i as w}from"./index-DzSm5_bv.js";import{_ as y,a as b}from"./CardContent.vue_vue_type_script_setup_true_lang-B899D1fp.js";import"./utils-7WVCegLb.js";const v={class:"min-h-screen flex items-center justify-center bg-background p-4"},_={class:"w-full max-w-sm"},k={class:"space-y-4"},C={key:0,class:"rounded-md bg-destructive/10 border border-destructive/30 px-3 py-2 text-sm text-destructive"},B=["disabled"],V={key:0},S={key:1},M=h({__name:"LoginView",setup(F){const c=g(),l=p(),s=f();async function u(){try{await s.loginWithMicrosoft();const n=l.query.redirect;c.push(n??"/")}catch{}}return(n,e)=>(r(),o("div",v,[t("div",_,[e[2]||(e[2]=m('<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(y,null,{default:d(()=>[a(b,{class:"pt-6"},{default:d(()=>[t("div",k,[i(s).error?(r(),o("div",C,x(i(s).error),1)):w("",!0),t("button",{type:"button",disabled:i(s).loading,class:"w-full flex items-center justify-center gap-3 rounded-md border border-border bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",onClick:u},[e[0]||(e[0]=t("svg",{class:"h-5 w-5 shrink-0",viewBox:"0 0 21 21",fill:"none",xmlns:"http://www.w3.org/2000/svg"},[t("rect",{x:"1",y:"1",width:"9",height:"9",fill:"#F25022"}),t("rect",{x:"11",y:"1",width:"9",height:"9",fill:"#7FBA00"}),t("rect",{x:"1",y:"11",width:"9",height:"9",fill:"#00A4EF"}),t("rect",{x:"11",y:"11",width:"9",height:"9",fill:"#FFB900"})],-1)),i(s).loading?(r(),o("span",V,"Signing in…")):(r(),o("span",S,"Sign in with Microsoft"))],8,B),e[1]||(e[1]=t("p",{class:"text-center text-xs text-muted-foreground"}," Use your @oliver.agency account ",-1))])]),_:1})]),_:1})])]))}});export{M as default};
|
||||
1
src/static/assets/PlannerView-DJPGnDPz.js
Normal file
1
src/static/assets/PlannerView-DJPGnDPz.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{c as r}from"./utils-D_0J15Md.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-yrXqsixb.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-7WVCegLb.js";import{d as s,o as n,c as t,n as l,h as c,a as d,z as u}from"./index-DzSm5_bv.js";const h=s({__name:"Progress",props:{value:{},max:{default:100},class:{},color:{default:"default"}},setup(a){const e=a,o=()=>Math.min(100,Math.max(0,e.value/e.max*100));return(i,m)=>(n(),t("div",{class:l(c(r)("relative h-2 w-full overflow-hidden rounded-full bg-secondary",e.class))},[d("div",{class:l(["h-full rounded-full transition-all duration-300",{"bg-primary":a.color==="default","bg-emerald-500":a.color==="success","bg-amber-500":a.color==="warning","bg-red-500":a.color==="danger"}]),style:u({width:`${o()}%`})},null,6)],2))}});export{h as _};
|
||||
1
src/static/assets/ProjectDetailView-2QTgygyj.js
Normal file
1
src/static/assets/ProjectDetailView-2QTgygyj.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{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-yrXqsixb.js";import{d as w}from"./dashboard-Bay5szWb.js";import{a as C,_ as $}from"./CardContent.vue_vue_type_script_setup_true_lang-BZS0eQer.js";import{_ as B}from"./Progress.vue_vue_type_script_setup_true_lang-CI2N8P-o.js";import{_ as N,f as V,a as D}from"./utils-D_0J15Md.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??0).toFixed(0))+"% ",3)]),d(B,{value:t.progress_pct,color:x(t.progress_pct)},null,8,["value","color"])])):i("",!0)]),_:2},1024)]),_:2},1032,["onClick"]))),128))]))]))}});export{st as default};
|
||||
1
src/static/assets/ProjectsView-VxSshwHq.js
Normal file
1
src/static/assets/ProjectsView-VxSshwHq.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{d as p,v as g,c as r,a as s,e as d,F as v,r as y,q as _,o,k as h,w as f,t as a,i,h as u,n as b,f as k}from"./index-DzSm5_bv.js";import{d as w}from"./dashboard-uOtmhTNc.js";import{a as C,_ as $}from"./CardContent.vue_vue_type_script_setup_true_lang-B899D1fp.js";import{_ as B}from"./Progress.vue_vue_type_script_setup_true_lang-DK67Z5Fm.js";import{_ as N}from"./Spinner.vue_vue_type_script_setup_true_lang-DxuuceC3.js";import{f as V,a as D}from"./utils-7WVCegLb.js";const F={class:"p-6"},z={key:0,class:"flex items-center justify-center h-40"},L={key:1,class:"text-center text-muted-foreground py-12"},P={key:2,class:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"},S={class:"flex items-start justify-between gap-2 mb-3"},j={class:"min-w-0"},q={class:"font-semibold text-sm text-foreground truncate"},A={key:0,class:"text-xs text-muted-foreground truncate"},E={key:0,class:"text-xs bg-muted text-muted-foreground px-1.5 py-0.5 rounded shrink-0"},M={class:"space-y-1.5"},R={class:"flex items-center justify-between text-xs"},T={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"},et=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",z,[d(N,{size:"lg",class:"text-primary"})])):l.value.length===0?(o(),r("div",L," No projects found ")):(o(),r("div",P,[(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",S,[s("div",j,[s("p",q,a(t.display_name),1),t.client?(o(),r("p",A,a(t.client),1)):i("",!0)]),t.job_number?(o(),r("span",E,a(t.job_number),1)):i("",!0)]),s("div",M,[s("div",R,[e[0]||(e[0]=s("span",{class:"text-muted-foreground"},"Total hours",-1)),s("span",T,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??0).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{et as default};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/static/assets/SettingsView-DsEb6gx-.js
Normal file
1
src/static/assets/SettingsView-DsEb6gx-.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{d as l,o as n,c as o,n as t,a as r}from"./index-DzSm5_bv.js";const i=l({__name:"Spinner",props:{size:{},class:{}},setup(s){return(a,e)=>(n(),o("svg",{class:t(["animate-spin text-current",s.size==="sm"?"h-3 w-3":s.size==="lg"?"h-6 w-6":"h-4 w-4",a.$props.class]),xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},[...e[0]||(e[0]=[r("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor","stroke-width":"4"},null,-1),r("path",{class:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"},null,-1)])],2))}});export{i as _};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{D as e}from"./index-yrXqsixb.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-DzSm5_bv.js";const i={users:()=>e.get("/api/admin/users"),keys:()=>e.get("/api/keys"),createKey:s=>e.post("/api/keys",s),revokeKey:s=>e.delete(`/api/keys/${s}`)};export{i as a};
|
||||
|
|
@ -1 +1 @@
|
|||
import{D as t}from"./index-yrXqsixb.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-DzSm5_bv.js";const e={summary:a=>t.get("/api/dashboard/summary",{params:a}),projects:a=>t.get("/api/dashboard/projects",{params:a}),timeline:a=>t.get("/api/dashboard/timeline",{params:a}),monthly:a=>t.get("/api/dashboard/monthly",{params:a}),dow:a=>t.get("/api/dashboard/dow",{params:a}),tools:a=>t.get("/api/dashboard/tools",{params:a}),activity:a=>t.get("/api/dashboard/activity",{params:a}),calendar:a=>t.get("/api/dashboard/calendar",{params:a}),project:(a,o)=>t.get("/api/dashboard/project/"+a,{params:o})};export{e as d};
|
||||
|
|
@ -1 +1 @@
|
|||
import{D as s,B as I,r as o}from"./index-yrXqsixb.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,A as I,q as o}from"./index-DzSm5_bv.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),l=o([]),r=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;r.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{r.value=!1}}async function y(t){n.value=!0;try{const a=await i.workItems(t);l.value=a.data}catch{l.value=[]}finally{n.value=!1}}return{integration:e,workItems:l,syncing:r,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
46
src/static/assets/index-DzSm5_bv.js
Normal file
46
src/static/assets/index-DzSm5_bv.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/static/assets/utils-7WVCegLb.js
Normal file
1
src/static/assets/utils-7WVCegLb.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -14,8 +14,8 @@
|
|||
else { document.documentElement.classList.remove('dark'); }
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/cc-dashboard/static/assets/index-yrXqsixb.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/cc-dashboard/static/assets/index-BXlrCgg3.css">
|
||||
<script type="module" crossorigin src="/cc-dashboard/static/assets/index-DzSm5_bv.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/cc-dashboard/static/assets/index-C-CL-7fz.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
5
web/.env.example
Normal file
5
web/.env.example
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Azure AD SSO
|
||||
VITE_AZURE_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
VITE_AZURE_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
|
||||
VITE_AZURE_REDIRECT_URI=https://optical-dev.oliver.solutions/cc-dashboard/
|
||||
# For local dev, change redirect URI to: http://localhost:5173/
|
||||
5924
web/package-lock.json
generated
Normal file
5924
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -30,7 +30,8 @@
|
|||
"tailwind-merge": "^2.4.0",
|
||||
"radix-vue": "^1.9.9",
|
||||
"@radix-icons/vue": "^1.0.0",
|
||||
"lucide-vue-next": "^0.427.0"
|
||||
"lucide-vue-next": "^0.427.0",
|
||||
"@azure/msal-browser": "^3.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.0",
|
||||
|
|
|
|||
18
web/src/api/msal.ts
Normal file
18
web/src/api/msal.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/// <reference types="vite/client" />
|
||||
import { PublicClientApplication } from '@azure/msal-browser'
|
||||
|
||||
export const msalInstance = new PublicClientApplication({
|
||||
auth: {
|
||||
clientId: import.meta.env.VITE_AZURE_CLIENT_ID as string,
|
||||
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_AZURE_TENANT_ID}`,
|
||||
redirectUri: import.meta.env.VITE_AZURE_REDIRECT_URI as string,
|
||||
},
|
||||
cache: { cacheLocation: 'sessionStorage', storeAuthStateInCookie: false },
|
||||
})
|
||||
|
||||
export const LOGIN_SCOPES = ['openid', 'profile', 'email']
|
||||
|
||||
export async function initMsal(): Promise<void> {
|
||||
await msalInstance.initialize()
|
||||
await msalInstance.handleRedirectPromise()
|
||||
}
|
||||
|
|
@ -5,23 +5,26 @@ import App from './App.vue'
|
|||
import router from './router'
|
||||
import { setupInterceptors } from './api/client'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
import { initMsal } from './api/msal'
|
||||
import './styles/globals.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
// Initialize MSAL before mounting — handles redirect callback if present
|
||||
initMsal().then(() => {
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(VueQueryPlugin)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(VueQueryPlugin)
|
||||
|
||||
// Setup axios interceptors after pinia is installed
|
||||
const authStore = useAuthStore()
|
||||
setupInterceptors(
|
||||
() => authStore.getToken(),
|
||||
() => {
|
||||
authStore.logout()
|
||||
router.push({ name: 'login' })
|
||||
}
|
||||
)
|
||||
const authStore = useAuthStore()
|
||||
setupInterceptors(
|
||||
() => authStore.getToken(),
|
||||
() => {
|
||||
authStore.logout()
|
||||
router.push({ name: 'login' })
|
||||
}
|
||||
)
|
||||
|
||||
app.mount('#app')
|
||||
app.mount('#app')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import apiClient from '@/api/client'
|
||||
import { msalInstance, LOGIN_SCOPES } from '@/api/msal'
|
||||
import type { UserOut } from '@/types'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
|
|
@ -13,28 +14,35 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
const isAuthenticated = computed(() => token.value !== null)
|
||||
const isAdmin = computed(() => user.value?.role === 'admin')
|
||||
|
||||
async function login(email: string, password: string): Promise<void> {
|
||||
async function loginWithMicrosoft(): Promise<void> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await apiClient.post<{ access_token: string; token_type: string }>(
|
||||
'/api/auth/login',
|
||||
{ email, password }
|
||||
const result = await msalInstance.loginPopup({ scopes: LOGIN_SCOPES })
|
||||
const idToken = result.idToken
|
||||
const res = await apiClient.post<{ access_token: string; refresh_token: string }>(
|
||||
'/api/auth/microsoft',
|
||||
{ id_token: idToken }
|
||||
)
|
||||
token.value = res.data.access_token
|
||||
await fetchMe()
|
||||
} catch (err: unknown) {
|
||||
const axiosError = err as { response?: { data?: { detail?: string } } }
|
||||
error.value = axiosError.response?.data?.detail ?? 'Login failed'
|
||||
const axiosError = err as { response?: { data?: { detail?: string } }; message?: string }
|
||||
error.value = axiosError.response?.data?.detail ?? axiosError.message ?? 'Login failed'
|
||||
throw err
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function logout(): void {
|
||||
async function logout(): Promise<void> {
|
||||
token.value = null
|
||||
user.value = null
|
||||
try {
|
||||
await msalInstance.clearCache()
|
||||
} catch {
|
||||
// clearCache may fail if no active account — safe to ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchMe(): Promise<void> {
|
||||
|
|
@ -53,7 +61,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
error,
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
login,
|
||||
loginWithMicrosoft,
|
||||
logout,
|
||||
fetchMe,
|
||||
getToken,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import Input from '@/components/ui/Input.vue'
|
||||
import Card from '@/components/ui/Card.vue'
|
||||
import CardContent from '@/components/ui/CardContent.vue'
|
||||
|
||||
|
|
@ -11,12 +8,9 @@ const router = useRouter()
|
|||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const email = ref('')
|
||||
const password = ref('')
|
||||
|
||||
async function handleLogin() {
|
||||
try {
|
||||
await authStore.login(email.value, password.value)
|
||||
await authStore.loginWithMicrosoft()
|
||||
const redirect = route.query.redirect as string | undefined
|
||||
router.push(redirect ?? '/')
|
||||
} catch {
|
||||
|
|
@ -42,7 +36,7 @@ async function handleLogin() {
|
|||
|
||||
<Card>
|
||||
<CardContent class="pt-6">
|
||||
<form class="space-y-4" @submit.prevent="handleLogin">
|
||||
<div class="space-y-4">
|
||||
<!-- Error -->
|
||||
<div
|
||||
v-if="authStore.error"
|
||||
|
|
@ -51,40 +45,28 @@ async function handleLogin() {
|
|||
{{ authStore.error }}
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label for="email" class="text-sm font-medium text-foreground">Email</label>
|
||||
<Input
|
||||
id="email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
placeholder="you@company.com"
|
||||
autocomplete="email"
|
||||
:disabled="authStore.loading"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label for="password" class="text-sm font-medium text-foreground">Password</label>
|
||||
<Input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
autocomplete="current-password"
|
||||
:disabled="authStore.loading"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
:loading="authStore.loading"
|
||||
<!-- Microsoft Sign In button -->
|
||||
<button
|
||||
type="button"
|
||||
:disabled="authStore.loading"
|
||||
class="w-full flex items-center justify-center gap-3 rounded-md border border-border bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
@click="handleLogin"
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</form>
|
||||
<!-- Microsoft logo -->
|
||||
<svg class="h-5 w-5 shrink-0" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="1" width="9" height="9" fill="#F25022"/>
|
||||
<rect x="11" y="1" width="9" height="9" fill="#7FBA00"/>
|
||||
<rect x="1" y="11" width="9" height="9" fill="#00A4EF"/>
|
||||
<rect x="11" y="11" width="9" height="9" fill="#FFB900"/>
|
||||
</svg>
|
||||
<span v-if="authStore.loading">Signing in…</span>
|
||||
<span v-else>Sign in with Microsoft</span>
|
||||
</button>
|
||||
|
||||
<p class="text-center text-xs text-muted-foreground">
|
||||
Use your @oliver.agency account
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue