domma-cms 0.17.0 → 0.18.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.
- package/CLAUDE.md +2 -0
- package/admin/css/dashboard.css +1 -1
- package/admin/index.html +2 -2
- package/admin/js/app.js +1 -1
- package/admin/js/lib/card-builder.js +3 -3
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +5 -5
- package/admin/js/templates/dashboard/cache.html +32 -0
- package/admin/js/templates/dashboard.html +4 -0
- package/admin/js/templates/settings.html +26 -0
- package/admin/js/views/block-editor-enhance.js +1 -1
- package/admin/js/views/dashboard/widgets/activity-feed.js +1 -1
- package/admin/js/views/dashboard/widgets/cache.js +1 -0
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/spike-feed.js +1 -1
- package/admin/js/views/dashboard/widgets/top-pages.js +1 -1
- package/admin/js/views/dashboard.js +1 -1
- package/admin/js/views/form-editor.js +6 -6
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/page-editor.js +42 -37
- package/admin/js/views/settings.js +3 -3
- package/config/cache.json +4 -0
- package/config/cache.json.example +12 -0
- package/package.json +1 -1
- package/public/js/forms.js +1 -1
- package/public/js/site.js +1 -1
- package/server/config.js +12 -1
- package/server/routes/api/cache.js +57 -0
- package/server/routes/api/navigation.js +2 -0
- package/server/routes/api/settings.js +3 -0
- package/server/routes/public.js +11 -3
- package/server/server.js +16 -3
- package/server/services/blocks.js +3 -0
- package/server/services/cache/drivers/MemoryDriver.js +118 -0
- package/server/services/cache/drivers/NoneDriver.js +12 -0
- package/server/services/cache/index.js +229 -0
- package/server/services/cache/lru.js +61 -0
- package/server/services/collections.js +17 -4
- package/server/services/content.js +7 -2
- package/server/services/forms.js +3 -0
- package/server/services/markdown.js +25 -15
- package/server/services/views.js +4 -0
- package/server/templates/page.html +130 -130
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
`,
|
|
3
|
-
`,
|
|
1
|
+
import{api as C,apiRequest as v}from"../api.js";import{populateThemeSelect as w}from"../lib/themes.js";export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(e){E.tabs(e.find("#settings-tabs").get(0));const k=E.loader(e.get(0),{type:"dots"}),l=await C.settings.get().catch(()=>({}));if(k.destroy(),e.find("#field-site-title").val(l.title||""),e.find("#field-tagline").val(l.tagline||""),e.find("#field-font-family").val(l.fontFamily||"Roboto"),e.find("#field-font-size").val(l.fontSize||16),w(e.find("#field-theme").get(0)),w(e.find("#field-admin-theme").get(0)),e.find("#field-theme").val(l.baseTheme||l.theme||"charcoal-dark"),l.baseTheme){e.find("#field-theme").prop("disabled",!0);const t=document.createElement("p");t.className="form-hint",t.style.cssText="margin-top:.4rem;font-size:.8rem;color:var(--dm-info);";const s=document.createElement("a");s.href="#/plugins/theme-roller",s.textContent="Theme Roller",t.appendChild(document.createTextNode(`Custom theme \u201C${l.theme}\u201D is active (based on ${l.baseTheme}). Manage via `)),t.appendChild(s),t.appendChild(document.createTextNode(".")),e.find("#field-theme").get(0).closest(".col-6").appendChild(t)}e.find("#field-admin-theme").val(l.adminTheme||"charcoal-dark");const p=l.autoTheme||{},h=!!p.enabled,b=e.find("#field-auto-theme-enabled"),y=e.find("#auto-theme-fields"),u=e.find("#field-theme"),f=u.get(0).innerHTML;e.find("#field-day-theme").html(f),e.find("#field-night-theme").html(f),l.baseTheme?(b.prop("disabled",!0),e.find("#auto-theme-roller-hint").show()):(b.prop("checked",h),h&&(y.show(),u.prop("disabled",!0)),b.on("change",function(){const t=this.checked;y.toggle(t),u.prop("disabled",t)})),e.find("#field-day-theme").val(p.dayTheme||"charcoal-light"),e.find("#field-night-theme").val(p.nightTheme||"charcoal-dark"),e.find("#field-day-start").val(p.dayStart||"07:00"),e.find("#field-night-start").val(p.nightStart||"19:00"),e.find("#field-spacer-size").val(l.layoutOptions?.spacerSize??40),e.find("#field-seo-title").val(l.seo?.defaultTitle||""),e.find("#field-seo-separator").val(l.seo?.titleSeparator||" | "),e.find("#field-seo-desc").val(l.seo?.defaultDescription||""),e.find("#field-footer-copy").val(l.footer?.copyright||""),e.find("#field-social-twitter").val(l.social?.twitter||""),e.find("#field-social-facebook").val(l.social?.facebook||""),e.find("#field-social-instagram").val(l.social?.instagram||""),e.find("#field-social-linkedin").val(l.social?.linkedin||""),e.find("#field-social-github").val(l.social?.github||""),e.find("#field-social-youtube").val(l.social?.youtube||""),e.find("#field-smtp-host").val(l.smtp?.host||""),e.find("#field-smtp-port").val(l.smtp?.port||587),e.find("#field-smtp-user").val(l.smtp?.user||""),e.find("#field-smtp-pass").val(l.smtp?.pass||""),e.find("#field-smtp-secure").prop("checked",l.smtp?.secure||!1),e.find("#field-smtp-from-address").val(l.smtp?.fromAddress||""),e.find("#field-smtp-from-name").val(l.smtp?.fromName||"");const i=l.backToTop||{};e.find("#field-btt-enabled").prop("checked",i.enabled!==!1),e.find("#field-btt-threshold").val(i.scrollThreshold??300),e.find("#field-btt-position").val(i.position||"bottom-right"),e.find("#field-btt-offset").val(i.offset??32),e.find("#field-btt-bottom-offset").val(i.bottomOffset??i.offset??32),e.find("#field-btt-label").val(i.label||""),e.find("#field-btt-smooth").prop("checked",i.smooth!==!1);const d=l.cookieConsent||{};e.find("#field-cc-enabled").prop("checked",d.enabled!==!1),e.find("#field-cc-message").val(d.message||""),e.find("#field-cc-accept-all").val(d.acceptAllText||"Accept All"),e.find("#field-cc-reject-all").val(d.rejectAllText||"Reject All"),e.find("#field-cc-customize").val(d.customizeText||"Customize"),e.find("#field-cc-save-prefs").val(d.savePreferencesText||"Save Preferences"),e.find("#field-cc-privacy-text").val(d.privacyPolicyText||"Privacy Policy"),e.find("#field-cc-privacy-url").val(d.privacyPolicyUrl||""),e.find("#field-cc-cookie-text").val(d.cookiePolicyText||"Cookie Policy"),e.find("#field-cc-cookie-url").val(d.cookiePolicyUrl||""),e.find("#field-cc-position").val(d.position||"bottom"),e.find("#field-cc-layout").val(d.layout||"bar"),e.find("#field-cc-theme").val(d.theme||"dark"),e.find("#field-cc-show-functional").prop("checked",d.showFunctional!==!1),e.find("#field-cc-show-analytics").prop("checked",d.showAnalytics!==!1),e.find("#field-cc-show-marketing").prop("checked",d.showMarketing!==!1),e.find("#field-cc-version").val(d.consentVersion||"1.0");const m=l.breadcrumbs||{};e.find("#field-breadcrumbs-enabled").prop("checked",m.enabled===!0),e.find("#field-breadcrumbs-home-label").val(m.homeLabel||"Home"),e.find("#field-breadcrumbs-position").val(m.position||"TL"),e.find("#field-bc-offset-x").val(m.offsetX??8),e.find("#field-bc-offset-y").val(m.offsetY??8);function c(t){e.find(".bc-pos-btn").each(function(){const s=this.dataset.pos===t;this.style.background=s?"var(--primary, #5b8cff)":"",this.style.color=s?"#fff":""})}c(m.position||"TL"),e.find(".bc-pos-btn").on("click",function(){const t=this.dataset.pos;e.find("#field-breadcrumbs-position").val(t),c(t)});let a=null;try{const{css:t}=await v("/settings/custom-css");e.find("#field-custom-css").val(t||""),E.editor&&(a=E.editor(e.find("#field-custom-css").get(0),{mode:"code",language:"css",lineNumbers:!0,showToolbar:!1,minHeight:420,placeholder:"/* Add your custom CSS here */",characterCount:!0}))}catch{}a&&a._editorEl&&a._editorEl.addEventListener("keydown",t=>{if(!t.ctrlKey&&!t.metaKey)return;if(t.key==="s"){t.preventDefault(),e.find("#save-css-btn").get(0)?.click();return}const s=a._editorEl,n=s.selectionStart!==s.selectionEnd;if((t.key==="c"||t.key==="x")&&!n){t.preventDefault();const o=s.value,g=s.selectionStart,T=o.lastIndexOf(`
|
|
2
|
+
`,g-1)+1,x=o.indexOf(`
|
|
3
|
+
`,g),S=x===-1?o.slice(T):o.slice(T,x+1);if(navigator.clipboard.writeText(S),t.key==="x"){const z=o.slice(0,T),P=x===-1?"":o.slice(x+1);s.value=z+P,s.selectionStart=s.selectionEnd=T,s.dispatchEvent(new Event("input",{bubbles:!0})),E.toast("Line cut",{type:"info",duration:1500})}else E.toast("Line copied",{type:"info",duration:1500})}}),e.find("#save-settings-btn").on("click",async()=>{const t=e.find("#field-admin-theme").val(),s=!l.baseTheme&&e.find("#field-auto-theme-enabled").prop("checked"),n={enabled:s,dayTheme:e.find("#field-day-theme").val()||"charcoal-light",nightTheme:e.find("#field-night-theme").val()||"charcoal-dark",dayStart:e.find("#field-day-start").val()||"07:00",nightStart:e.find("#field-night-start").val()||"19:00"},o={title:e.find("#field-site-title").val().trim(),tagline:e.find("#field-tagline").val().trim(),fontFamily:e.find("#field-font-family").val()||"Roboto",fontSize:parseInt(e.find("#field-font-size").val(),10)||16,theme:l.baseTheme?l.theme:s?n.dayTheme:e.find("#field-theme").val(),...l.baseTheme?{baseTheme:l.baseTheme}:{},autoTheme:n,adminTheme:t,layoutOptions:{spacerSize:parseInt(e.find("#field-spacer-size").val(),10)||40},seo:{defaultTitle:e.find("#field-seo-title").val().trim(),titleSeparator:e.find("#field-seo-separator").val()||" | ",defaultDescription:e.find("#field-seo-desc").val().trim()},footer:{copyright:e.find("#field-footer-copy").val().trim(),links:l.footer?.links||[]},social:{twitter:e.find("#field-social-twitter").val().trim(),facebook:e.find("#field-social-facebook").val().trim(),instagram:e.find("#field-social-instagram").val().trim(),linkedin:e.find("#field-social-linkedin").val().trim(),github:e.find("#field-social-github").val().trim(),youtube:e.find("#field-social-youtube").val().trim()},smtp:{host:e.find("#field-smtp-host").val().trim(),port:parseInt(e.find("#field-smtp-port").val(),10)||587,user:e.find("#field-smtp-user").val().trim(),pass:e.find("#field-smtp-pass").val(),secure:e.find("#field-smtp-secure").prop("checked"),fromAddress:e.find("#field-smtp-from-address").val().trim(),fromName:e.find("#field-smtp-from-name").val().trim()},backToTop:{enabled:e.find("#field-btt-enabled").prop("checked"),scrollThreshold:parseInt(e.find("#field-btt-threshold").val(),10)||300,position:e.find("#field-btt-position").val()||"bottom-right",offset:parseInt(e.find("#field-btt-offset").val(),10)||32,bottomOffset:parseInt(e.find("#field-btt-bottom-offset").val(),10)||32,label:e.find("#field-btt-label").val().trim(),smooth:e.find("#field-btt-smooth").prop("checked")},cookieConsent:{enabled:e.find("#field-cc-enabled").prop("checked"),message:e.find("#field-cc-message").val().trim(),acceptAllText:e.find("#field-cc-accept-all").val().trim(),rejectAllText:e.find("#field-cc-reject-all").val().trim(),customizeText:e.find("#field-cc-customize").val().trim(),savePreferencesText:e.find("#field-cc-save-prefs").val().trim(),privacyPolicyText:e.find("#field-cc-privacy-text").val().trim(),privacyPolicyUrl:e.find("#field-cc-privacy-url").val().trim(),cookiePolicyText:e.find("#field-cc-cookie-text").val().trim(),cookiePolicyUrl:e.find("#field-cc-cookie-url").val().trim(),position:e.find("#field-cc-position").val(),layout:e.find("#field-cc-layout").val(),theme:e.find("#field-cc-theme").val(),showFunctional:e.find("#field-cc-show-functional").prop("checked"),showAnalytics:e.find("#field-cc-show-analytics").prop("checked"),showMarketing:e.find("#field-cc-show-marketing").prop("checked"),consentVersion:e.find("#field-cc-version").val().trim()||"1.0"}},g=e.find("#save-settings-btn");g.prop("disabled",!0);try{await C.settings.save(o),Domma.theme.set(t),E.toast("Settings saved.",{type:"success"})}catch{E.toast("Failed to save settings.",{type:"error"})}finally{g.prop("disabled",!1)}}),e.find("#send-test-email-btn").on("click",async()=>{const t=e.find("#field-test-email-to").val().trim(),s=e.find("#test-email-result").get(0),n=e.find("#send-test-email-btn");n.prop("disabled",!0),s&&(s.textContent="Sending\u2026",s.style.color="");try{const o=await v("/settings/test-email",{method:"POST",body:JSON.stringify({to:t||void 0})});s&&(s.textContent=o.message||"Test email sent.",s.style.color="var(--success,#4ade80)")}catch(o){s&&(s.textContent=o.message||"Failed to send test email.",s.style.color="var(--danger,#f87171)")}finally{n.prop("disabled",!1)}}),e.find("#save-breadcrumbs-btn").on("click",async()=>{const t=e.find("#save-breadcrumbs-btn");t.prop("disabled",!0);try{const s={enabled:e.find("#field-breadcrumbs-enabled").prop("checked"),homeLabel:e.find("#field-breadcrumbs-home-label").val().trim()||"Home",position:e.find("#field-breadcrumbs-position").val()||"TL",offsetX:parseInt(e.find("#field-bc-offset-x").val(),10)||8,offsetY:parseInt(e.find("#field-bc-offset-y").val(),10)||8};await C.settings.save({...l,breadcrumbs:s}),l.breadcrumbs=s,E.toast("Breadcrumbs settings saved.",{type:"success"})}catch{E.toast("Failed to save breadcrumbs settings.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#save-css-btn").on("click",async()=>{const t=a?a.getValue():e.find("#field-custom-css").val(),s=e.find("#save-css-btn");s.prop("disabled",!0);try{await v("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:t})}),E.toast("Custom CSS saved.",{type:"success"})}catch(n){E.toast(n.message||"Failed to save CSS.",{type:"error"})}finally{s.prop("disabled",!1)}});async function r(){try{const t=await v("/cache/status"),s=t.enabled?`enabled (${t.driver})`:"disabled",n=t.maxItems?` \xB7 ${t.size} / ${t.maxItems} entries`:"";e.find("#cache-status").text(s+n)}catch{e.find("#cache-status").text("unknown")}try{const{entries:t}=await v("/cache/keys");A(e.find("#cache-keys-table"),t),e.find("#cache-key-count").text(`(${t.length})`)}catch(t){e.find("#cache-keys-table").get(0).textContent="Failed to load keys: "+t.message}}await r(),e.find("#btn-clear-cache").on("click",async()=>{if(!await E.confirm("Clear the entire response cache?"))return;const t=e.find("#btn-clear-cache");t.prop("disabled",!0);try{await v("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"}),await r()}catch(s){E.toast(s.message||"Failed to clear cache.",{type:"error"})}finally{t.prop("disabled",!1)}}),e.find("#btn-refresh-cache-keys").on("click",r)}};function A(e,k){const l=e.get(0);if(!l)return;for(;l.firstChild;)l.removeChild(l.firstChild);if(!k.length){const f=document.createElement("p");f.className="text-muted",f.style.fontSize=".875rem",f.textContent="Cache is empty.",l.appendChild(f);return}const p=[...k].reverse(),h=document.createElement("table");h.className="table table-compact";const b=document.createElement("thead"),y=document.createElement("tr");for(const f of["Key","Tags","Expires"]){const i=document.createElement("th");i.textContent=f,y.appendChild(i)}b.appendChild(y),h.appendChild(b);const u=document.createElement("tbody");for(const f of p){const i=document.createElement("tr"),d=document.createElement("td");d.style.fontFamily="monospace",d.style.fontSize="12px",d.textContent=f.key,i.appendChild(d);const m=document.createElement("td");m.style.fontSize="12px";for(const a of f.tags){const r=document.createElement("span");r.className="badge",r.style.marginRight="4px",r.style.fontSize="11px",r.textContent=a,m.appendChild(r)}i.appendChild(m);const c=document.createElement("td");if(c.style.fontSize="12px",c.style.color="var(--dm-muted, #6b7280)",f.expiresAt===null)c.textContent="no expiry";else{const a=f.expiresAt-Date.now();a<=0?c.textContent="expired":a<6e4?c.textContent=`${Math.round(a/1e3)}s`:a<36e5?c.textContent=`${Math.round(a/6e4)}m`:c.textContent=`${Math.round(a/36e5)}h`}i.appendChild(c),u.appendChild(i)}h.appendChild(u),l.appendChild(h)}
|
package/package.json
CHANGED
package/public/js/forms.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const targets=document.querySelectorAll("[data-form]");targets.length&&targets.forEach(initFormTarget);function showMessage(
|
|
1
|
+
const targets=document.querySelectorAll("[data-form]");targets.length&&targets.forEach(initFormTarget);function showMessage(c,s,t){const e=c.querySelector(".fb-form-success, .fb-form-error");e&&e.remove();const i=document.createElement("div");i.className=t==="success"?"fb-form-success":"fb-form-error",i.textContent=s,c.appendChild(i)}function attachRuntimeLifecycle(c,s){if(c._formLogicRuntime=s,!c.parentNode||typeof MutationObserver>"u")return;const t=new MutationObserver(function(e){for(const i of e)for(const n of i.removedNodes)if(n===c||n.nodeType===1&&n.contains&&n.contains(c)){s.destroy(),t.disconnect();return}});t.observe(c.parentNode,{childList:!0,subtree:!1})}function buildBlueprintFromFields(c,s){const t={};return c.forEach(function(e){if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const i=e.type==="checkbox"?"boolean":e.type==="date"?"string":e.type,n={...e.formConfig||{}};n.span==="full"&&s&&(n.span=s);const r={type:i,label:e.label||e.name,required:e.required||!1,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{helperText:e.helper},...e.tooltip&&{tooltip:e.tooltip},...n}};e.minLength!==void 0&&(r.minLength=e.minLength),e.maxLength!==void 0&&(r.maxLength=e.maxLength),e.min!==void 0&&(r.min=e.min),e.max!==void 0&&(r.max=e.max),e.type==="chooser"&&(e.variant&&(r.variant=e.variant),e.multiple&&(r.multiple=!0),e.density&&(r.density=e.density),e.columns&&(r.columns=e.columns),e.accent&&(r.accent=e.accent),e.accentStyle&&(r.accentStyle=e.accentStyle),e.glow&&(r.glow=!0),e.glowColour&&(r.glowColour=e.glowColour),e.shadow&&(r.shadow=e.shadow),e.shadowColour&&(r.shadowColour=e.shadowColour)),t[e.name]=r}),t}function buildInitialData(c){const s={};return c.forEach(function(t){if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(s[t.name]=typeof e=="object"?e.value:e)}}),s}function patchDateInputs(c,s){(s||[]).forEach(function(t){if(t.type!=="date"||!t.name)return;const e=c.querySelector('[name="'+t.name+'"]');e&&e.type!=="date"&&(e.type="date")})}function buildWizardSteps(c,s){const t=[];let e=[],i=s||"Step 1",n="";return c.forEach(function(r){r.type==="page-break"?(t.push({title:i,description:n,fieldGroup:e}),e=[],i=r.label||"Step "+(t.length+1),n=r.description||""):r.type!=="spacer"&&e.push(r)}),t.push({title:i,description:n,fieldGroup:e}),t}function injectHoneypot(c){const s=document.createElement("div");s.className="fb-form-honeypot",s.setAttribute("aria-hidden","true");const t=document.createElement("input");t.name="website",t.type="text",t.tabIndex=-1,t.autocomplete="url",t.placeholder="https://",s.appendChild(t);const e=document.createElement("input");e.name="_t",e.type="hidden",e.value=Date.now(),s.appendChild(e),c.appendChild(s)}function injectSpacers(c,s){const t=c.querySelector("form");if(!t)return;const e=Array.from(t.querySelectorAll(".form-group"));let i=0;s.forEach(function(n){if(n.type==="spacer"){const r=document.createElement("div");r.className="fb-spacer";const a=e[i];if(a)t.insertBefore(r,a);else{const l=t.querySelector('[type="submit"]');l?t.insertBefore(r,l):t.appendChild(r)}}else n.type!=="page-break"&&i++})}function submitForm(c,s,t,e,i){const n=i||e,r=n.querySelector('[name="website"]')?.value||"",a=n.querySelector('[name="_t"]')?.value||"",l=Object.assign({},s,{_hp:r,_t:a});return fetch("/api/forms/submit/"+c,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(l)}).then(m=>m.json().then(u=>({ok:m.ok,body:u}))).then(m=>{m.ok&&m.body.ok?(e.textContent="",showMessage(e,m.body.message||t.successMessage||"Thank you.","success")):showMessage(e,m.body.error||"Something went wrong.","error")}).catch(()=>{showMessage(e,"Unable to submit. Please check your connection.","error")})}function renderManualForm(c,s,t,e,i){const n=document.createElement("form");n.noValidate=!0,s.forEach(function(a){const l=document.createElement("div");l.className="form-group",l.style.marginBottom="1.25rem";const m=document.createElement("label");if(m.className="form-label",m.textContent=a.label||a.name,a.required){const o=document.createElement("span");o.textContent=" *",o.style.color="#f87171",m.appendChild(o)}let u;a.type==="textarea"?(u=document.createElement("textarea"),u.rows=a.formConfig?.rows||4,u.className="form-input"):a.type==="select"?(u=document.createElement("select"),u.className="form-input",(a.options||[]).forEach(function(o){const p=document.createElement("option");p.value=typeof o=="string"?o:o.value??"",p.textContent=typeof o=="string"?o:o.label||p.value,u.appendChild(p)})):(u=document.createElement("input"),u.type=a.type||"text",u.className="form-input",a.placeholder&&(u.placeholder=a.placeholder)),u.name=a.name,u.required=a.required||!1,l.appendChild(m),l.appendChild(u),n.appendChild(l)}),t.honeypot&&injectHoneypot(n);const r=document.createElement("button");r.type="submit",r.className="btn btn-primary",r.textContent=t.submitText||"Submit",n.appendChild(r),n.addEventListener("submit",function(a){a.preventDefault();const l={};if(s.forEach(function(m){const u=n.querySelector('[name="'+m.name+'"]');u&&(l[m.name]=u.value)}),window.FormLogicEngine&&i){const m=window.FormLogicEngine,u=[],o=[];if(s.forEach(function(p){if(m.evaluateFieldVisibility(p,l)==="hidden"){delete l[p.name];return}const d=m.evaluateFieldRequirement(p,l),f=l[p.name];d&&(!f||!String(f).trim())&&u.push(p.label||p.name);const h=m.validateField(p,f||"",l);h.length&&o.push(h[0].message)}),u.length||o.length){const p=[];u.length&&p.push("Required: "+u.join(", ")),o.length&&p.push(o.join("; ")),showMessage(c,p.join(". "),"error");return}}c.classList.add("fb-form-loading"),r.disabled=!0,submitForm(e,l,t,c,n).finally(function(){c.classList.remove("fb-form-loading"),r.disabled=!1})}),c.appendChild(n),window.FormLogicEngine&&i&&s.some(a=>a.logic)&&new window.FormLogicEngine.FormLogicRuntime(i,c).init()}function initFormTarget(c){const s=c.getAttribute("data-form");s&&fetch("/api/forms/"+s+"/public").then(t=>{if(!t.ok)throw new Error("Form not found: "+s);return t.json()}).then(t=>{const e=t.fields||[],i=t.settings||{},n=document.createElement("div");n.className="fb-form-wrapper",c.appendChild(n);const r=e.some(a=>a.type==="page-break");if(typeof Domma<"u"&&Domma.forms){const a=i.columns||1;if(r&&Domma.forms.wizard){const m=buildWizardSteps(e,t.title).map(function(o){return{title:o.title,description:o.description,fields:buildBlueprintFromFields(o.fieldGroup,a)}}),u=Domma.forms.wizard(n,{schema:{steps:m},onSubmit:function(o){return submitForm(s,o,i,n,null)}});Promise.resolve(u).then(function(){if(patchDateInputs(n,e),window.FormLogicEngine&&e.some(o=>o.logic)){const o=new window.FormLogicEngine.FormLogicRuntime(t,n);o.init(),attachRuntimeLifecycle(n,o)}if(i.honeypot){const o=n.querySelector("form");o&&injectHoneypot(o)}})}else if(Domma.forms.render){const l=buildBlueprintFromFields(e,a),m=buildInitialData(e),u=Domma.forms.render(n,l,m,{submitText:i.submitText||"Submit",layout:i.layout||"stacked",columns:a,onSubmit:function(o){return submitForm(s,o,i,n,null)}});Promise.resolve(u).then(function(){if(patchDateInputs(n,e),window.FormLogicEngine&&e.some(o=>o.logic)){const o=new window.FormLogicEngine.FormLogicRuntime(t,n);o.init(),attachRuntimeLifecycle(n,o)}if(e.some(o=>o.type==="spacer")&&injectSpacers(n,e),i.honeypot){const o=n.querySelector("form");o&&injectHoneypot(o)}})}}else renderManualForm(n,e.filter(a=>a.type!=="page-break"&&a.type!=="spacer"),i,s,t)}).catch(t=>{const e=document.createElement("p");e.textContent="Form unavailable.",e.style.cssText="color:#f87171;font-style:italic;",c.appendChild(e),console.warn("[forms]",t.message)})}
|
package/public/js/site.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
$(()=>{const w=window.__CMS_NAV__||{},l=window.__CMS_SITE__||{};if(l.autoTheme?.enabled){let t=function(s){const h=(s||"07:00").split(":");return+h[0]*60+(+h[1]||0)},o=function(){const s=new Date,h=s.getHours()*60+s.getMinutes();return h>=t(e.dayStart)&&h<t(e.nightStart)?e.dayTheme:e.nightTheme};var i=t,d=o;const e=l.autoTheme;Domma.theme.set(o()),setInterval(()=>Domma.theme.set(o()),6e4)}if($("#site-navbar").length&&w.brand){const e={...w.brand},t=e.size&&e.size!=="md"?` navbar-brand-${e.size}`:"";if(e.logo||e.icon||e.tagline){let o="";e.logo?o+=`<img src="${e.logo}" class="navbar-brand-logo" alt="${e.text||""}">`:e.icon&&(o+=`<span data-icon="${e.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),e.text&&(o+=`<span class="navbar-brand-text${t}">${e.text}</span>`),e.tagline&&(o+=`<small class="navbar-brand-tagline">${e.tagline}</small>`),e.html=o}else t&&e.text&&(e.html=`<span class="navbar-brand-text${t}">${e.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:e,items:w.items||[],variant:w.variant||"dark",position:w.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar")}const v=$("#site-footer");if(v.length){const e=l.social||{},t={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let o='<div class="footer-inner container">';if(l.footer){const m=l.footer;o+=`<p>${m.copyright||""}</p>`,m.links?.length&&(o+='<nav class="footer-links">',m.links.forEach(n=>{o+=`<a href="${n.url}">${n.text}</a>`}),o+="</nav>");const f=Object.keys(t).filter(n=>e[n]);f.length&&(o+='<div class="footer-social">',f.forEach(n=>{const{label:a,svg:r}=t[n];o+=`<a href="${e[n]}" target="_blank" rel="noopener noreferrer" aria-label="${a}" class="footer-social-link">${r}</a>`}),o+="</div>")}o+="</div>",v.html(o);const s=S.get("reduced_motion"),h=s!==null?!!s:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),g=v.get(0).querySelector(".footer-inner");if(g){const m=document.createElement("input");m.type="checkbox",m.className="form-switch-input",m.id="dm-motion-switch",m.checked=h,m.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const f=document.createElement("span");f.className="form-switch-label",f.textContent="Reduce motion";const n=document.createElement("label");n.className="form-switch footer-motion-switch",n.title="Reduce motion",n.appendChild(m),n.appendChild(f),g.appendChild(n)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan();const p=$(".page-body");if(p.length){p.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),p.find(".tabs").each(function(){Domma.elements.tabs(this)}),p.find(".carousel").each(function(){Domma.elements.carousel(this,{autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"})}),p.find(".dm-countdown").each(function(){const e={autoStart:!0};this.dataset.to&&(e.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(e.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(e.format=this.dataset.format),Domma.elements.timer(this,e)}),p.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),p.find(".dm-progression").each(function(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(e=>{const t=e.dataset.revealMode||"stagger",o=e.dataset.revealAnimation||"slide-up",s=parseInt(e.dataset.revealDuration,10)||400,h=parseInt(e.dataset.revealStagger,10)||60,g=parseInt(e.dataset.revealDelay,10)||0,m=e.dataset.revealDirection==="rtl",f=Array.from(e.children),n={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};f.forEach((a,r)=>{a.style.opacity="0",a.style.transform=n[o]||"",a.style.transition=`opacity ${s}ms ease, transform ${s}ms ease`;const c=m?f.length-1-r:r;a.style.transitionDelay=t==="stagger"?`${g+c*h}ms`:`${g}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const a=new IntersectionObserver(r=>{r.forEach(c=>{c.isIntersecting&&(c.target.offsetWidth,c.target.style.opacity="1",c.target.style.transform="none",a.unobserve(c.target))})},{threshold:.1});f.forEach(r=>a.observe(r))}))}),p.find(".card[data-collapsible]").each(function(){const e=this.querySelector(".card-header");e&&e.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),p.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const e=this.dataset.soTarget,t=document.getElementById(e);if(!t)return;const o=E.slideover({title:t.dataset.soTitle||"",size:t.dataset.soSize||"md",position:t.dataset.soPosition||"right"});t.style.display="",o.setContent(t),o.open()})})}if(typeof $.setup=="function"){const e=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(t=>{try{const o=atob(t.dataset.config),s=JSON.parse(o);Object.assign(e,s)}catch{}}),Object.keys(e).length>0){const t={};for(const[o,s]of Object.entries(e)){const h=s?.events?.click,{confirm:g,toast:m,alert:f,prompt:n,...a}=h||{};g||m||f||n?($(o).on("click",async function(c){if(c.preventDefault(),g&&!await E.confirm(g))return;let u=null;if(!(n&&(u=await E.prompt(n,{inputPlaceholder:a.promptPlaceholder||"",inputValue:a.promptDefault||""}),u===null))){if(a.target){const y=$(a.target);a.toggleClass&&y.toggleClass(a.toggleClass),a.addClass&&y.addClass(a.addClass),a.removeClass&&y.removeClass(a.removeClass),u!==null&&(a.setText&&y.text(u),a.setVal&&y.val(u),a.setAttr&&y.attr(a.setAttr,u))}a.href&&(window.location.href=a.href),m&&E.toast(m,{type:a.toastType||"success"}),f&&E.alert(f)}}),Object.keys(a).length&&(t[o]={...s,events:{...s.events,click:a}})):t[o]=s}$.setup(t)}}p.length&&wireCTAButtons(p.get(0))});function wireCTAButtons(w){w.querySelectorAll(".dm-cta-trigger").forEach(l=>{l.addEventListener("click",async()=>{const C=l.dataset.action,v=l.dataset.entry,b=l.dataset.confirm;let p=S.get("auth_token");if(!p){E.toast("Please log in to perform this action.",{type:"warning"});return}if(b&&!await E.confirm(b))return;const i=Array.from(l.childNodes).map(e=>e.cloneNode(!0));l.disabled=!0,l.textContent="Running\u2026";const d=e=>fetch(`/api/actions/${C}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`},body:JSON.stringify({entryId:v})});try{let e=await d(p);if(e.status===401){const o=S.get("auth_refresh_token");if(o){const s=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:o})});if(s.ok){const{token:h}=await s.json();S.set("auth_token",h),p=h,e=await d(p)}}}const t=await e.json().catch(()=>({}));if(!e.ok)throw new Error(t.error||t.message||`Error ${e.status}`);E.toast(t.message||"Action completed.",{type:"success"})}catch(e){E.toast(e.message||"Action failed.",{type:"error"})}finally{l.disabled=!1,l.textContent="",i.forEach(e=>l.appendChild(e)),Domma.icons.scan(l)}})})}(function(){const l=document.querySelectorAll("[data-collection-table]");!l.length||typeof T>"u"||!T.create||l.forEach(C=>{let v;try{v=JSON.parse(atob(C.dataset.payload))}catch{return}const{columns:b,rows:p,search:i,sortable:d,exportable:e,pageSize:t,empty:o,ctaConfig:s}=v;if(!b?.length)return;if(s){const m=b.findIndex(f=>f.key==="_cta");m!==-1&&(b[m]={key:"_cta",title:"",render:(f,n)=>{const a=document.createElement("button");if(a.className=`btn btn-${s.style||"primary"} dm-cta-trigger`,a.dataset.action=s.action||"",a.dataset.entry=n._entryId||"",s.confirm&&(a.dataset.confirm=s.confirm),s.icon){const r=document.createElement("span");r.dataset.icon=s.icon,a.appendChild(r),a.appendChild(document.createTextNode(" "))}return a.appendChild(document.createTextNode(s.label||"Run")),a}})}const h="col-table-"+Math.random().toString(36).slice(2,7),g=document.createElement("div");g.id=h,C.replaceChildren(g),T.create("#"+h,{data:p,columns:b,search:i,sortable:d,exportable:e,pageSize:t,emptyMessage:o}),s&&wireCTAButtons(g)})})(),(function(){const l=document.querySelectorAll("[data-form-inline]");if(!l.length||typeof F>"u")return;function C(i,d){const e={};return(i||[]).forEach(t=>{if(t.type==="page-break"||t.type==="spacer"||!t.name)return;const o=t.type==="checkbox"?"boolean":t.type==="date"?"string":t.type,s={...t.formConfig||{}};s.span==="full"&&d&&(s.span=d),e[t.name]={type:o,label:t.label,required:t.required,options:t.options,formConfig:{...t.placeholder&&{placeholder:t.placeholder},...t.helper&&{hint:t.helper},...s}}}),e}function v(i){const d={};return(i||[]).forEach(e=>{if(!(!e.name||e.type==="page-break"||e.type==="spacer")&&(e.type==="select"||e.type==="multiselect")&&e.required){const t=(e.options||[])[0];t&&(d[e.name]=typeof t=="object"?t.value:t)}}),d}function b(i,d){(d||[]).forEach(e=>{if(e.type!=="date"||!e.name)return;const t=i.querySelector(`[name="${e.name}"]`);t&&t.type!=="date"&&(t.type="date")})}function p(i,d,e){let t=i.querySelector(".cms-form-message");t||(t=document.createElement("p"),t.className="cms-form-message",i.appendChild(t)),t.textContent=d,t.style.cssText=e?"color:var(--danger,#f87171);margin-top:.75rem;":"color:var(--success,#4ade80);margin-top:.75rem;"}l.forEach(i=>{let d;try{d=JSON.parse(atob(i.dataset.formInline))}catch{return}const e=d.fields||[],t=d.settings||{},o=t.columns||1,s=t.layout||"stacked",h=e.some(n=>n.type==="page-break"),g=async n=>{try{const a=i.querySelector('[name="website"]'),r=i.querySelector('[name="_t"]'),c=Object.assign({},n);a!==null&&(c._hp=a.value),r!==null&&(c._t=r.value);const u=await H.post(`/api/forms/submit/${d.slug}`,c);if(u?.redirect){window.location.href=u.redirect;return}for(;i.firstChild;)i.removeChild(i.firstChild);p(i,u?.message||t.successMessage||"Thank you for your submission.",!1)}catch(a){throw p(i,a.message||"Submission failed. Please try again.",!0),a}};function m(n){const a=n.querySelector("form");if(!a)return;const r=document.createElement("div");r.className="fb-form-honeypot",r.setAttribute("aria-hidden","true");const c=document.createElement("input");c.name="website",c.type="text",c.tabIndex=-1,c.autocomplete="url",c.placeholder="https://",r.appendChild(c);const u=document.createElement("input");u.name="_t",u.type="hidden",u.value=Date.now(),r.appendChild(u),a.appendChild(r)}function f(){if(!window.FormLogicEngine||!e.some(a=>a.logic))return;const n=new window.FormLogicEngine.FormLogicRuntime(d,i);if(n.init(),i._formLogicRuntime=n,i.parentNode&&typeof MutationObserver<"u"){const a=new MutationObserver(function(r){for(const c of r)for(const u of c.removedNodes)if(u===i||u.nodeType===1&&u.contains&&u.contains(i)){n.destroy(),a.disconnect();return}});a.observe(i.parentNode,{childList:!0,subtree:!1})}}if(h&&F.wizard){const n=[];let a=[],r=d.title||"Step 1",c="";e.forEach(y=>{y.type==="page-break"?(n.push({title:r,description:c,fields:C(a,o)}),a=[],r=y.label||`Step ${n.length+1}`,c=y.description||""):y.type!=="spacer"&&a.push(y)}),n.push({title:r,description:c,fields:C(a,o)});const u=F.wizard(i,{schema:{steps:n},onSubmit:g});Promise.resolve(u).then(function(){b(i,e),t.honeypot!==!1&&m(i),f()})}else if(F.render){const n=F.render(i,C(e,o),v(e),{submitText:t.submitText||"Submit",layout:s,columns:o,onSubmit:g});Promise.resolve(n).then(function(){if(s==="grid"&&t.submitSpan==="full"){const a=i.querySelector(".form-buttons");a&&a.classList.add("col-span-full")}b(i,e),t.honeypot!==!1&&m(i),f()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
1
|
+
$(()=>{const w=window.__CMS_NAV__||{},d=window.__CMS_SITE__||{};if(d.autoTheme?.enabled){let e=function(s){const r=(s||"07:00").split(":");return+r[0]*60+(+r[1]||0)},o=function(){const s=new Date,r=s.getHours()*60+s.getMinutes();return r>=e(t.dayStart)&&r<e(t.nightStart)?t.dayTheme:t.nightTheme};var i=e,m=o;const t=d.autoTheme;Domma.theme.set(o()),setInterval(()=>Domma.theme.set(o()),6e4)}if($("#site-navbar").length&&w.brand){const t={...w.brand},e=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let o="";t.logo?o+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(o+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(o+=`<span class="navbar-brand-text${e}">${t.text}</span>`),t.tagline&&(o+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=o}else e&&t.text&&(t.html=`<span class="navbar-brand-text${e}">${t.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:t,items:w.items||[],variant:w.variant||"dark",position:w.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar")}const b=$("#site-footer");if(b.length){const t=d.social||{},e={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let o='<div class="footer-inner container">';if(d.footer){const u=d.footer;o+=`<p>${u.copyright||""}</p>`,u.links?.length&&(o+='<nav class="footer-links">',u.links.forEach(n=>{o+=`<a href="${n.url}">${n.text}</a>`}),o+="</nav>");const f=Object.keys(e).filter(n=>t[n]);f.length&&(o+='<div class="footer-social">',f.forEach(n=>{const{label:a,svg:c}=e[n];o+=`<a href="${t[n]}" target="_blank" rel="noopener noreferrer" aria-label="${a}" class="footer-social-link">${c}</a>`}),o+="</div>")}o+="</div>",b.html(o);const s=S.get("reduced_motion"),r=s!==null?!!s:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),g=b.get(0).querySelector(".footer-inner");if(g){const u=document.createElement("input");u.type="checkbox",u.className="form-switch-input",u.id="dm-motion-switch",u.checked=r,u.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const f=document.createElement("span");f.className="form-switch-label",f.textContent="Reduce motion";const n=document.createElement("label");n.className="form-switch footer-motion-switch",n.title="Reduce motion",n.appendChild(u),n.appendChild(f),g.appendChild(n)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan();const h=$(".page-body");if(h.length){h.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),h.find(".tabs").each(function(){Domma.elements.tabs(this)}),h.find(".carousel").each(function(){const t={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const e=parseInt(this.dataset.animationDuration,10);Number.isFinite(e)&&(t.animationDuration=e)}this.dataset.animationEasing&&(t.animationEasing=this.dataset.animationEasing),Domma.elements.carousel(this,t)}),h.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),h.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),h.find(".dm-progression").each(function(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(t=>{const e=t.dataset.revealMode||"stagger",o=t.dataset.revealAnimation||"slide-up",s=parseInt(t.dataset.revealDuration,10)||400,r=parseInt(t.dataset.revealStagger,10)||60,g=parseInt(t.dataset.revealDelay,10)||0,u=t.dataset.revealDirection==="rtl",f=Array.from(t.children),n={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};f.forEach((a,c)=>{a.style.opacity="0",a.style.transform=n[o]||"",a.style.transition=`opacity ${s}ms ease, transform ${s}ms ease`;const l=u?f.length-1-c:c;a.style.transitionDelay=e==="stagger"?`${g+l*r}ms`:`${g}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const a=new IntersectionObserver(c=>{c.forEach(l=>{l.isIntersecting&&(l.target.offsetWidth,l.target.style.opacity="1",l.target.style.transform="none",a.unobserve(l.target))})},{threshold:.1});f.forEach(c=>a.observe(c))}))}),h.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),h.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,e=document.getElementById(t);if(!e)return;const o=E.slideover({title:e.dataset.soTitle||"",size:e.dataset.soSize||"md",position:e.dataset.soPosition||"right"});e.style.display="",o.setContent(e),o.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(e=>{try{const o=atob(e.dataset.config),s=JSON.parse(o);Object.assign(t,s)}catch{}}),Object.keys(t).length>0){const e={};for(const[o,s]of Object.entries(t)){const r=s?.events?.click,{confirm:g,toast:u,alert:f,prompt:n,...a}=r||{};g||u||f||n?($(o).on("click",async function(l){if(l.preventDefault(),g&&!await E.confirm(g))return;let p=null;if(!(n&&(p=await E.prompt(n,{inputPlaceholder:a.promptPlaceholder||"",inputValue:a.promptDefault||""}),p===null))){if(a.target){const y=$(a.target);a.toggleClass&&y.toggleClass(a.toggleClass),a.addClass&&y.addClass(a.addClass),a.removeClass&&y.removeClass(a.removeClass),p!==null&&(a.setText&&y.text(p),a.setVal&&y.val(p),a.setAttr&&y.attr(a.setAttr,p))}a.href&&(window.location.href=a.href),u&&E.toast(u,{type:a.toastType||"success"}),f&&E.alert(f)}}),Object.keys(a).length&&(e[o]={...s,events:{...s.events,click:a}})):e[o]=s}$.setup(e)}}h.length&&wireCTAButtons(h.get(0))});function wireCTAButtons(w){w.querySelectorAll(".dm-cta-trigger").forEach(d=>{d.addEventListener("click",async()=>{const C=d.dataset.action,b=d.dataset.entry,v=d.dataset.confirm;let h=S.get("auth_token");if(!h){E.toast("Please log in to perform this action.",{type:"warning"});return}if(v&&!await E.confirm(v))return;const i=Array.from(d.childNodes).map(t=>t.cloneNode(!0));d.disabled=!0,d.textContent="Running\u2026";const m=t=>fetch(`/api/actions/${C}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify({entryId:b})});try{let t=await m(h);if(t.status===401){const o=S.get("auth_refresh_token");if(o){const s=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:o})});if(s.ok){const{token:r}=await s.json();S.set("auth_token",r),h=r,t=await m(h)}}}const e=await t.json().catch(()=>({}));if(!t.ok)throw new Error(e.error||e.message||`Error ${t.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(t){E.toast(t.message||"Action failed.",{type:"error"})}finally{d.disabled=!1,d.textContent="",i.forEach(t=>d.appendChild(t)),Domma.icons.scan(d)}})})}(function(){const d=document.querySelectorAll("[data-collection-table]");!d.length||typeof T>"u"||!T.create||d.forEach(C=>{let b;try{b=JSON.parse(atob(C.dataset.payload))}catch{return}const{columns:v,rows:h,search:i,sortable:m,exportable:t,pageSize:e,empty:o,ctaConfig:s}=b;if(!v?.length)return;if(s){const u=v.findIndex(f=>f.key==="_cta");u!==-1&&(v[u]={key:"_cta",title:"",render:(f,n)=>{const a=document.createElement("button");if(a.className=`btn btn-${s.style||"primary"} dm-cta-trigger`,a.dataset.action=s.action||"",a.dataset.entry=n._entryId||"",s.confirm&&(a.dataset.confirm=s.confirm),s.icon){const c=document.createElement("span");c.dataset.icon=s.icon,a.appendChild(c),a.appendChild(document.createTextNode(" "))}return a.appendChild(document.createTextNode(s.label||"Run")),a}})}const r="col-table-"+Math.random().toString(36).slice(2,7),g=document.createElement("div");g.id=r,C.replaceChildren(g),T.create("#"+r,{data:h,columns:v,search:i,sortable:m,exportable:t,pageSize:e,emptyMessage:o}),s&&wireCTAButtons(g)})})(),(function(){const d=document.querySelectorAll("[data-form-inline]");if(!d.length||typeof F>"u")return;function C(i,m){const t={};return(i||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const o=e.type==="checkbox"?"boolean":e.type==="date"?"string":e.type,s={...e.formConfig||{}};s.span==="full"&&m&&(s.span=m);const r={type:o,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...s}};e.type==="chooser"&&(e.variant&&(r.variant=e.variant),e.multiple&&(r.multiple=!0),e.density&&(r.density=e.density),e.columns&&(r.columns=e.columns),e.accent&&(r.accent=e.accent),e.accentStyle&&(r.accentStyle=e.accentStyle),e.glow&&(r.glow=!0),e.glowColour&&(r.glowColour=e.glowColour),e.shadow&&(r.shadow=e.shadow),e.shadowColour&&(r.shadowColour=e.shadowColour)),t[e.name]=r}),t}function b(i){const m={};return(i||[]).forEach(t=>{if(!(!t.name||t.type==="page-break"||t.type==="spacer")&&(t.type==="select"||t.type==="multiselect")&&t.required){const e=(t.options||[])[0];e&&(m[t.name]=typeof e=="object"?e.value:e)}}),m}function v(i,m){(m||[]).forEach(t=>{if(t.type!=="date"||!t.name)return;const e=i.querySelector(`[name="${t.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function h(i,m,t){let e=i.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",i.appendChild(e)),e.textContent=m,e.style.cssText=t?"color:var(--danger,#f87171);margin-top:.75rem;":"color:var(--success,#4ade80);margin-top:.75rem;"}d.forEach(i=>{let m;try{m=JSON.parse(atob(i.dataset.formInline))}catch{return}const t=m.fields||[],e=m.settings||{},o=e.columns||1,s=e.layout||"stacked",r=t.some(n=>n.type==="page-break"),g=async n=>{try{const a=i.querySelector('[name="website"]'),c=i.querySelector('[name="_t"]'),l=Object.assign({},n);a!==null&&(l._hp=a.value),c!==null&&(l._t=c.value);const p=await H.post(`/api/forms/submit/${m.slug}`,l);if(p?.redirect){window.location.href=p.redirect;return}for(;i.firstChild;)i.removeChild(i.firstChild);h(i,p?.message||e.successMessage||"Thank you for your submission.",!1)}catch(a){throw h(i,a.message||"Submission failed. Please try again.",!0),a}};function u(n){const a=n.querySelector("form");if(!a)return;const c=document.createElement("div");c.className="fb-form-honeypot",c.setAttribute("aria-hidden","true");const l=document.createElement("input");l.name="website",l.type="text",l.tabIndex=-1,l.autocomplete="url",l.placeholder="https://",c.appendChild(l);const p=document.createElement("input");p.name="_t",p.type="hidden",p.value=Date.now(),c.appendChild(p),a.appendChild(c)}function f(){if(!window.FormLogicEngine||!t.some(a=>a.logic))return;const n=new window.FormLogicEngine.FormLogicRuntime(m,i);if(n.init(),i._formLogicRuntime=n,i.parentNode&&typeof MutationObserver<"u"){const a=new MutationObserver(function(c){for(const l of c)for(const p of l.removedNodes)if(p===i||p.nodeType===1&&p.contains&&p.contains(i)){n.destroy(),a.disconnect();return}});a.observe(i.parentNode,{childList:!0,subtree:!1})}}if(r&&F.wizard){const n=[];let a=[],c=m.title||"Step 1",l="";t.forEach(y=>{y.type==="page-break"?(n.push({title:c,description:l,fields:C(a,o)}),a=[],c=y.label||`Step ${n.length+1}`,l=y.description||""):y.type!=="spacer"&&a.push(y)}),n.push({title:c,description:l,fields:C(a,o)});const p=F.wizard(i,{schema:{steps:n},onSubmit:g});Promise.resolve(p).then(function(){v(i,t),e.honeypot!==!1&&u(i),f()})}else if(F.render){const n=F.render(i,C(t,o),b(t),{submitText:e.submitText||"Submit",layout:s,columns:o,onSubmit:g});Promise.resolve(n).then(function(){if(s==="grid"&&e.submitSpan==="full"){const a=i.querySelector(".form-buttons");a&&a.classList.add("col-span-full")}v(i,t),e.honeypot!==!1&&u(i),f()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
package/server/config.js
CHANGED
|
@@ -49,8 +49,19 @@ export function saveConfig(name, data) {
|
|
|
49
49
|
const serverConfig = loadJson('server');
|
|
50
50
|
if (process.env.PORT) serverConfig.port = parseInt(process.env.PORT, 10);
|
|
51
51
|
|
|
52
|
+
const cacheDefaults = {
|
|
53
|
+
enabled: process.env.NODE_ENV === 'production',
|
|
54
|
+
driver: 'memory',
|
|
55
|
+
memory: {maxItems: 1000, defaultTtlSeconds: 3600}
|
|
56
|
+
};
|
|
57
|
+
const cacheFile = path.join(CONFIG_DIR, 'cache.json');
|
|
58
|
+
const cacheConfig = fs.existsSync(cacheFile)
|
|
59
|
+
? {...cacheDefaults, ...JSON.parse(fs.readFileSync(cacheFile, 'utf8'))}
|
|
60
|
+
: cacheDefaults;
|
|
61
|
+
|
|
52
62
|
export const config = {
|
|
53
63
|
server: serverConfig,
|
|
54
64
|
auth: loadJson('auth'),
|
|
55
|
-
content: loadJson('content')
|
|
65
|
+
content: loadJson('content'),
|
|
66
|
+
cache: cacheConfig
|
|
56
67
|
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache API
|
|
3
|
+
* GET /api/cache/status - report enabled flag plus live stats
|
|
4
|
+
* GET /api/cache/keys - list cached keys with their tags (no values)
|
|
5
|
+
* POST /api/cache/toggle - flip enabled on/off, persist to config/cache.json
|
|
6
|
+
* POST /api/cache/clear - flush every cached entry
|
|
7
|
+
*
|
|
8
|
+
* All endpoints are admin-only.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import {fileURLToPath} from 'url';
|
|
13
|
+
import * as cache from '../../services/cache/index.js';
|
|
14
|
+
import {authenticate, requireAdmin} from '../../middleware/auth.js';
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const CACHE_CONFIG_FILE = path.resolve(__dirname, '../../../config/cache.json');
|
|
18
|
+
|
|
19
|
+
const CACHE_DEFAULTS = {
|
|
20
|
+
enabled: process.env.NODE_ENV === 'production',
|
|
21
|
+
driver: 'memory',
|
|
22
|
+
memory: {maxItems: 1000, defaultTtlSeconds: 3600}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function readCacheConfig() {
|
|
26
|
+
try {
|
|
27
|
+
return {...CACHE_DEFAULTS, ...JSON.parse(await fs.readFile(CACHE_CONFIG_FILE, 'utf8'))};
|
|
28
|
+
} catch {
|
|
29
|
+
return {...CACHE_DEFAULTS};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function writeCacheConfig(cfg) {
|
|
34
|
+
await fs.writeFile(CACHE_CONFIG_FILE, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function cacheRoutes(fastify) {
|
|
38
|
+
const adminOnly = {preHandler: [authenticate, requireAdmin]};
|
|
39
|
+
|
|
40
|
+
fastify.get('/cache/status', adminOnly, async () => cache.getStats());
|
|
41
|
+
|
|
42
|
+
fastify.get('/cache/keys', adminOnly, async () => ({entries: cache.listEntries()}));
|
|
43
|
+
|
|
44
|
+
fastify.post('/cache/toggle', adminOnly, async (request, reply) => {
|
|
45
|
+
const enabled = !!request.body?.enabled;
|
|
46
|
+
const cfg = await readCacheConfig();
|
|
47
|
+
cfg.enabled = enabled;
|
|
48
|
+
await writeCacheConfig(cfg);
|
|
49
|
+
await cache.setEnabled(enabled);
|
|
50
|
+
return reply.send(cache.getStats());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
fastify.post('/cache/clear', adminOnly, async () => {
|
|
54
|
+
await cache.clear();
|
|
55
|
+
return cache.getStats();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import {getConfig, saveConfig} from '../../config.js';
|
|
7
7
|
import {authenticate, requirePermission} from '../../middleware/auth.js';
|
|
8
|
+
import * as cache from '../../services/cache/index.js';
|
|
8
9
|
|
|
9
10
|
export async function navigationRoutes(fastify) {
|
|
10
11
|
const canRead = {preHandler: [authenticate, requirePermission('navigation', 'read')]};
|
|
@@ -35,6 +36,7 @@ export async function navigationRoutes(fastify) {
|
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
saveConfig('navigation', data);
|
|
39
|
+
await cache.invalidateTags(['nav']);
|
|
38
40
|
return { success: true };
|
|
39
41
|
});
|
|
40
42
|
}
|
|
@@ -10,6 +10,7 @@ import nodemailer from 'nodemailer';
|
|
|
10
10
|
import fs from 'fs/promises';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import {fileURLToPath} from 'url';
|
|
13
|
+
import * as cache from '../../services/cache/index.js';
|
|
13
14
|
|
|
14
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
const CUSTOM_CSS_FILE = path.resolve(__dirname, '../../../content/custom.css');
|
|
@@ -64,6 +65,7 @@ export async function settingsRoutes(fastify) {
|
|
|
64
65
|
merged.smtp = {...merged.smtp, pass: existing.smtp.pass};
|
|
65
66
|
}
|
|
66
67
|
saveConfig('site', merged);
|
|
68
|
+
await cache.invalidateTags(['site']);
|
|
67
69
|
return { success: true };
|
|
68
70
|
});
|
|
69
71
|
|
|
@@ -133,6 +135,7 @@ export async function settingsRoutes(fastify) {
|
|
|
133
135
|
return reply.status(400).send({ error: 'CSS exceeds the 100 KB limit.' });
|
|
134
136
|
}
|
|
135
137
|
await fs.writeFile(CUSTOM_CSS_FILE, css, 'utf8');
|
|
138
|
+
await cache.invalidateTags(['site']);
|
|
136
139
|
return { success: true };
|
|
137
140
|
});
|
|
138
141
|
}
|
package/server/routes/public.js
CHANGED
|
@@ -8,6 +8,7 @@ import {getPage} from '../services/content.js';
|
|
|
8
8
|
import {renderPage} from '../services/renderer.js';
|
|
9
9
|
import {checkVisibility} from '../middleware/auth.js';
|
|
10
10
|
import {hooks} from '../services/hooks.js';
|
|
11
|
+
import * as cache from '../services/cache/index.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Escape user-controlled strings before interpolating into HTML.
|
|
@@ -64,9 +65,10 @@ export async function publicRoutes(fastify) {
|
|
|
64
65
|
return reply.type('text/html').send(await render404(urlPath));
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
// Enforce page visibility
|
|
68
|
+
// Enforce page visibility — role only resolved for gated pages,
|
|
69
|
+
// so public pages share a single cache entry keyed `roleanon`.
|
|
70
|
+
let userRole = null;
|
|
68
71
|
if (page.visibility && page.visibility !== 'public') {
|
|
69
|
-
let userRole = null;
|
|
70
72
|
try {
|
|
71
73
|
const decoded = await request.jwtVerify();
|
|
72
74
|
if (decoded.type === 'access') userRole = decoded.role;
|
|
@@ -78,7 +80,13 @@ export async function publicRoutes(fastify) {
|
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
const
|
|
83
|
+
const cacheKey = `page:${urlPath}:role${userRole ?? 'anon'}`;
|
|
84
|
+
const cacheTags = [`page:${urlPath}`, ...(page.tags || []), 'nav', 'site'];
|
|
85
|
+
const html = await cache.wrap(
|
|
86
|
+
cacheKey,
|
|
87
|
+
() => renderPage(page),
|
|
88
|
+
{tags: cacheTags}
|
|
89
|
+
);
|
|
82
90
|
hooks.emit('content:pageViewed', {urlPath, title: page.title || ''});
|
|
83
91
|
return reply.type('text/html').send(html);
|
|
84
92
|
});
|
package/server/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import {seedAll as seedPresetCollections} from './services/presetCollections.js'
|
|
|
24
24
|
import {seedDefaultBlocks} from './services/blocks.js';
|
|
25
25
|
import {seedDefaultComponents} from './services/components.js';
|
|
26
26
|
import {refreshComponentTagAllowlist} from './services/markdown.js';
|
|
27
|
+
import * as cache from './services/cache/index.js';
|
|
27
28
|
|
|
28
29
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
30
|
const ROOT = path.resolve(__dirname, '..');
|
|
@@ -122,13 +123,19 @@ await app.register(staticPlugin, {
|
|
|
122
123
|
decorateReply: false
|
|
123
124
|
});
|
|
124
125
|
|
|
125
|
-
// Serve admin panel assets — no-
|
|
126
|
+
// Serve admin panel assets — no-store on JS/CSS so ESM module URLs are never
|
|
127
|
+
// cached. `no-cache` revalidates but browsers may keep the parsed ESM module
|
|
128
|
+
// keyed by URL across reloads, which means a hard refresh can still load the
|
|
129
|
+
// stale code path. `no-store` skips the cache entirely. Other admin assets
|
|
130
|
+
// (images, html) keep no-cache. Local-only impact — admin is not public.
|
|
126
131
|
await app.register(staticPlugin, {
|
|
127
132
|
root: path.join(ROOT, 'admin'),
|
|
128
133
|
prefix: '/admin/',
|
|
129
134
|
decorateReply: false,
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
cacheControl: false,
|
|
136
|
+
setHeaders: (res, filePath) => {
|
|
137
|
+
const noStore = /\.(?:js|mjs|css)$/i.test(filePath);
|
|
138
|
+
res.setHeader('Cache-Control', noStore ? 'no-store' : 'no-cache');
|
|
132
139
|
}
|
|
133
140
|
});
|
|
134
141
|
|
|
@@ -172,6 +179,10 @@ await seedDefaultBlocks();
|
|
|
172
179
|
await seedDefaultComponents();
|
|
173
180
|
await refreshComponentTagAllowlist();
|
|
174
181
|
|
|
182
|
+
// Initialise response cache (Memory/None/Redis driver per config.cache).
|
|
183
|
+
await cache.initCache(config.cache);
|
|
184
|
+
console.log(`[cache] driver=${cache.isEnabled() ? config.cache.driver : 'disabled'}`);
|
|
185
|
+
|
|
175
186
|
// Serve uploaded media files — nosniff prevents browsers rendering spoofed content types
|
|
176
187
|
await app.register(staticPlugin, {
|
|
177
188
|
root: mediaDir,
|
|
@@ -250,6 +261,7 @@ const {versionsRoutes} = await import('./routes/api/versions.js');
|
|
|
250
261
|
const {effectsRoutes} = await import('./routes/api/effects.js');
|
|
251
262
|
const {notificationsRoutes} = await import('./routes/api/notifications.js');
|
|
252
263
|
const {dashboardRoutes} = await import('./routes/api/dashboard.js');
|
|
264
|
+
const {cacheRoutes} = await import('./routes/api/cache.js');
|
|
253
265
|
|
|
254
266
|
await app.register(pagesRoutes, { prefix: '/api' });
|
|
255
267
|
await app.register(settingsRoutes, { prefix: '/api' });
|
|
@@ -269,6 +281,7 @@ await app.register(versionsRoutes, {prefix: '/api'});
|
|
|
269
281
|
await app.register(effectsRoutes, {prefix: '/api'});
|
|
270
282
|
await app.register(notificationsRoutes, {prefix: '/api'});
|
|
271
283
|
await app.register(dashboardRoutes, {prefix: '/api'});
|
|
284
|
+
await app.register(cacheRoutes, {prefix: '/api'});
|
|
272
285
|
|
|
273
286
|
// ---------------------------------------------------------------------------
|
|
274
287
|
// CMS Plugins (server-side Fastify plugins from plugins/ directory)
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import fs from 'fs/promises';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import {fileURLToPath} from 'url';
|
|
9
|
+
import * as cache from './cache/index.js';
|
|
9
10
|
|
|
10
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const BLOCKS_DIR = path.resolve(__dirname, '../../content/blocks');
|
|
@@ -341,6 +342,7 @@ export async function saveBlock(name, content, {bundled, css} = {}) {
|
|
|
341
342
|
});
|
|
342
343
|
}
|
|
343
344
|
}
|
|
345
|
+
await cache.invalidateTags([`block:${name}`]);
|
|
344
346
|
return {success: true, name};
|
|
345
347
|
}
|
|
346
348
|
|
|
@@ -368,6 +370,7 @@ export async function deleteBlock(name) {
|
|
|
368
370
|
});
|
|
369
371
|
await fs.unlink(blockMetaPath(name)).catch(() => {
|
|
370
372
|
});
|
|
373
|
+
await cache.invalidateTags([`block:${name}`]);
|
|
371
374
|
}
|
|
372
375
|
|
|
373
376
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {LRU} from '../lru.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory cache driver with TTL and tag invalidation.
|
|
5
|
+
*
|
|
6
|
+
* Each entry is stored as {value, expiresAt, tags}. TTL is lazy — entries
|
|
7
|
+
* past their expiresAt are returned as undefined on the next get and dropped.
|
|
8
|
+
*
|
|
9
|
+
* Tag index: a Map<tag, Set<key>>. set() with {tags} adds the key to each
|
|
10
|
+
* tag's set; invalidateTags() iterates the listed tags and deletes every
|
|
11
|
+
* key in their sets. The forward index (entry.tags) lets us clean up old
|
|
12
|
+
* links when a key is re-set with a different tag list.
|
|
13
|
+
*/
|
|
14
|
+
export class MemoryDriver {
|
|
15
|
+
/**
|
|
16
|
+
* @param {{maxItems?: number, defaultTtlSeconds?: number}} options
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
const maxItems = options.maxItems ?? 1000;
|
|
20
|
+
this._defaultTtlSeconds = options.defaultTtlSeconds ?? null;
|
|
21
|
+
this._lru = new LRU(maxItems);
|
|
22
|
+
/** @type {Map<string, Set<string>>} */
|
|
23
|
+
this._tagIndex = new Map();
|
|
24
|
+
this._now = () => Date.now();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async get(key) {
|
|
28
|
+
const entry = this._lru.get(key);
|
|
29
|
+
if (!entry) return undefined;
|
|
30
|
+
if (entry.expiresAt !== null && entry.expiresAt <= this._now()) {
|
|
31
|
+
this._removeKey(key);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return entry.value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async set(key, value, opts = {}) {
|
|
38
|
+
const existing = this._lru.get(key);
|
|
39
|
+
if (existing && existing.tags) {
|
|
40
|
+
for (const t of existing.tags) {
|
|
41
|
+
const set = this._tagIndex.get(t);
|
|
42
|
+
if (set) {
|
|
43
|
+
set.delete(key);
|
|
44
|
+
if (set.size === 0) this._tagIndex.delete(t);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const ttl = opts.ttlSeconds ?? this._defaultTtlSeconds;
|
|
50
|
+
const expiresAt = ttl ? this._now() + ttl * 1000 : null;
|
|
51
|
+
const tags = opts.tags && opts.tags.length ? [...opts.tags] : null;
|
|
52
|
+
|
|
53
|
+
this._lru.set(key, {value, expiresAt, tags});
|
|
54
|
+
|
|
55
|
+
if (tags) {
|
|
56
|
+
for (const t of tags) {
|
|
57
|
+
let set = this._tagIndex.get(t);
|
|
58
|
+
if (!set) {
|
|
59
|
+
set = new Set();
|
|
60
|
+
this._tagIndex.set(t, set);
|
|
61
|
+
}
|
|
62
|
+
set.add(key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async invalidateTags(tags) {
|
|
68
|
+
for (const t of tags) {
|
|
69
|
+
const set = this._tagIndex.get(t);
|
|
70
|
+
if (!set) continue;
|
|
71
|
+
for (const key of [...set]) {
|
|
72
|
+
this._removeKey(key);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async clear() {
|
|
78
|
+
this._lru.clear();
|
|
79
|
+
this._tagIndex.clear();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Current entry count (sync — for the dashboard widget). */
|
|
83
|
+
size() {
|
|
84
|
+
return this._lru.size;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Snapshot the currently cached entries as `{key, tags, expiresAt}`.
|
|
89
|
+
* No values — they can be megabytes of HTML. Uses `entries()` (non-mutating)
|
|
90
|
+
* so the LRU order is preserved through the iteration.
|
|
91
|
+
*/
|
|
92
|
+
listEntries() {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const [key, entry] of this._lru.entries()) {
|
|
95
|
+
if (!entry) continue;
|
|
96
|
+
out.push({
|
|
97
|
+
key,
|
|
98
|
+
tags: entry.tags ? [...entry.tags] : [],
|
|
99
|
+
expiresAt: entry.expiresAt
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_removeKey(key) {
|
|
106
|
+
const entry = this._lru.get(key);
|
|
107
|
+
if (entry && entry.tags) {
|
|
108
|
+
for (const t of entry.tags) {
|
|
109
|
+
const set = this._tagIndex.get(t);
|
|
110
|
+
if (set) {
|
|
111
|
+
set.delete(key);
|
|
112
|
+
if (set.size === 0) this._tagIndex.delete(t);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this._lru.delete(key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Null driver — used when the cache is disabled.
|
|
3
|
+
* Every read misses; every write is dropped on the floor.
|
|
4
|
+
*/
|
|
5
|
+
export class NoneDriver {
|
|
6
|
+
async get() { return undefined; }
|
|
7
|
+
async set() { /* no-op */ }
|
|
8
|
+
async invalidateTags() { /* no-op */ }
|
|
9
|
+
async clear() { /* no-op */ }
|
|
10
|
+
size() { return 0; }
|
|
11
|
+
listEntries() { return []; }
|
|
12
|
+
}
|