domma-cms 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/admin/css/dashboard.css +1 -0
  2. package/admin/dist/domma/domma-tools.css +3 -3
  3. package/admin/dist/domma/domma-tools.min.js +4 -4
  4. package/admin/index.html +1 -0
  5. package/admin/js/api.js +1 -1
  6. package/admin/js/app.js +1 -1
  7. package/admin/js/templates/dashboard/activity-feed.html +3 -0
  8. package/admin/js/templates/dashboard/health-detail.html +2 -0
  9. package/admin/js/templates/dashboard/journeys.html +17 -0
  10. package/admin/js/templates/dashboard/kpi-strip.html +34 -0
  11. package/admin/js/templates/dashboard/spike-feed.html +3 -0
  12. package/admin/js/templates/dashboard/top-pages.html +3 -0
  13. package/admin/js/templates/dashboard/traffic-chart.html +3 -0
  14. package/admin/js/templates/dashboard.html +22 -44
  15. package/admin/js/views/dashboard/lib/escape.js +1 -0
  16. package/admin/js/views/dashboard/widgets/activity-feed.js +1 -0
  17. package/admin/js/views/dashboard/widgets/health-detail.js +1 -0
  18. package/admin/js/views/dashboard/widgets/journeys.js +1 -0
  19. package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -0
  20. package/admin/js/views/dashboard/widgets/spike-feed.js +6 -0
  21. package/admin/js/views/dashboard/widgets/top-pages.js +1 -0
  22. package/admin/js/views/dashboard/widgets/traffic-chart.js +1 -0
  23. package/admin/js/views/dashboard.js +1 -1
  24. package/admin/js/views/form-editor.js +7 -7
  25. package/admin/js/views/index.js +1 -1
  26. package/config/plugins.json +3 -0
  27. package/package.json +2 -2
  28. package/plugins/analytics/daily.json +5 -0
  29. package/plugins/analytics/journeys.json +10 -0
  30. package/plugins/analytics/lifetime.json +25 -0
  31. package/plugins/analytics/plugin.js +231 -16
  32. package/plugins/analytics/public/inject-body.html +26 -2
  33. package/server/routes/api/dashboard.js +239 -0
  34. package/server/server.js +2 -0
  35. package/server/services/email.js +60 -20
  36. package/server/services/health.js +282 -0
  37. package/server/services/plugins.js +37 -5
package/admin/index.html CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  <!-- Admin CSS -->
19
19
  <link rel="stylesheet" href="/admin/css/admin.css?v=20260422-2">
20
+ <link rel="stylesheet" href="/admin/css/dashboard.css?v=1">
20
21
 
21
22
  <!-- Forms CSS — needed for conditional logic preview (.fb-field-hidden, etc.) -->
22
23
  <link rel="stylesheet" href="/public/css/forms.css">
package/admin/js/api.js CHANGED
@@ -1 +1 @@
1
- const d="/api";function a(){return S.get("auth_token")}function m(){return S.get("auth_refresh_token")}function l(e){S.set("auth_token",e)}function h(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function u(){const e=m();if(!e)throw new Error("No refresh token");const o=await fetch(`${d}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw h(),R.navigate("/login"),new Error("Token refresh failed");const{token:s}=await o.json();return l(s),s}async function t(e,o={}){let s=a();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let n=await fetch(`${d}${e}`,{...o,headers:r(s)});if(n.status===401&&m())try{s=await u(),n=await fetch(`${d}${e}`,{...o,headers:r(s)})}catch{return}if(!n.ok){const i=await n.json().catch(()=>({error:"Request failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.status===204?null:n.json()}async function y(e,o){const s=a(),r=s?{Authorization:`Bearer ${s}`}:{},n=await fetch(`${d}${e}`,{method:"POST",headers:r,body:o});if(!n.ok){const i=await n.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),create:e=>t("/layouts",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/layouts/${e}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/layouts/${e}`,{method:"DELETE"}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>y("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"})},plugins:{list:()=>t("/plugins",{method:"GET"}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"})},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o)=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${s?"?"+s:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,s)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:s})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/collections/${e}/public${s?"?"+s:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/views/${e}/execute${s?"?"+s:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),s=await fetch(`${d}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const c=await s.json().catch(()=>({}));throw new Error(c.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${d}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Block already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),s=await fetch(`${d}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const c=await s.json().catch(()=>({}));throw new Error(c.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${d}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Component already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:s}){e&&l(e),o&&S.set("auth_refresh_token",o),s&&S.set("auth_user",s)}export function logout(){const e=m();e&&fetch(`${d}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),h(),R.navigate("/login")}export{t as apiRequest,u as refreshAccessToken,a as getToken,h as clearAuth};
1
+ const d="/api";function a(){return S.get("auth_token")}function m(){return S.get("auth_refresh_token")}function l(e){S.set("auth_token",e)}function h(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function u(){const e=m();if(!e)throw new Error("No refresh token");const o=await fetch(`${d}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw h(),R.navigate("/login"),new Error("Token refresh failed");const{token:s}=await o.json();return l(s),s}async function t(e,o={}){let s=a();const r=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let n=await fetch(`${d}${e}`,{...o,headers:r(s)});if(n.status===401&&m())try{s=await u(),n=await fetch(`${d}${e}`,{...o,headers:r(s)})}catch{return}if(!n.ok){const i=await n.json().catch(()=>({error:"Request failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.status===204?null:n.json()}async function y(e,o){const s=a(),r=s?{Authorization:`Bearer ${s}`}:{},n=await fetch(`${d}${e}`,{method:"POST",headers:r,body:o});if(!n.ok){const i=await n.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)}),getCustomCss:()=>t("/settings/custom-css",{method:"GET"}),saveCustomCss:e=>t("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:e})})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),create:e=>t("/layouts",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/layouts/${e}`,{method:"PUT",body:JSON.stringify(o)}),remove:e=>t(`/layouts/${e}`,{method:"DELETE"}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>y("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"})},plugins:{list:()=>t("/plugins",{method:"GET"}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"})},marketplace:{catalogue:()=>t("/plugins/marketplace",{method:"GET"}),install:(e,o)=>t("/plugins/marketplace/install",{method:"POST",body:JSON.stringify({slug:e,version:o})}),uninstall:e=>t(`/plugins/marketplace/${encodeURIComponent(e)}`,{method:"DELETE"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${s?"?"+s:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,s)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:s})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/collections/${e}/public${s?"?"+s:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)}),migrateStorage:(e,o)=>t(`/collections/${e}/migrate-storage`,{method:"POST",body:JSON.stringify({storage:o})})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/views/${e}/execute${s?"?"+s:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},versions:{list:e=>t(`/versions/list${e}`),get:(e,o)=>t(`/versions/get/${encodeURIComponent(o)}${e}`),create:(e,o)=>t(`/versions/create${e}`,{method:"POST",body:JSON.stringify({label:o})}),restore:(e,o)=>t(`/versions/restore/${encodeURIComponent(o)}${e}`,{method:"POST"}),delete:(e,o)=>t(`/versions/delete/${encodeURIComponent(o)}${e}`,{method:"DELETE"}),bulkDelete:(e,o)=>t(`/versions/bulk-delete${e}`,{method:"POST",body:JSON.stringify({filenames:o})}),prune:(e,o)=>t(`/versions/prune${e}`,{method:"POST",body:JSON.stringify({keep:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),s=await fetch(`${d}/blocks/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const c=await s.json().catch(()=>({}));throw new Error(c.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmblock.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${d}/blocks/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Block already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},components:{list:()=>t("/components",{method:"GET"}),get:e=>t(`/components/${encodeURIComponent(e)}`,{method:"GET"}),compile:(e,o)=>t("/components/compile",{method:"POST",body:JSON.stringify({name:e,source:o})}),put:(e,o)=>t(`/components/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/components/${encodeURIComponent(e)}`,{method:"DELETE"}),async exportBundle(e){const o=a(),s=await fetch(`${d}/components/${encodeURIComponent(e)}/export`,{headers:o?{Authorization:`Bearer ${o}`}:{}});if(!s.ok){const c=await s.json().catch(()=>({}));throw new Error(c.error||`Export failed (${s.status})`)}const r=await s.blob(),n=URL.createObjectURL(r),i=document.createElement("a");i.href=n,i.download=`${e}.dmcomponent.json`,document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(n)},async importBundle(e,{overwrite:o=!1}={}){const s=await fetch(`${d}/components/import`,{method:"POST",headers:{"Content-Type":"application/json",...a()?{Authorization:`Bearer ${a()}`}:{}},body:JSON.stringify({...e,overwrite:o})});if(s.status===409){const r=await s.json().catch(()=>({})),n=new Error(r.error||"Component already exists");throw n.code="CONFLICT",n.name=r.name,n}if(!s.ok){const r=await s.json().catch(()=>({}));throw new Error(r.error||`Import failed (${s.status})`)}return s.json()}},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})},system:{notifications:{list:()=>t("/system/notifications",{method:"GET"}),unreadCount:()=>t("/system/notifications/unread-count",{method:"GET"}),markRead:e=>t(`/system/notifications/${e}/read`,{method:"POST"}),dismiss:e=>t(`/system/notifications/${e}/dismiss`,{method:"POST"}),remove:e=>t(`/system/notifications/${e}`,{method:"DELETE"})}},dashboard:{summary:()=>t("/dashboard/summary",{method:"GET"}),summaryLite:()=>t("/dashboard/summary?lite=1",{method:"GET"})}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:s}){e&&l(e),o&&S.set("auth_refresh_token",o),s&&S.set("auth_user",s)}export function logout(){const e=m();e&&fetch(`${d}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),h(),R.navigate("/login")}export{t as apiRequest,u as refreshAccessToken,a as getToken,h as clearAuth};
package/admin/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import{getSidebarConfig as G}from"./config/sidebar-config.js";import{views as Q}from"./views/index.js?v=13";import{api as s,getUser as u,isAuthenticated as c,logout as X}from"./api.js";import{installHttpInterceptor as Z}from"./http-interceptor.js";$(()=>{Z(),(async()=>{try{const t=c()?await s.settings.get():null;Domma.theme.init({theme:t?.adminTheme||"charcoal-dark",persist:!0})}catch{Domma.theme.init({theme:"charcoal-dark",persist:!0})}})();const W=["jb-company","jb-agent","jb-candidate"],U=["/job-board","/my-profile"];function f(t){return t&&W.includes(t.role)}function w(t){return U.some(e=>t===e||t.startsWith(e+"/"))}R.use(async(t,e,i)=>{if(t.path==="/login"||t.path==="/reset-password")return i();if(!c()){R.navigate("/login");return}if(f(u())&&!w(t.path)){R.navigate("/job-board");return}i()});let g=null;async function V(){if(!c())return{};try{const[t,e,i,a,o,n,d,p,b,D,r,h,J,q]=await Promise.all([s.pages.list().catch(()=>[]),s.media.list().catch(()=>[]),s.users.list().catch(()=>[]),s.plugins.list().catch(()=>[]),s.collections.list().catch(()=>[]),s.forms.list().catch(()=>[]),s.views.list().catch(()=>[]),s.actions.list().catch(()=>[]),s.blocks.list().catch(()=>[]),s.components.list().catch(()=>[]),s.navigation.get().catch(()=>({})),s.layouts.get().catch(()=>({})),s.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]})),s.system.notifications.unreadCount().catch(()=>({count:0}))]),T=r.items||[],Y=T.filter(y=>!y.hidden).length,L=T.length,z=a.filter(y=>y.enabled).length,j=a.length;return{pages:t.length,media:e.length,users:i.length,plugins:j>0?`${z}/${j}`:null,collections:o.length,forms:n.length,views:d.length,actions:p.length,blocks:b.length,components:D.length,navigation:L>0?`${Y}/${L}`:null,layouts:Object.keys(h).length,roles:(J.entries||[]).length,unreadNotifications:q.count||0}}catch{return{}}}async function k(){try{return(await s.get("/auth/permissions")).permissions||[]}catch{return[]}}async function K(){try{return await s.get("/settings/db-status")}catch{return{configured:!1}}}async function x(t){g&&$("#admin-sidebar").empty();const[e,i,a]=await Promise.all([V(),K(),c()?s.settings.get().catch(()=>({})):Promise.resolve({})]),o=a.adminBrand||{},n=o.title||"CMS Admin",d=o.icon||"layout",p=o.logo||"",b=$("#admin-topbar .topbar-brand");p?b.html(`<img src="${l(p)}" class="topbar-brand-logo" alt="${l(n)}"><span class="topbar-brand-text">${l(n)}</span>`):b.html(`<span data-icon="${l(d)}"></span><span class="topbar-brand-text">${l(n)}</span>`),Domma.icons.scan("#admin-topbar .topbar-brand");const D=N.map(r=>{if(!r.countKey)return r;const h=e[r.countKey];return{...r,badge:h!=null&&h>0?String(h):null}});if(g=Domma.elements.sidebar("#admin-sidebar",{header:{title:n,icon:d},items:G(t,e,D),collapsible:!0,collapseAt:992,push:!0,contentSelector:".dashboard-main",top:"60px"}),O(),F(),i?.configured){const r=$("#admin-sidebar").find(".sidebar-header-title").first();r.length&&!r.find(".db-badge").length&&r.append('<span class="db-badge" title="MongoDB connected" style="display:inline-flex;align-items:center;background:#16a34a;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:9999px;letter-spacing:.5px;margin-left:6px;vertical-align:middle;line-height:1;">DB</span>')}}function O(){$("#admin-sidebar .sidebar-link").each(function(){const t=$(this).find(".sidebar-text").text().trim();t&&E.tooltip(this,{content:t,position:"right"})})}function F(){const t="sidebar_collapsed_sections",e=new Set(S.get(t)||[]);$("#admin-sidebar .sidebar-heading").each(function(){const i=$(this);i.addClass("sidebar-heading--collapsible"),i.append('<span class="sidebar-heading-toggle"><span data-icon="chevron-down"></span></span>');const a=i.text().trim();if(e.has(a)){i.addClass("is-collapsed");let o=i.next();for(;o.length&&!o.hasClass("sidebar-heading")&&!o.hasClass("sidebar-divider");)o.hide(),o=o.next()}i.on("click",function(){const o=!i.hasClass("is-collapsed");i.toggleClass("is-collapsed",o);let n=i.next();for(;n.length&&!n.hasClass("sidebar-heading")&&!n.hasClass("sidebar-divider");)n.toggle(!o),n=n.next();o?e.add(a):e.delete(a),S.set(t,[...e])})}),Domma.icons.scan("#admin-sidebar")}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&m&&(clearInterval(m),m=null),g&&t.path!=="/login"&&t.path!=="/reset-password"&&g.setActive("#"+t.path)});const H=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/navigation",view:"navigation",title:"Navigation - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if(f(u())&&!w(t.path)){R.navigate("/job-board");return}if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),await _();const i=await k();x(i);try{const a=await s.system.notifications.list().catch(()=>[]),o=(Array.isArray(a)?a:[]).filter(n=>n.unread&&["warning","critical"].includes(n.data?.severity)).slice(0,3);for(const n of o){const d=l(n.data?.title||""),p=l((n.data?.body||"").slice(0,120));E.toast(`${d} \u2014 ${p}`,{type:n.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}B()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const P="cms_card_states",v=S.get(P)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&v[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const e=$(this).closest(".card");e.toggleClass("card-collapsed");const i=$(this).find("h2, h3").first().text().trim();i&&(v[i]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(P,v))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const A={...Q},C=[...H];let N=[];async function _(){if(c())try{const t=await s.plugins.adminConfig();N=t.sidebar||[],t.routes?.length&&C.push(...t.routes);for(const[e,i]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${i.entry}`);A[e]=a[i.exportName]}catch{}for(const{id:e,href:i}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=i,document.head.appendChild(a)}}catch{}}function B(){const t=u();if(!t||$("#topbar-user-name").length)return;const i={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
1
+ import{getSidebarConfig as G}from"./config/sidebar-config.js";import{views as Q}from"./views/index.js?v=16";import{api as s,getUser as u,isAuthenticated as c,logout as X}from"./api.js";import{installHttpInterceptor as Z}from"./http-interceptor.js";$(()=>{Z(),(async()=>{try{const t=c()?await s.settings.get():null;Domma.theme.init({theme:t?.adminTheme||"charcoal-dark",persist:!0})}catch{Domma.theme.init({theme:"charcoal-dark",persist:!0})}})();const W=["jb-company","jb-agent","jb-candidate"],U=["/job-board","/my-profile"];function f(t){return t&&W.includes(t.role)}function w(t){return U.some(e=>t===e||t.startsWith(e+"/"))}R.use(async(t,e,i)=>{if(t.path==="/login"||t.path==="/reset-password")return i();if(!c()){R.navigate("/login");return}if(f(u())&&!w(t.path)){R.navigate("/job-board");return}i()});let g=null;async function V(){if(!c())return{};try{const[t,e,i,a,o,n,d,p,b,D,r,h,J,q]=await Promise.all([s.pages.list().catch(()=>[]),s.media.list().catch(()=>[]),s.users.list().catch(()=>[]),s.plugins.list().catch(()=>[]),s.collections.list().catch(()=>[]),s.forms.list().catch(()=>[]),s.views.list().catch(()=>[]),s.actions.list().catch(()=>[]),s.blocks.list().catch(()=>[]),s.components.list().catch(()=>[]),s.navigation.get().catch(()=>({})),s.layouts.get().catch(()=>({})),s.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]})),s.system.notifications.unreadCount().catch(()=>({count:0}))]),T=r.items||[],Y=T.filter(y=>!y.hidden).length,L=T.length,z=a.filter(y=>y.enabled).length,j=a.length;return{pages:t.length,media:e.length,users:i.length,plugins:j>0?`${z}/${j}`:null,collections:o.length,forms:n.length,views:d.length,actions:p.length,blocks:b.length,components:D.length,navigation:L>0?`${Y}/${L}`:null,layouts:Object.keys(h).length,roles:(J.entries||[]).length,unreadNotifications:q.count||0}}catch{return{}}}async function k(){try{return(await s.get("/auth/permissions")).permissions||[]}catch{return[]}}async function K(){try{return await s.get("/settings/db-status")}catch{return{configured:!1}}}async function x(t){g&&$("#admin-sidebar").empty();const[e,i,a]=await Promise.all([V(),K(),c()?s.settings.get().catch(()=>({})):Promise.resolve({})]),o=a.adminBrand||{},n=o.title||"CMS Admin",d=o.icon||"layout",p=o.logo||"",b=$("#admin-topbar .topbar-brand");p?b.html(`<img src="${l(p)}" class="topbar-brand-logo" alt="${l(n)}"><span class="topbar-brand-text">${l(n)}</span>`):b.html(`<span data-icon="${l(d)}"></span><span class="topbar-brand-text">${l(n)}</span>`),Domma.icons.scan("#admin-topbar .topbar-brand");const D=N.map(r=>{if(!r.countKey)return r;const h=e[r.countKey];return{...r,badge:h!=null&&h>0?String(h):null}});if(g=Domma.elements.sidebar("#admin-sidebar",{header:{title:n,icon:d},items:G(t,e,D),collapsible:!0,collapseAt:992,push:!0,contentSelector:".dashboard-main",top:"60px"}),O(),F(),i?.configured){const r=$("#admin-sidebar").find(".sidebar-header-title").first();r.length&&!r.find(".db-badge").length&&r.append('<span class="db-badge" title="MongoDB connected" style="display:inline-flex;align-items:center;background:#16a34a;color:#fff;font-size:10px;font-weight:700;padding:2px 6px;border-radius:9999px;letter-spacing:.5px;margin-left:6px;vertical-align:middle;line-height:1;">DB</span>')}}function O(){$("#admin-sidebar .sidebar-link").each(function(){const t=$(this).find(".sidebar-text").text().trim();t&&E.tooltip(this,{content:t,position:"right"})})}function F(){const t="sidebar_collapsed_sections",e=new Set(S.get(t)||[]);$("#admin-sidebar .sidebar-heading").each(function(){const i=$(this);i.addClass("sidebar-heading--collapsible"),i.append('<span class="sidebar-heading-toggle"><span data-icon="chevron-down"></span></span>');const a=i.text().trim();if(e.has(a)){i.addClass("is-collapsed");let o=i.next();for(;o.length&&!o.hasClass("sidebar-heading")&&!o.hasClass("sidebar-divider");)o.hide(),o=o.next()}i.on("click",function(){const o=!i.hasClass("is-collapsed");i.toggleClass("is-collapsed",o);let n=i.next();for(;n.length&&!n.hasClass("sidebar-heading")&&!n.hasClass("sidebar-divider");)n.toggle(!o),n=n.next();o?e.add(a):e.delete(a),S.set(t,[...e])})}),Domma.icons.scan("#admin-sidebar")}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&m&&(clearInterval(m),m=null),g&&t.path!=="/login"&&t.path!=="/reset-password"&&g.setActive("#"+t.path)});const H=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/navigation",view:"navigation",title:"Navigation - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if(f(u())&&!w(t.path)){R.navigate("/job-board");return}if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),await _();const i=await k();x(i);try{const a=await s.system.notifications.list().catch(()=>[]),o=(Array.isArray(a)?a:[]).filter(n=>n.unread&&["warning","critical"].includes(n.data?.severity)).slice(0,3);for(const n of o){const d=l(n.data?.title||""),p=l((n.data?.body||"").slice(0,120));E.toast(`${d} \u2014 ${p}`,{type:n.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}B()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const P="cms_card_states",v=S.get(P)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&v[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const e=$(this).closest(".card");e.toggleClass("card-collapsed");const i=$(this).find("h2, h3").first().text().trim();i&&(v[i]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(P,v))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const A={...Q},C=[...H];let N=[];async function _(){if(c())try{const t=await s.plugins.adminConfig();N=t.sidebar||[],t.routes?.length&&C.push(...t.routes);for(const[e,i]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${i.entry}`);A[e]=a[i.exportName]}catch{}for(const{id:e,href:i}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=i,document.head.appendChild(a)}}catch{}}function B(){const t=u();if(!t||$("#topbar-user-name").length)return;const i={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
2
2
  <span id="topbar-user-name" class="topbar-user-name">${l(t.name)}</span>
3
3
  <span class="topbar-role-badge topbar-role-badge--${l(t.role)}">${i}</span>
4
4
  `),$("#topbar-actions").html(`
@@ -0,0 +1,3 @@
1
+ <h3>Recent activity</h3>
2
+ <div data-field="rows"></div>
3
+ <p class="dash-empty" hidden>No recent activity.</p>
@@ -0,0 +1,2 @@
1
+ <h3>Health</h3>
2
+ <div data-field="rows"></div>
@@ -0,0 +1,17 @@
1
+ <h3>User journeys — last 7 days</h3>
2
+ <div class="dash-journey-grid">
3
+ <div>
4
+ <h4>Top entry pages</h4>
5
+ <div data-field="entry"></div>
6
+ </div>
7
+ <div>
8
+ <h4>Top exit pages</h4>
9
+ <div data-field="exit"></div>
10
+ </div>
11
+ <div>
12
+ <h4>Top paths</h4>
13
+ <div data-field="paths"></div>
14
+ </div>
15
+ </div>
16
+ <p class="dash-bounce">Bounce rate: <strong data-field="bounce">—</strong></p>
17
+ <p class="dash-empty" hidden>No journey data yet — encourage some traffic and check back.</p>
@@ -0,0 +1,34 @@
1
+ <div class="dash-card dash-kpi">
2
+ <span class="dash-kpi-icon" data-icon="trending-up"></span>
3
+ <div>
4
+ <div class="dash-kpi-value" data-field="hits-today">—</div>
5
+ <div class="dash-kpi-label">Hits today</div>
6
+ <div class="dash-kpi-delta" data-field="hits-delta"></div>
7
+ </div>
8
+ </div>
9
+ <div class="dash-card dash-kpi">
10
+ <span class="dash-kpi-icon" data-icon="users"></span>
11
+ <div>
12
+ <div class="dash-kpi-value" data-field="active-sessions">—</div>
13
+ <div class="dash-kpi-label">Active sessions (5m)</div>
14
+ </div>
15
+ </div>
16
+ <div class="dash-card dash-kpi">
17
+ <span class="dash-kpi-icon" data-icon="inbox"></span>
18
+ <div>
19
+ <div class="dash-kpi-value" data-field="submissions-week">—</div>
20
+ <div class="dash-kpi-label">Submissions this week</div>
21
+ </div>
22
+ </div>
23
+ <div class="dash-card dash-kpi">
24
+ <span class="dash-kpi-icon" data-icon="activity"></span>
25
+ <div>
26
+ <div class="dash-kpi-value">
27
+ <span class="dash-health-pill" data-field="health-pill" data-level="ok">
28
+ <span data-icon="circle"></span>
29
+ <span data-field="health-label">OK</span>
30
+ </span>
31
+ </div>
32
+ <div class="dash-kpi-label">Site health</div>
33
+ </div>
34
+ </div>
@@ -0,0 +1,3 @@
1
+ <h3>Spikes (last hour)</h3>
2
+ <div data-field="spike-list"></div>
3
+ <p class="dash-empty" hidden>No anomalies detected.</p>
@@ -0,0 +1,3 @@
1
+ <h3>Top pages — last 7 days</h3>
2
+ <div data-field="rows"></div>
3
+ <p class="dash-empty" hidden>No traffic recorded yet.</p>
@@ -0,0 +1,3 @@
1
+ <h3>Traffic — last 30 days</h3>
2
+ <canvas id="dash-traffic-canvas" height="120"></canvas>
3
+ <p class="dash-empty" hidden>No traffic recorded yet.</p>
@@ -1,50 +1,28 @@
1
1
  <div class="view-header">
2
2
  <h1><span data-icon="home"></span> Dashboard</h1>
3
- <a href="#/pages/new" class="btn btn-primary"><span data-icon="plus"></span> New Page</a>
4
- </div>
5
-
6
- <div class="row mb-4">
7
- <div class="col-4">
8
- <div class="card stat-card">
9
- <div class="card-body">
10
- <div class="stat-icon"><span data-icon="file-text"></span></div>
11
- <div class="stat-body">
12
- <div class="stat-value" id="stat-total">—</div>
13
- <div class="stat-label">Total Pages</div>
14
- </div>
15
- </div>
16
- </div>
17
- </div>
18
- <div class="col-4">
19
- <div class="card stat-card">
20
- <div class="card-body">
21
- <div class="stat-icon stat-icon--success"><span data-icon="check-circle"></span></div>
22
- <div class="stat-body">
23
- <div class="stat-value" id="stat-published">—</div>
24
- <div class="stat-label">Published</div>
25
- </div>
26
- </div>
27
- </div>
28
- </div>
29
- <div class="col-4">
30
- <div class="card stat-card">
31
- <div class="card-body">
32
- <div class="stat-icon stat-icon--warning"><span data-icon="clock"></span></div>
33
- <div class="stat-body">
34
- <div class="stat-value" id="stat-drafts">—</div>
35
- <div class="stat-label">Drafts</div>
36
- </div>
37
- </div>
38
- </div>
3
+ <div class="view-header-actions">
4
+ <span class="dash-updated" id="dash-updated"></span>
5
+ <a href="#/pages/new" class="btn btn-primary"><span data-icon="plus"></span> New Page</a>
39
6
  </div>
40
7
  </div>
41
8
 
42
- <div class="card card-collapsible mt-4">
43
- <div class="card-header" role="button" tabindex="0">
44
- <div class="card-header-content"><h2>Recent Pages</h2></div>
45
- <span class="card-collapse-icon" data-icon="chevron-down"></span>
46
- </div>
47
- <div class="card-body">
48
- <div id="recent-pages-table"></div>
49
- </div>
9
+ <div id="dash-warnings" class="dash-warnings" hidden></div>
10
+
11
+ <div class="dashboard-grid">
12
+ <section id="dash-kpi-strip" class="dash-row dash-row-kpi"></section>
13
+
14
+ <section class="dash-row dash-row-traffic">
15
+ <div id="dash-traffic-chart" class="dash-card dash-card-wide"></div>
16
+ <div id="dash-spike-feed" class="dash-card"></div>
17
+ </section>
18
+
19
+ <section class="dash-row dash-row-content">
20
+ <div id="dash-top-pages" class="dash-card"></div>
21
+ <div id="dash-journeys" class="dash-card"></div>
22
+ </section>
23
+
24
+ <section class="dash-row dash-row-health">
25
+ <div id="dash-health-detail" class="dash-card"></div>
26
+ <div id="dash-activity-feed" class="dash-card"></div>
27
+ </section>
50
28
  </div>
@@ -0,0 +1 @@
1
+ export function escapeHtml(t){return t==null?"":String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}export function safeHref(t){if(t==null)return"#";const r=String(t).trim();if(r==="")return"#";if(r.startsWith("/"))return escapeHtml(r);const e=r.toLowerCase();return e.startsWith("http://")||e.startsWith("https://")||e.startsWith("mailto:")?escapeHtml(r):"#"}
@@ -0,0 +1 @@
1
+ import{escapeHtml as s}from"../lib/escape.js?v=2";const p={edit:"edited",publish:"published",form:"received submission"};function f(e,n){e&&(e.innerHTML=n)}export default function m(e,n){const r=Array.isArray(n?.activity)?n.activity:[];r.length===0?e.find(".dash-empty").removeAttr("hidden"):e.find(".dash-empty").attr("hidden","hidden");const i=r.map(t=>{const o=t.at?D(t.at).fromNow():"",a=t.target||t.form||"",d=t.actor?"<strong>"+s(t.actor)+"</strong> ":"",c=s(p[t.type]||t.type||"");return'<div class="dash-spike-row"><span>'+d+c+" <code>"+s(a)+"</code></span><span>"+s(o)+"</span></div>"});f(e.find('[data-field="rows"]').get(0),i.join(""))}
@@ -0,0 +1 @@
1
+ const p={ok:"check-circle",warn:"alert-triangle",fail:"x-circle"};export default function h(o,s){const r=s?.health?.checks||[],c=o.find('[data-field="rows"]');c.empty();const a=c.get(0);if(a){for(const e of r){const n=document.createElement("div");n.className="dash-spike-row dash-health-pill",n.setAttribute("data-level",e.level);const l=document.createElement("span"),i=document.createElement("span");i.setAttribute("data-icon",p[e.level]||"circle"),l.appendChild(i),l.appendChild(document.createTextNode(" "+(e.label||e.id)));const d=document.createElement("span");if(d.textContent=String(e.value),n.appendChild(l),n.appendChild(d),a.appendChild(n),Array.isArray(e.detail)&&e.detail.length>0){const t=document.createElement("div");t.className="dash-health-detail",t.style.fontSize="12px",t.style.opacity="0.75",t.style.padding="4px 0 8px",t.textContent=e.detail.slice(0,5).join(", "),a.appendChild(t)}}window.Domma&&Domma.icons&&Domma.icons.scan()}}
@@ -0,0 +1 @@
1
+ import{escapeHtml as r}from"../lib/escape.js?v=2";function s(n,a){return n.map(e=>'<div class="dash-spike-row">'+a(e)+"</div>").join("")}function i(n,a){n&&(n.innerHTML=a)}export default function f(n,a){const e=a?.journeys,d=n.find(".dash-empty"),o=n.find('[data-field="entry"]'),p=n.find('[data-field="exit"]'),u=n.find('[data-field="paths"]'),c=n.find('[data-field="bounce"]');if(!e||!e.totalSessions){d.removeAttr("hidden"),o.empty(),p.empty(),u.empty(),c.text("\u2014");return}d.attr("hidden","hidden"),i(o.get(0),s(e.entry||[],t=>"<span>"+r(t.url)+"</span><span>"+(Number(t.count)|0)+"</span>")),i(p.get(0),s(e.exit||[],t=>"<span>"+r(t.url)+"</span><span>"+(Number(t.count)|0)+"</span>")),i(u.get(0),s(e.paths||[],t=>"<span>"+r(t.from)+" &rarr; "+r(t.to)+"</span><span>"+(Number(t.count)|0)+"</span>")),c.text((Number(e.bounceRate)*100).toFixed(1)+"%")}
@@ -0,0 +1 @@
1
+ export default function n(e,t){const d=t?.traffic?.today??0,a=t?.traffic?.deltaPct;e.find('[data-field="hits-today"]').text(d);const s=e.find('[data-field="hits-delta"]');a==null?s.text("").removeClass("up down"):a>=0?s.text("+"+a+"% vs yesterday").removeClass("down").addClass("up"):s.text(a+"% vs yesterday").removeClass("up").addClass("down"),e.find('[data-field="active-sessions"]').text(t?.realtime?.activeSessions??0),e.find('[data-field="submissions-week"]').text(t?._submissionsWeek??"\u2014");const i=t?.health?.status||"ok",l={ok:"OK",warn:"Attention",fail:"Failing"};e.find('[data-field="health-pill"]').attr("data-level",i),e.find('[data-field="health-label"]').text(l[i])}
@@ -0,0 +1,6 @@
1
+ import{escapeHtml as a,safeHref as d}from"../lib/escape.js?v=2";export default function p(i,r){const t=Array.isArray(r?.spikes)?r.spikes:[];t.length===0?i.find(".dash-empty").removeAttr("hidden"):i.find(".dash-empty").attr("hidden","hidden");const s=t.map(e=>`
2
+ <div class="dash-spike-row">
3
+ <span><a href="${d(e.url)}">${a(e.url)}</a></span>
4
+ <span><strong>${Number(e.hits)|0}</strong> \xB7 \xD7${Number(e.ratio)||0}</span>
5
+ </div>
6
+ `).join(""),n=i.find('[data-field="spike-list"]').get(0);n&&(n.innerHTML=s)}
@@ -0,0 +1 @@
1
+ import{escapeHtml as l,safeHref as i}from"../lib/escape.js?v=2";function c(n,s,e){if(s=s||80,e=e||24,!n||n.length===0)return"";const a=n.map(p=>Number(p)||0),o=Math.max.apply(null,a.concat([1])),t=s/Math.max(a.length-1,1),r=a.map((p,d)=>(d*t).toFixed(1)+","+(e-p/o*e).toFixed(1)).join(" ");return'<svg class="dash-spark" viewBox="0 0 '+s+" "+e+'" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none"><polyline fill="none" stroke="currentColor" stroke-width="1.5" points="'+r+'"/></svg>'}export default function m(n,s){const e=Array.isArray(s?.topPages)?s.topPages:[];e.length===0?n.find(".dash-empty").removeAttr("hidden"):n.find(".dash-empty").attr("hidden","hidden");const a=e.map(t=>{const r=t.deltaPct==null?"":'<span class="'+(t.deltaPct>=0?"up":"down")+' dash-kpi-delta">'+(t.deltaPct>=0?"+":"")+(Number(t.deltaPct)||0)+"%</span>";return'<div class="dash-spike-row"><span><a href="'+i(t.url)+'">'+l(t.url)+"</a> "+c(t.spark)+"</span><span><strong>"+(Number(t.hits)|0)+"</strong> "+r+"</span></div>"}).join(""),o=n.find('[data-field="rows"]').get(0);o&&(o.innerHTML=a)}
@@ -0,0 +1 @@
1
+ const p="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.js";let s=null;function u(){return window.Chart?Promise.resolve(window.Chart):s||(s=new Promise((r,n)=>{const e=document.createElement("script");e.src=p,e.onload=()=>r(window.Chart),e.onerror=n,document.head.appendChild(e)}),s)}let i=null;export default async function m(r,n){const e=await u(),o=30,a=[];for(let t=o-1;t>=0;t-=1)a.push({label:D().subtract(t,"days").format("MM-DD"),hits:0});if(Array.isArray(n?.topPages))for(const t of n.topPages){if(!Array.isArray(t.spark))continue;const l=o-t.spark.length;t.spark.forEach((f,h)=>{a[l+h].hits+=f})}const d=a.every(t=>t.hits===0);d?r.find(".dash-empty").removeAttr("hidden"):r.find(".dash-empty").attr("hidden","hidden");const c=r.find("#dash-traffic-canvas").get(0);c&&(i&&(i.destroy(),i=null),!d&&(i=new e(c,{type:"line",data:{labels:a.map(t=>t.label),datasets:[{data:a.map(t=>t.hits),borderColor:"rgba(37, 99, 235, 1)",backgroundColor:"rgba(37, 99, 235, 0.15)",fill:!0,tension:.3,pointRadius:0}]},options:{responsive:!0,maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{ticks:{maxTicksLimit:8}},y:{beginAtZero:!0}}}})))}
@@ -1 +1 @@
1
- import{api as n}from"../api.js";export const dashboardView={templateUrl:"/admin/js/templates/dashboard.html",async onMount(e){const s=E.loader(e.get(0),{type:"dots"}),a=await n.pages.list().catch(()=>[]);s.destroy();const d=a.length,r=a.filter(t=>t.status==="published").length,i=a.filter(t=>t.status==="draft").length;e.find("#stat-total").text(d),e.find("#stat-published").text(r),e.find("#stat-drafts").text(i),Domma.effects.counter("#stat-total, #stat-published, #stat-drafts",{trigger:"immediate",duration:1e3,stagger:120,easing:"ease-out"});const o=[...a].sort((t,l)=>(l.updatedAt||"").localeCompare(t.updatedAt||"")).slice(0,10);T.create("#recent-pages-table",{data:o,columns:[{key:"title",title:"Title"},{key:"urlPath",title:"URL"},{key:"status",title:"Status",render:t=>`<span class="badge badge-${t==="published"?"success":"warning"}">${t}</span>`},{key:"updatedAt",title:"Updated",render:t=>t?D(t).format("DD MMM YYYY"):"\u2014"},{key:"urlPath",title:"Actions",render:t=>`<a href="#/pages/edit${t}" class="btn btn-sm btn-primary">Edit</a>`}],emptyMessage:'No pages yet. <a href="#/pages/new">Create your first page</a>.'}),Domma.icons.scan(),Domma.effects.reveal(".stat-card",{animation:"fade",stagger:80,duration:400}),Domma.effects.reveal(".card.mt-4",{animation:"fade",delay:200,duration:400})}};
1
+ import{api as n}from"../api.js";import l from"./dashboard/widgets/kpi-strip.js?v=1";import c from"./dashboard/widgets/traffic-chart.js?v=2";import m from"./dashboard/widgets/spike-feed.js?v=2";import u from"./dashboard/widgets/top-pages.js?v=2";import y from"./dashboard/widgets/journeys.js?v=2";import b from"./dashboard/widgets/health-detail.js?v=1";import w from"./dashboard/widgets/activity-feed.js?v=1";const i=[{slot:"#dash-kpi-strip",template:"/admin/js/templates/dashboard/kpi-strip.html",render:l},{slot:"#dash-traffic-chart",template:"/admin/js/templates/dashboard/traffic-chart.html",render:c},{slot:"#dash-spike-feed",template:"/admin/js/templates/dashboard/spike-feed.html",render:m},{slot:"#dash-top-pages",template:"/admin/js/templates/dashboard/top-pages.html",render:u},{slot:"#dash-journeys",template:"/admin/js/templates/dashboard/journeys.html",render:y},{slot:"#dash-health-detail",template:"/admin/js/templates/dashboard/health-detail.html",render:b},{slot:"#dash-activity-feed",template:"/admin/js/templates/dashboard/activity-feed.html",render:w}],g=30*1e3;async function k(t){const a=await fetch(t,{credentials:"same-origin"});if(!a.ok)throw new Error(`Failed to load ${t}: ${a.status}`);return a.text()}function v(t,a){t.length&&(t.get(0).innerHTML=a)}function j(t,a){const s=t.find("#dash-warnings");if(!a||a.length===0){s.attr("hidden","hidden");return}s.removeAttr("hidden"),s.get(0).textContent="Some sections could not load: "+a.join(" \xB7 ")}function h(t){t.find("#dash-updated").text("Updated "+D().format("HH:mm:ss"))}function f(t){if(!t||!Array.isArray(t.activity))return t;const a=D().subtract(7,"days").valueOf();return t._submissionsWeek=t.activity.filter(s=>s.type==="form"&&D(s.at).valueOf()>=a).length,t}let o=null;export const dashboardView={templateUrl:"/admin/js/templates/dashboard.html",async onMount(t){const a=E.loader(t.get(0),{type:"dots"}),s=await Promise.all(i.map(e=>k(e.template)));i.forEach((e,d)=>{v(t.find(e.slot),s[d])});let r;try{r=await n.dashboard.summary(),r=f(r)}catch(e){a.destroy();const d=t.find("#dash-warnings");d.removeAttr("hidden"),d.get(0).textContent="Could not load dashboard: "+e.message;return}for(const e of i){const d=t.find(e.slot);try{await e.render(d,r)}catch(p){console.warn("[dashboard]",e.slot,"render failed:",p)}}j(t,r.warnings),h(t),Domma.icons.scan(),a.destroy(),o&&clearInterval(o),o=setInterval(async()=>{if(!document.body.contains(t.get(0))){clearInterval(o),o=null;return}try{const e=await n.dashboard.summaryLite();e.traffic&&(r.traffic={...r.traffic,...e.traffic}),e.realtime&&(r.realtime=e.realtime),e.spikes&&(r.spikes=e.spikes),e.health&&(r.health={...r.health,status:e.health.status}),f(r),await l(t.find("#dash-kpi-strip"),r),await m(t.find("#dash-spike-feed"),r),h(t)}catch(e){console.warn("[dashboard] lite refresh failed:",e)}},g)}};