feat: Reinitialize frontend with SvelteKit and TypeScript

- Delete old Vite+Svelte frontend
- Initialize new SvelteKit project with TypeScript
- Configure Tailwind CSS v4 + DaisyUI
- Implement JWT authentication with auto-refresh
- Create login page with form validation (Zod)
- Add protected route guards
- Update Docker configuration for single-stage build
- Add E2E tests with Playwright (6/11 passing)
- Fix Svelte 5 reactivity with $state() runes

Known issues:
- 5 E2E tests failing (timing/async issues)
- Token refresh implementation needs debugging
- Validation error display timing
This commit is contained in:
2026-02-17 16:19:59 -05:00
parent 54df6018f5
commit de2d83092e
28274 changed files with 3816354 additions and 90 deletions

View File

@@ -0,0 +1 @@
var D=Object.defineProperty;var g=i=>{throw TypeError(i)};var F=(i,e,s)=>e in i?D(i,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):i[e]=s;var y=(i,e,s)=>F(i,typeof e!="symbol"?e+"":e,s),w=(i,e,s)=>e.has(i)||g("Cannot "+s);var t=(i,e,s)=>(w(i,e,"read from private field"),s?s.call(i):e.get(i)),l=(i,e,s)=>e.has(i)?g("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,s),M=(i,e,s,a)=>(w(i,e,"write to private field"),a?a.call(i,s):e.set(i,s),s);import{R as A,T as C,U as k,V as G,y as x,W as B,A as R,G as S,X as T,Y as U}from"./CCV2x70u.js";var r,n,h,u,p,_,v;class X{constructor(e,s=!0){y(this,"anchor");l(this,r,new Map);l(this,n,new Map);l(this,h,new Map);l(this,u,new Set);l(this,p,!0);l(this,_,()=>{var e=A;if(t(this,r).has(e)){var s=t(this,r).get(e),a=t(this,n).get(s);if(a)C(a),t(this,u).delete(s);else{var c=t(this,h).get(s);c&&(t(this,n).set(s,c.effect),t(this,h).delete(s),c.fragment.lastChild.remove(),this.anchor.before(c.fragment),a=c.effect)}for(const[f,o]of t(this,r)){if(t(this,r).delete(f),f===e)break;const d=t(this,h).get(o);d&&(k(d.effect),t(this,h).delete(o))}for(const[f,o]of t(this,n)){if(f===s||t(this,u).has(f))continue;const d=()=>{if(Array.from(t(this,r).values()).includes(f)){var b=document.createDocumentFragment();T(o,b),b.append(x()),t(this,h).set(f,{effect:o,fragment:b})}else k(o);t(this,u).delete(f),t(this,n).delete(f)};t(this,p)||!a?(t(this,u).add(f),G(o,d,!1)):d()}}});l(this,v,e=>{t(this,r).delete(e);const s=Array.from(t(this,r).values());for(const[a,c]of t(this,h))s.includes(a)||(k(c.effect),t(this,h).delete(a))});this.anchor=e,M(this,p,s)}ensure(e,s){var a=A,c=U();if(s&&!t(this,n).has(e)&&!t(this,h).has(e))if(c){var f=document.createDocumentFragment(),o=x();f.append(o),t(this,h).set(e,{effect:B(()=>s(o)),fragment:f})}else t(this,n).set(e,B(()=>s(this.anchor)));if(t(this,r).set(a,e),c){for(const[d,m]of t(this,n))d===e?a.unskip_effect(m):a.skip_effect(m);for(const[d,m]of t(this,h))d===e?a.unskip_effect(m.effect):a.skip_effect(m.effect);a.oncommit(t(this,_)),a.ondiscard(t(this,v))}else R&&(this.anchor=S),t(this,_).call(this)}}r=new WeakMap,n=new WeakMap,h=new WeakMap,u=new WeakMap,p=new WeakMap,_=new WeakMap,v=new WeakMap;export{X as B};

View File

@@ -0,0 +1 @@
import{a4 as d,a5 as g,a6 as c,v as m,a7 as i,a8 as b,f as p,a9 as v,u as h,aa as k}from"./CCV2x70u.js";function x(t=!1){const a=d,e=a.l.u;if(!e)return;let o=()=>v(a.s);if(t){let n=0,s={};const _=h(()=>{let l=!1;const r=a.s;for(const f in r)r[f]!==s[f]&&(s[f]=r[f],l=!0);return l&&n++,n});o=()=>p(_)}e.b.length&&g(()=>{u(a,o),i(e.b)}),c(()=>{const n=m(()=>e.m.map(b));return()=>{for(const s of n)typeof s=="function"&&s()}}),e.a.length&&c(()=>{u(a,o),i(e.a)})}function u(t,a){if(t.l.s)for(const e of t.l.s)p(e);a()}k();export{x as i};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{g as L,d as D,P as T,f as P,h as b,i as B,j as Y,D as h,k as x,m as M,n as N,o as U,q,S as w,L as $,u as j,v as z,w as C,x as G}from"./CCV2x70u.js";import{c as Z}from"./JkAhLmb1.js";function H(r,a,t,s){var o;var f=!x||(t&M)!==0,v=(t&N)!==0,O=(t&G)!==0,n=s,l=!0,g=()=>(l&&(l=!1,n=O?z(s):s),n),u;if(v){var R=w in r||$ in r;u=((o=L(r,a))==null?void 0:o.set)??(R&&a in r?e=>r[a]=e:void 0)}var _,I=!1;v?[_,I]=Z(()=>r[a]):_=r[a],_===void 0&&s!==void 0&&(_=g(),u&&(f&&D(),u(_)));var i;if(f?i=()=>{var e=r[a];return e===void 0?g():(l=!0,e)}:i=()=>{var e=r[a];return e!==void 0&&(n=void 0),e===void 0?n:e},f&&(t&T)===0)return i;if(u){var m=r.$$legacy;return(function(e,S){return arguments.length>0?((!f||!S||m||I)&&u(S?i():e),e):i()})}var c=!1,d=((t&C)!==0?j:U)(()=>(c=!1,i()));v&&P(d);var A=Y;return(function(e,S){if(arguments.length>0){const E=S?P(d):f&&v?b(e):e;return B(d,E),c=!0,n!==void 0&&(n=E),e}return q&&c||(A.f&h)!==0?d.v:P(d)})}export{H as p};

View File

@@ -0,0 +1 @@
import{z as T,A as o,J as h,K as A,M as b,N as p,O as E,Q as R,F as g,E as l}from"./CCV2x70u.js";import{B as v}from"./BG30BmlR.js";function S(t,u,_=!1){o&&h();var n=new v(t),c=_?A:0;function i(a,r){if(o){const e=b(t);var s;if(e===p?s=0:e===E?s=!1:s=parseInt(e.substring(1)),a!==s){var f=R();g(f),n.anchor=f,l(!1),n.ensure(a,r),l(!0);return}}n.ensure(a,r)}T(()=>{var a=!1;u((r,s=0)=>{a=!0,i(s,r)}),a||i(!1,null)},c)}export{S as i};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{ab as E,ac as A}from"./CCV2x70u.js";const S="http://localhost:3000/api",y="headroom_access_token",k="headroom_refresh_token";function T(){return typeof localStorage>"u"?null:localStorage.getItem(y)}function b(){return typeof localStorage>"u"?null:localStorage.getItem(k)}function _(e,t){typeof localStorage>"u"||(localStorage.setItem(y,e),localStorage.setItem(k,t))}function w(){typeof localStorage>"u"||(localStorage.removeItem(y),localStorage.removeItem(k))}class L extends Error{constructor(t,o,r){super(t),this.name="ApiError",this.status=o,this.data=r}}let f=!1,h=[];function R(e){h.push(e)}function v(e){h.forEach(t=>{t(e)}),h=[]}async function j(){const e=b();if(!e)throw new Error("No refresh token available");const t=await fetch(`${S}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refresh_token:e})});if(!t.ok)throw w(),new Error("Token refresh failed");const o=await t.json();return _(o.access_token,o.refresh_token),o.access_token}async function u(e,t={}){const o=`${S}${e}`,r={"Content-Type":"application/json",...t.headers},n=T();n&&(r.Authorization=`Bearer ${n}`);const c={...t,headers:r};t.body&&typeof t.body=="object"&&(c.body=JSON.stringify(t.body));try{let a=await fetch(o,c);if(a.status===401&&n){if(!f){f=!0;try{const i=await j();f=!1,v(i)}catch{throw f=!1,h=[],w(),typeof window<"u"&&window.dispatchEvent(new CustomEvent("auth:logout")),new L("Session expired. Please log in again.",401,null)}}return new Promise((i,l)=>{R(g=>{c.headers={...c.headers,Authorization:`Bearer ${g}`},fetch(o,c).then(P=>m(P)).then(i).catch(l)})})}return m(a)}catch(a){throw a}}async function m(e){var n,c,a,i;const t=((c=(n=e.headers)==null?void 0:n.get)==null?void 0:c.call(n,"content-type"))||((i=(a=e.headers)==null?void 0:a.get)==null?void 0:i.call(a,"Content-Type")),r=t&&t.includes("application/json")?await e.json():await e.text();if(!e.ok){const l=typeof r=="object"?r:{message:r},g=l.message||"API request failed";throw new L(g,e.status,l)}return r}const p={get:(e,t={})=>u(e,{...t,method:"GET"}),post:(e,t,o={})=>u(e,{...o,method:"POST",body:t}),put:(e,t,o={})=>u(e,{...o,method:"PUT",body:t}),patch:(e,t,o={})=>u(e,{...o,method:"PATCH",body:t}),delete:(e,t={})=>u(e,{...t,method:"DELETE"})},I={login:e=>p.post("/auth/login",e),logout:()=>p.post("/auth/logout"),refresh:()=>p.post("/auth/refresh",{refresh_token:b()})};function C(){const{subscribe:e,set:t,update:o}=A(null);return{subscribe:e,set:t,update:o,clear:()=>t(null)}}const d=C();function O(){const{subscribe:e,set:t,update:o}=A({isAuthenticated:!1,isLoading:!1,error:null});return{subscribe:e,set:t,update:o,setLoading:r=>o(n=>({...n,isLoading:r})),setError:r=>o(n=>({...n,error:r})),clearError:()=>o(r=>({...r,error:null})),setAuthenticated:r=>o(n=>({...n,isAuthenticated:r}))}}const s=O(),x=E([d,s],([e,t])=>e!==null&&t.isAuthenticated);E(d,e=>(e==null?void 0:e.role)||null);function J(){T()&&s.setAuthenticated(!0)}async function K(e){s.setLoading(!0),s.clearError();try{const t=await I.login(e);if(t.access_token&&t.refresh_token)return _(t.access_token,t.refresh_token),d.set(t.user||null),s.setAuthenticated(!0),{success:!0,user:t.user};throw new Error("Invalid response from server")}catch(t){const o=t instanceof Error?t.message:"Login failed";return s.setError(o),{success:!1,error:o}}finally{s.setLoading(!1)}}async function q(){s.setLoading(!0);try{await I.logout()}catch(e){console.error("Logout API error:",e)}finally{w(),d.clear(),s.setAuthenticated(!1),s.setLoading(!1)}}export{J as a,K as b,s as c,x as i,q as l,d as u};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{y as u,z as o,H as _,A as t,C as g,B as i,E as l,F as d,G as p,I as E}from"./CCV2x70u.js";function v(n,r){let s=null,y=t;var a;if(t){s=p;for(var e=E(document.head);e!==null&&(e.nodeType!==g||e.data!==n);)e=i(e);if(e===null)l(!1);else{var f=i(e);e.remove(),d(f)}}t||(a=document.head.appendChild(u()));try{o(()=>r(a),_)}finally{y&&(l(!0),d(s))}}export{v as h};

View File

@@ -0,0 +1 @@
import{Z as c,_ as l,a0 as f,a1 as b,a2 as o,i as d,a3 as p,f as _}from"./CCV2x70u.js";let s=!1,i=Symbol();function v(e,n,r){const u=r[n]??(r[n]={store:null,source:b(void 0),unsubscribe:f});if(u.store!==e&&!(i in r))if(u.unsubscribe(),u.store=e??null,e==null)u.source.v=void 0,u.unsubscribe=f;else{var a=!0;u.unsubscribe=o(e,t=>{a?u.source.v=t:d(u.source,t)}),a=!1}return e&&i in r?p(e):_(u.source)}function y(){const e={};function n(){c(()=>{for(var r in e)e[r].unsubscribe();l(e,i,{enumerable:!1,value:!0})})}return[e,n]}function N(e){var n=s;try{return s=!1,[e(),s]}finally{s=n}}export{v as a,N as c,y as s};

View File

@@ -0,0 +1 @@
import{ag as h,y as u,I as f,ah as E,j as c,ai as g,aj as w,A as i,G as s,ak as y,J as N,al as A,F as M,am as x}from"./CCV2x70u.js";var l;const d=((l=globalThis==null?void 0:globalThis.window)==null?void 0:l.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:t=>t});function L(t){return(d==null?void 0:d.createHTML(t))??t}function b(t,r=!1){var e=h("template");return t=t.replaceAll("<!>","<!---->"),e.innerHTML=r?L(t):t,e.content}function n(t,r){var e=c;e.nodes===null&&(e.nodes={start:t,end:r,a:null,t:null})}function P(t,r){var e=(r&g)!==0,m=(r&w)!==0,a,v=!t.startsWith("<!>");return()=>{if(i)return n(s,null),s;a===void 0&&(a=b(v?t:"<!>"+t,!0),e||(a=f(a)));var o=m||E?document.importNode(a,!0):a.cloneNode(!0);if(e){var T=f(o),p=o.lastChild;n(T,p)}else n(o,o);return o}}function R(t=""){if(!i){var r=u(t+"");return n(r,r),r}var e=s;return e.nodeType!==A?(e.before(e=u()),M(e)):x(e),n(e,e),e}function C(){if(i)return n(s,null),s;var t=document.createDocumentFragment(),r=document.createComment(""),e=u();return t.append(r,e),n(r,e),t}function D(t,r){if(i){var e=c;((e.f&y)===0||e.nodes.end===null)&&(e.nodes.end=s),N();return}t!==null&&t.before(r)}const I="5";var _;typeof window<"u"&&((_=window.__svelte??(window.__svelte={})).v??(_.v=new Set)).add(I);export{D as a,n as b,C as c,P as f,R as t};

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{l as o,a as r}from"../chunks/DBDqKY8A.js";export{o as load_css,r as start};

View File

@@ -0,0 +1 @@
import{a as m,f as b}from"../chunks/pJd4F_Tq.js";import{i as j}from"../chunks/BgHfHpED.js";import{A as N,J as P,p as k,b as A,s as u,c as r,r as s,t as R,ad as T,ae as U}from"../chunks/CCV2x70u.js";import{u as q,l as z,a as B}from"../chunks/CrZRXG6z.js";import{s as E,e as F}from"../chunks/Bx__7-vK.js";import{i as y}from"../chunks/CC5oASRR.js";import{s as G,a as I}from"../chunks/JkAhLmb1.js";import{g as K}from"../chunks/DBDqKY8A.js";function O(v,o,a,n,d){var c;N&&P();var e=(c=o.$$slots)==null?void 0:c[a],i=!1;e===!0&&(e=o.children,i=!0),e===void 0||e(v,i?()=>n:n)}var Q=b('<li><a href="/team-members">Team Members</a></li> <li><a href="/projects">Projects</a></li>',1),S=b('<div class="dropdown dropdown-end"><label tabindex="0" class="btn btn-ghost btn-circle avatar"><div class="w-10 rounded-full bg-primary"><span class="text-xl"> </span></div></label> <ul tabindex="0" class="mt-3 p-2 shadow menu menu-compact dropdown-content bg-base-100 rounded-box w-52"><li><a href="/dashboard" class="justify-between">Dashboard</a></li> <!> <li><a href="/reports">Reports</a></li> <div class="divider"></div> <li><button class="text-error">Logout</button></li></ul></div>'),V=b('<a href="/login" class="btn btn-primary btn-sm">Login</a>'),W=b('<nav class="navbar bg-base-100 shadow-lg"><div class="flex-1"><a href="/" class="btn btn-ghost normal-case text-xl">Headroom</a></div> <div class="flex-none gap-2"><!></div></nav>');function X(v,o){k(o,!1);const a=()=>I(q,"$user",n),[n,d]=G();async function e(){await z(),K("/login")}j();var i=W(),c=u(r(i),2),L=r(c);{var $=l=>{var f=S(),p=r(f),g=r(p),h=r(g),C=r(h,!0);s(h),s(g),s(p);var x=u(p,2),_=u(r(x),2);{var D=t=>{var J=Q();T(2),m(t,J)};y(_,t=>{(a().role==="superuser"||a().role==="manager")&&t(D)})}var w=u(_,6),H=r(w);s(w),s(x),s(f),R(t=>E(C,t),[()=>{var t;return(t=a().email)==null?void 0:t.charAt(0).toUpperCase()}]),F("click",H,e),m(l,f)},M=l=>{var f=V();m(l,f)};y(L,l=>{a()?l($):l(M,!1)})}s(c),s(i),m(v,i),A(),d()}var Y=b('<div class="min-h-screen bg-base-200"><!> <main class="container mx-auto px-4 py-6"><!></main></div>');function ia(v,o){k(o,!1),U(()=>{B()}),j();var a=Y(),n=r(a);X(n,{});var d=u(n,2),e=r(d);O(e,o,"default",{}),s(d),s(a),m(v,a),A()}export{ia as component};

View File

@@ -0,0 +1 @@
import{a as h,f as g}from"../chunks/pJd4F_Tq.js";import{i as l}from"../chunks/BgHfHpED.js";import{p as v,af as d,t as _,b as x,c as e,r as o,s as $}from"../chunks/CCV2x70u.js";import{s as p}from"../chunks/Bx__7-vK.js";import{s as b,p as m}from"../chunks/DBDqKY8A.js";const k={get error(){return m.error},get status(){return m.status}};b.updated.check;const f=k;var E=g("<h1> </h1> <p> </p>",1);function A(i,c){v(c,!1),l();var t=E(),r=d(t),n=e(r,!0);o(r);var s=$(r,2),u=e(s,!0);o(s),_(()=>{var a;p(n,f.status),p(u,(a=f.error)==null?void 0:a.message)}),h(i,t),x()}export{A as component};

View File

@@ -0,0 +1 @@
import{g as s}from"../chunks/DBDqKY8A.js";import{i}from"../chunks/CrZRXG6z.js";import{c,a as u}from"../chunks/pJd4F_Tq.js";import{z as l,K as f,af as m}from"../chunks/CCV2x70u.js";import{B as p}from"../chunks/BG30BmlR.js";function b(t,a,...e){var n=new p(t);l(()=>{const r=a()??null;n.ensure(r,r&&(o=>r(o,...e)))},f)}const d=async()=>{{let t=!1;if(i.subscribe(e=>{t=e})(),!t)return s("/login"),{authenticated:!1}}return{authenticated:!0}},T=Object.freeze(Object.defineProperty({__proto__:null,load:d},Symbol.toStringTag,{value:"Module"}));function A(t,a){var e=c(),n=m(e);b(n,()=>a.children),u(t,e)}export{A as component,T as universal};

View File

@@ -0,0 +1 @@
import{a as n,f as p}from"../chunks/pJd4F_Tq.js";import{i as m}from"../chunks/BgHfHpED.js";import{p as c,l as f,a as l,b as d}from"../chunks/CCV2x70u.js";import{s as g,a as _}from"../chunks/JkAhLmb1.js";import{g as e}from"../chunks/DBDqKY8A.js";import{i as u}from"../chunks/CrZRXG6z.js";var h=p('<div class="flex items-center justify-center min-h-screen"><div class="loading loading-spinner loading-lg text-primary"></div></div>');function j(s,a){c(a,!1);const t=()=>_(u,"$isAuthenticated",i),[i,o]=g();f(()=>(t(),e),()=>{t()?e("/dashboard"):e("/login")}),l(),m();var r=h();n(s,r),d(),o()}export{j as component};

View File

@@ -0,0 +1,2 @@
import{a as v,f as p}from"../chunks/pJd4F_Tq.js";import{i as k}from"../chunks/BgHfHpED.js";import{p as w,b as T,e as y,$ as A,c as s,s as r,r as t,t as L}from"../chunks/CCV2x70u.js";import{e as R,s as W}from"../chunks/Bx__7-vK.js";import{i as j}from"../chunks/CC5oASRR.js";import{h as F}from"../chunks/DhYTxIvM.js";import{s as H,a as J}from"../chunks/JkAhLmb1.js";import{u as M,l as q}from"../chunks/CrZRXG6z.js";import{g as z}from"../chunks/DBDqKY8A.js";var B=p('<div class="alert alert-info mb-4"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> <span> </span></div>'),C=p(`<div class="max-w-4xl mx-auto"><div class="card bg-base-100 shadow-xl"><div class="card-body"><h1 class="card-title text-3xl mb-4">Welcome to Headroom! 👋</h1> <!> <p class="text-lg mb-4">You have successfully authenticated. This is a protected dashboard page
that requires a valid JWT token to access.</p> <div class="divider"></div> <h2 class="text-xl font-bold mb-2">Authentication Features Implemented:</h2> <ul class="list-disc list-inside space-y-2 mb-6"><li>✅ JWT Token Authentication</li> <li>✅ Token Auto-refresh on 401</li> <li>✅ Protected Route Guards</li> <li>✅ Form Validation with Zod</li> <li>✅ Role-based Access Control</li> <li>✅ Redis Token Storage</li></ul> <div class="card-actions"><button class="btn btn-error">Logout</button></div></div></div></div>`);function K(u,h){w(h,!1);const o=()=>J(M,"$user",f),[f,b]=H();async function g(){await q(),z("/login")}k();var e=C();F("x1i5gj",a=>{y(()=>{A.title="Dashboard - Headroom"})});var l=s(e),n=s(l),d=r(s(n),2);{var x=a=>{var i=B(),m=r(s(i),2),$=s(m);t(m),t(i),L(()=>W($,`Logged in as ${o().email??""} (${o().role??""})`)),v(a,i)};j(d,a=>{o()&&a(x)})}var c=r(d,10),_=s(c);t(c),t(n),t(l),t(e),R("click",_,g),v(u,e),T(),b()}export{K as component};

File diff suppressed because one or more lines are too long