@rubytech/create-maxy-code 0.1.125 → 0.1.127

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 (78) hide show
  1. package/dist/index.js +23 -1
  2. package/package.json +2 -2
  3. package/payload/platform/config/brand.json +20 -1
  4. package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts +33 -0
  5. package/payload/platform/lib/aeo-llms-txt-writer/dist/index.d.ts.map +1 -0
  6. package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js +119 -0
  7. package/payload/platform/lib/aeo-llms-txt-writer/dist/index.js.map +1 -0
  8. package/payload/platform/lib/aeo-llms-txt-writer/src/index.ts +172 -0
  9. package/payload/platform/lib/aeo-llms-txt-writer/tsconfig.json +8 -0
  10. package/payload/platform/lib/graph-style/dist/index.d.ts +78 -0
  11. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -0
  12. package/payload/platform/lib/graph-style/dist/index.js +296 -0
  13. package/payload/platform/lib/graph-style/dist/index.js.map +1 -0
  14. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +114 -0
  15. package/payload/platform/lib/graph-style/src/index.ts +307 -0
  16. package/payload/platform/lib/graph-style/tsconfig.json +9 -0
  17. package/payload/platform/lib/graph-style/vitest.config.ts +9 -0
  18. package/payload/platform/package-lock.json +318 -0
  19. package/payload/platform/package.json +3 -2
  20. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  21. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  22. package/payload/platform/plugins/admin/PLUGIN.md +4 -1
  23. package/payload/platform/plugins/admin/mcp/dist/index.js +73 -0
  24. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  25. package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts +34 -0
  26. package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.d.ts.map +1 -0
  27. package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js +176 -0
  28. package/payload/platform/plugins/admin/mcp/dist/tools/publish-site.js.map +1 -0
  29. package/payload/platform/plugins/admin/skills/publish-site/SKILL.md +19 -57
  30. package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.d.ts +5 -38
  31. package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.d.ts.map +1 -1
  32. package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.js +7 -114
  33. package/payload/platform/plugins/aeo/mcp/dist/tools/aeo-write-llms-txt.js.map +1 -1
  34. package/payload/platform/plugins/docs/references/visitor-graph.md +4 -3
  35. package/payload/platform/plugins/graph-viewer/.claude-plugin/plugin.json +8 -0
  36. package/payload/platform/plugins/graph-viewer/PLUGIN.md +56 -0
  37. package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts +7 -0
  38. package/payload/platform/plugins/graph-viewer/mcp/dist/index.d.ts.map +1 -0
  39. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js +74 -0
  40. package/payload/platform/plugins/graph-viewer/mcp/dist/index.js.map +1 -0
  41. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts +5 -0
  42. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.d.ts.map +1 -0
  43. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js +43 -0
  44. package/payload/platform/plugins/graph-viewer/mcp/dist/lib/neo4j.js.map +1 -0
  45. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts +28 -0
  46. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.d.ts.map +1 -0
  47. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js +73 -0
  48. package/payload/platform/plugins/graph-viewer/mcp/dist/render/draw.js.map +1 -0
  49. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts +40 -0
  50. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.d.ts.map +1 -0
  51. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js +77 -0
  52. package/payload/platform/plugins/graph-viewer/mcp/dist/render/layout.js.map +1 -0
  53. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts +45 -0
  54. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.d.ts.map +1 -0
  55. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js +170 -0
  56. package/payload/platform/plugins/graph-viewer/mcp/dist/tools/graph-render.js.map +1 -0
  57. package/payload/platform/plugins/graph-viewer/mcp/package.json +25 -0
  58. package/payload/platform/plugins/graph-viewer/mcp/vitest.config.ts +8 -0
  59. package/payload/platform/plugins/graph-viewer/skills/render-graph/SKILL.md +35 -0
  60. package/payload/platform/scripts/check-plugin-tools-mcp-consistency.mjs +136 -0
  61. package/payload/platform/templates/specialists/agents/content-producer.md +3 -3
  62. package/payload/server/public/assets/{admin-D6IfAzYY.js → admin-FcRHAL-3.js} +7 -7
  63. package/payload/server/public/assets/{data-BGbQyufe.js → data-Ds37mflX.js} +1 -1
  64. package/payload/server/public/assets/{graph-D-1lRTeL.js → graph-CmWRhaiS.js} +1 -1
  65. package/payload/server/public/assets/graph-labels-Ch2r00Gt.js +1 -0
  66. package/payload/server/public/assets/page-BOtNny_4.js +51 -0
  67. package/payload/server/public/assets/page-BcHhJXUt.js +1 -0
  68. package/payload/server/public/consent.css +97 -0
  69. package/payload/server/public/consent.js +259 -0
  70. package/payload/server/public/data.html +3 -3
  71. package/payload/server/public/graph.html +3 -3
  72. package/payload/server/public/index.html +4 -4
  73. package/payload/server/public/privacy.html +68 -5
  74. package/payload/server/public/v.js +53 -0
  75. package/payload/server/server.js +465 -176
  76. package/payload/server/public/assets/graph-labels-BRXJHNYE.js +0 -1
  77. package/payload/server/public/assets/page-B4oirCvn.js +0 -1
  78. package/payload/server/public/assets/page-FmJ7PIYx.js +0 -51
@@ -0,0 +1 @@
1
+ import{o as e}from"./chunk-DD-I1_y5.js";import{d as t,g as n,u as r,y as i}from"./ChatInput-CJo_77bp.js";import{_ as a,a as o,i as s,l as c,p as l,t as u,y as d}from"./graph-labels-Ch2r00Gt.js";var f=n(`circle-arrow-up`,[[`circle`,{cx:`12`,cy:`12`,r:`10`,key:`1mglay`}],[`path`,{d:`m16 12-4-4-4 4`,key:`177agl`}],[`path`,{d:`M12 16V8`,key:`1sbj14`}]]),p=n(`download`,[[`path`,{d:`M12 15V3`,key:`m9g1x1`}],[`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4`,key:`ih7n3h`}],[`path`,{d:`m7 10 5 5 5-5`,key:`brsn70`}]]),m=n(`file`,[[`path`,{d:`M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z`,key:`1oefj6`}],[`path`,{d:`M14 2v5a1 1 0 0 0 1 1h5`,key:`wfsgrz`}]]),h=n(`folder`,[[`path`,{d:`M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z`,key:`1kt360`}]]),g=n(`upload`,[[`path`,{d:`M12 3v12`,key:`1x0j5s`}],[`path`,{d:`m17 8-5-5-5 5`,key:`7q97r8`}],[`path`,{d:`M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4`,key:`ih7n3h`}]]);function _(e){let t=new Set,n=[];for(let r of e)for(let e of r.labels)e&&(t.has(e)||u.has(e)&&(t.add(e),n.push(e)));return n}var v=80;function y(e){let t=b(e.properties)??e.labels[0]??``,n=t.length>v?t.slice(0,v):t;return`/graph?${new URLSearchParams({nodeId:e.nodeId,label:n}).toString()}`}function b(e){for(let t of[`title`,`name`,`summary`,`headline`]){let n=e[t];if(typeof n==`string`&&n.length>0)return n}return null}var x=e(i(),1),S=r();function C(){let[e,t]=(0,x.useState)(null),[n,r]=(0,x.useState)(!1),[i,o]=(0,x.useState)(void 0),[l,u]=(0,x.useState)(null),[f,p]=(0,x.useState)(void 0),[m,h]=(0,x.useState)(null),[g,_]=(0,x.useState)(null);(0,x.useEffect)(()=>{let e=!1,n=null;try{n=sessionStorage.getItem(`maxy-admin-session-key`)}catch{}if(!n){t(null),r(!0);return}return fetch(`/api/admin/session?session_key=${encodeURIComponent(n)}`).then(async i=>{if(!e){if(i.status===401){try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}window.location.href=`/`;return}if(i.ok)try{let e=await i.json();typeof e.businessName==`string`&&o(e.businessName),e.conversationId!==void 0&&u(e.conversationId??null),_(e.role??null),p(e.userName===void 0?null:e.userName),h(e.avatar??null)}catch{}t(n),r(!0)}}).catch(()=>{e||(t(n),r(!0))}),()=>{e=!0}},[]);let v=(0,x.useCallback)(()=>{try{sessionStorage.removeItem(`maxy-admin-session-key`)}catch{}window.location.href=`/`},[]);return n?e?(0,S.jsxs)(s,{cacheKey:e,businessName:i,conversationId:l,onLogout:v,userName:f,userAvatar:m,role:g,children:[(0,S.jsx)(c,{}),(0,S.jsxs)(`div`,{className:`data-page`,children:[(0,S.jsx)(`header`,{className:`data-header`,children:(0,S.jsxs)(`h1`,{className:`data-title`,children:[(0,S.jsx)(d,{size:18}),` Data`]})}),(0,S.jsx)(w,{cacheKey:e})]})]}):(0,S.jsxs)(`div`,{className:`data-page`,children:[(0,S.jsx)(`header`,{className:`data-header`,children:(0,S.jsxs)(`h1`,{className:`data-title`,children:[(0,S.jsx)(d,{size:18}),` Data`]})}),(0,S.jsxs)(`div`,{className:`data-empty`,children:[(0,S.jsx)(`p`,{children:`You are not signed in.`}),(0,S.jsxs)(`p`,{children:[`Open the `,(0,S.jsx)(`a`,{href:`/`,className:`data-link`,children:`main admin page`}),` and log in, then return here.`]})]})]}):(0,S.jsx)(`div`,{className:`data-page`,children:(0,S.jsxs)(`div`,{className:`data-loading`,children:[(0,S.jsx)(a,{size:18,className:`spin`}),` Loading…`]})})}function w({cacheKey:e,onOpenInGraph:t}){let{adminFetch:n,cacheKey:r,sessionRefetchNonce:i,banner:a,reloadToLogin:s}=o({initialCacheKey:e,surface:`data`});return(0,S.jsxs)(`main`,{className:`data-main`,children:[a&&(0,S.jsx)(`button`,{type:`button`,className:`data-session-banner`,onClick:s,"data-reason":a.reason,children:a.message}),(0,S.jsx)(T,{adminFetch:n,sessionRefetchNonce:i,onOpenInGraph:t}),(0,S.jsx)(A,{adminFetch:n,cacheKey:r,sessionRefetchNonce:i})]})}function T({adminFetch:e,sessionRefetchNonce:t,onOpenInGraph:n}){let[r,i]=(0,x.useState)(``),[o,s]=(0,x.useState)(``),[c,u]=(0,x.useState)(null),[d,f]=(0,x.useState)(`hybrid`),[p,m]=(0,x.useState)(!1),[h,g]=(0,x.useState)(null),[v,b]=(0,x.useState)(null),[C,w]=(0,x.useState)(0),[T,O]=(0,x.useState)(!1);(0,x.useEffect)(()=>{let e=r.trim();if(!e){s(``),u(null);return}O(!1);let t=setTimeout(()=>s(e),300);return()=>clearTimeout(t)},[r]),(0,x.useEffect)(()=>{if(!o)return;let t=!1;return m(!0),g(null),e(`/api/admin/graph-search?q=${encodeURIComponent(o)}&labels=*&limit=20${T?`&threshold=0`:``}`).then(async e=>{let t=await e.json().catch(()=>({error:`HTTP ${e.status}`}));if(!e.ok)throw Error(t.error??`HTTP ${e.status}`);return t}).then(e=>{t||(u(e.results),f(e.mode??`hybrid`),w(typeof e.suppressed==`number`?e.suppressed:0))}).catch(e=>{t||g(e instanceof Error?e.message:String(e))}).finally(()=>{t||m(!1)}),()=>{t=!0}},[o,e,t,T]);let k=(0,x.useMemo)(()=>c?_(c):[],[c]),A=v&&k.includes(v)?v:null,j=(0,x.useMemo)(()=>c?A?c.filter(e=>e.labels.includes(A)):c:null,[c,A]),M=(0,x.useCallback)(e=>{if(n){n(e);return}window.location.href=y(e)},[n]);return(0,S.jsxs)(`section`,{className:`data-panel`,children:[(0,S.jsx)(`h2`,{className:`data-panel-title`,children:`Search`}),(0,S.jsx)(`p`,{className:`data-panel-subtitle`,children:`Find anything in your knowledge base by keyword.`}),(0,S.jsxs)(`div`,{className:`data-search-input`,children:[(0,S.jsx)(l,{size:14}),(0,S.jsx)(`input`,{type:`text`,placeholder:`Search your knowledge…`,value:r,onChange:e=>i(e.target.value),autoFocus:!0,autoComplete:`off`,spellCheck:!1}),p&&(0,S.jsx)(a,{size:14,className:`spin`})]}),h&&(0,S.jsxs)(`div`,{className:`data-error`,children:[`Search failed: `,h]}),k.length>0&&(0,S.jsx)(`div`,{className:`data-chip-row`,role:`group`,"aria-label":`Filter by type`,children:k.map(e=>{let t=e===A;return(0,S.jsx)(`button`,{type:`button`,className:`data-chip`,"data-active":t,onClick:()=>b(t?null:e),children:D([e])??e},e)})}),c&&!p&&(0,S.jsxs)(`div`,{className:`data-results-meta`,children:[j.length,` of `,c.length,` result`,c.length===1?``:`s`]}),c&&!p&&!A&&C>0&&!T&&(0,S.jsxs)(`button`,{type:`button`,className:`data-suppressed-banner`,onClick:()=>O(!0),children:[C,` low-confidence result`,C===1?``:`s`,` hidden — show all`]}),c&&!p&&!A&&T&&(0,S.jsx)(`button`,{type:`button`,className:`data-suppressed-banner`,onClick:()=>O(!1),children:`Showing all results — hide low-confidence`}),c&&c.length===0&&!p&&(0,S.jsx)(`div`,{className:`data-empty-results`,children:`No matches. Try a different keyword.`}),j&&j.length>0&&(0,S.jsx)(`ul`,{className:`data-results`,children:j.map(e=>(0,S.jsx)(E,{hit:e,mode:d,onOpenInGraph:M},e.nodeId))})]})}function E({hit:e,mode:t,onOpenInGraph:n}){let r=O(e.properties),i=k(e.properties),a=D(e.labels),[o,s]=(0,x.useState)(!1),c=i&&i.length>280,l=o||!c?i:i?.slice(0,280)+`…`,u=t===`bm25`?`bm25 ${e.bm25Score.toFixed(2)}`:`vector ${e.vectorScore.toFixed(2)} · bm25 ${e.bm25Score.toFixed(2)} · combined ${e.score.toFixed(2)}`;return(0,S.jsxs)(`li`,{className:`data-result`,children:[(0,S.jsxs)(`div`,{role:`button`,tabIndex:0,className:`data-result-open`,onClick:()=>n(e),onKeyDown:t=>{(t.key===`Enter`||t.key===` `)&&(t.preventDefault(),n(e))},"aria-label":`Open ${r??a??`node`} in graph`,children:[a&&(0,S.jsx)(`div`,{className:`data-result-header`,children:(0,S.jsx)(`span`,{className:`data-result-labels`,children:a})}),r&&(0,S.jsx)(`div`,{className:`data-result-title`,children:r}),i&&(0,S.jsx)(`pre`,{className:`data-result-body`,children:l}),(0,S.jsx)(`div`,{className:`data-result-scores`,children:u})]}),c&&(0,S.jsx)(`button`,{type:`button`,className:`data-result-toggle`,onClick:()=>s(e=>!e),children:o?`Show less`:`Show more`})]})}function D(e){if(e.length===0)return null;let t=e[0].replace(/([a-z])([A-Z])/g,`$1 $2`);return t.charAt(0).toUpperCase()+t.slice(1).toLowerCase()}function O(e){for(let t of[`title`,`name`,`summary`,`headline`]){let n=e[t];if(typeof n==`string`&&n.length>0)return n.length>140?n.slice(0,140)+`…`:n}return null}function k(e){for(let t of[`content`,`summary`,`body`,`description`,`text`]){let n=e[t];if(typeof n==`string`&&n.length>0)return n}return null}function A({adminFetch:e,cacheKey:n,sessionRefetchNonce:r}){let[i,o]=(0,x.useState)(`.`),[s,c]=(0,x.useState)(null),[l,u]=(0,x.useState)([]),[d,_]=(0,x.useState)(!1),[v,y]=(0,x.useState)(null),[b,C]=(0,x.useState)(!1),[w,T]=(0,x.useState)(null),[E,D]=(0,x.useState)(null),[O,k]=(0,x.useState)(null),[A,N]=(0,x.useState)(null),P=(0,x.useRef)(null),[F,I]=(0,x.useState)(0);(0,x.useEffect)(()=>{let t=!1;return _(!0),y(null),e(`/api/admin/files?path=${encodeURIComponent(i===`.`?``:i)}`).then(async e=>{let t=await e.json().catch(()=>({error:`HTTP ${e.status}`}));if(!e.ok)throw Error(t.error??`HTTP ${e.status}`);return t}).then(e=>{t||(c(e.entries),u(e.displayPath??[]))}).catch(e=>{t||(c([]),u([]),y(e instanceof Error?e.message:String(e)))}).finally(()=>{t||_(!1)}),()=>{t=!0}},[e,i,F,r]);let L=(0,x.useCallback)(e=>{o(e)},[]),R=(0,x.useCallback)(()=>{if(i===`.`||i===``)return;let e=i.split(`/`).filter(Boolean);e.pop(),o(e.length===0?`.`:e.join(`/`))},[i]),z=(0,x.useCallback)(e=>{let t=i===`.`?e.name:`${i}/${e.name}`,r=`/api/admin/files/download?session_key=${encodeURIComponent(n)}&path=${encodeURIComponent(t)}`,a=document.createElement(`a`);a.href=r,a.rel=`noopener noreferrer`,document.body.appendChild(a),a.click(),a.remove()},[i,n]),B=(0,x.useCallback)(()=>{P.current?.click()},[]),V=(0,x.useCallback)(e=>{D(null),N(e.name)},[]),H=(0,x.useCallback)(()=>{N(null)},[]),U=(0,x.useCallback)(async t=>{let n=i===`.`?t.name:`${i}/${t.name}`;D(null),N(null),k(t.name);try{let t=await e(`/api/admin/files?path=${encodeURIComponent(n)}`,{method:`DELETE`}),r=await t.json().catch(()=>({error:`HTTP ${t.status}`}));if(!t.ok)throw Error(r.error??`HTTP ${t.status}`);I(e=>e+1)}catch(e){D(e instanceof Error?e.message:String(e))}finally{k(null)}},[i,e]);(0,x.useEffect)(()=>{if(!A)return;let e=e=>{e.key===`Escape`&&N(null)};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[A]);let W=(0,x.useCallback)(async e=>{T(null),C(!0);try{let t=new FormData;t.append(`session_key`,n),t.append(`file`,e);let r=await fetch(`/api/admin/files/upload`,{method:`POST`,body:t}),i=await r.json().catch(()=>({error:`HTTP ${r.status}`}));if(!r.ok)throw Error(i.error??`HTTP ${r.status}`);o(i.path.split(`/`).slice(0,-1).join(`/`)||`.`),I(e=>e+1)}catch(e){T(e instanceof Error?e.message:String(e))}finally{C(!1),P.current&&(P.current.value=``)}},[n]);return(0,S.jsxs)(`section`,{className:`data-panel`,children:[(0,S.jsxs)(`div`,{className:`data-panel-heading`,children:[(0,S.jsx)(`h2`,{className:`data-panel-title`,children:`Files`}),(0,S.jsxs)(`div`,{className:`data-file-actions`,children:[(0,S.jsxs)(`button`,{type:`button`,className:`data-btn`,onClick:B,disabled:b,children:[b?(0,S.jsx)(a,{size:14,className:`spin`}):(0,S.jsx)(g,{size:14}),` Upload`]}),(0,S.jsx)(`input`,{type:`file`,ref:P,hidden:!0,onChange:e=>{let t=e.target.files?.[0];t&&W(t)}})]})]}),(0,S.jsxs)(`p`,{className:`data-panel-subtitle`,children:[`Everything under your install's `,(0,S.jsx)(`code`,{children:`data/`}),` directory. Click a folder to open it, a file to download it. Uploads go to `,(0,S.jsx)(`code`,{children:`data/uploads/`}),`.`]}),(0,S.jsxs)(`div`,{className:`data-breadcrumbs`,children:[(0,S.jsx)(`button`,{type:`button`,className:`data-crumb`,onClick:()=>L(`.`),children:`data`}),l.map((e,t)=>{let n=l.slice(0,t+1).map(e=>e.name).join(`/`);return(0,S.jsxs)(`span`,{className:`data-crumb-wrap`,children:[(0,S.jsx)(`span`,{className:`data-crumb-sep`,children:`/`}),(0,S.jsx)(`button`,{type:`button`,className:`data-crumb`,onClick:()=>L(n),title:e.name,children:e.displayName??e.name})]},n)}),i!==`.`&&(0,S.jsxs)(`button`,{type:`button`,className:`data-btn data-btn-ghost data-up-btn`,onClick:R,children:[(0,S.jsx)(f,{size:14}),` Up`]})]}),w&&(0,S.jsxs)(`div`,{className:`data-error`,children:[`Upload failed: `,w]}),E&&(0,S.jsxs)(`div`,{className:`data-error`,children:[`Delete failed: `,E]}),v&&(0,S.jsx)(`div`,{className:`data-error`,children:v}),d&&(0,S.jsxs)(`div`,{className:`data-loading`,children:[(0,S.jsx)(a,{size:14,className:`spin`}),` Loading…`]}),!d&&s&&s.length===0&&!v&&(0,S.jsx)(`div`,{className:`data-empty-results`,children:`Empty directory.`}),!d&&s&&s.length>0&&(0,S.jsx)(`ul`,{className:`data-entries`,children:s.map(e=>{let n=e.displayName??e.name,r=O===e.name,o=A===e.name;return(0,S.jsx)(`li`,{className:`data-entry`,children:e.kind===`directory`?(0,S.jsxs)(`button`,{type:`button`,className:`data-entry-btn`,onClick:()=>L(i===`.`?e.name:`${i}/${e.name}`),children:[(0,S.jsx)(h,{size:14}),(0,S.jsx)(`span`,{className:`data-entry-name`,title:e.name,children:n})]}):e.kind===`file`?(0,S.jsxs)(S.Fragment,{children:[(0,S.jsxs)(`button`,{type:`button`,className:`data-entry-btn`,onClick:()=>z(e),title:`Download ${n}`,disabled:o,children:[(0,S.jsx)(m,{size:14}),(0,S.jsx)(`span`,{className:`data-entry-name`,title:e.name,children:n}),(0,S.jsxs)(`span`,{className:`data-entry-meta`,title:`Modified ${M(e.modifiedAt)}`,children:[j(e.sizeBytes),` · `,M(e.modifiedAt)]}),(0,S.jsx)(p,{size:12,className:`data-entry-download-icon`})]}),o?(0,S.jsxs)(`div`,{className:`data-entry-confirm`,role:`group`,"aria-label":`Confirm delete ${n}`,children:[(0,S.jsx)(`span`,{className:`data-entry-confirm-label`,children:`Delete?`}),(0,S.jsx)(`button`,{type:`button`,className:`data-entry-confirm-yes`,onClick:()=>U(e),autoFocus:!0,children:`Yes`}),(0,S.jsx)(`button`,{type:`button`,className:`data-entry-confirm-no`,onClick:H,children:`No`})]}):(0,S.jsx)(`button`,{type:`button`,className:`data-entry-delete`,onClick:()=>V(e),disabled:r,title:`Delete ${n}`,"aria-label":`Delete ${n}`,children:r?(0,S.jsx)(a,{size:14,className:`spin`}):(0,S.jsx)(t,{size:14})})]}):(0,S.jsxs)(`div`,{className:`data-entry-btn data-entry-disabled`,children:[(0,S.jsx)(m,{size:14}),(0,S.jsx)(`span`,{className:`data-entry-name`,children:n}),(0,S.jsx)(`span`,{className:`data-entry-meta`,children:`special`})]})},e.name)})})]})}function j(e){return e==null?``:e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/1024/1024).toFixed(1)} MB`:`${(e/1024/1024/1024).toFixed(1)} GB`}function M(e){try{let t=new Date(e);if(isNaN(t.getTime()))return`—`;let n=typeof navigator<`u`?navigator.languages&&navigator.languages.length>0?[...navigator.languages]:[navigator.language]:void 0;return t.toLocaleString(n,{dateStyle:`medium`,timeStyle:`short`})}catch{return`—`}}export{p as i,w as n,b as r,C as t};
@@ -0,0 +1,97 @@
1
+ /* PECR consent banner styles (Task 375). Brand overrides arrive as inline
2
+ * CSS variables on :root from /consent.js after fetching /v/brand-config. */
3
+
4
+ :root {
5
+ --mxy-consent-bg: #FFFFFF;
6
+ --mxy-consent-fg: #1F1F1F;
7
+ --mxy-consent-accent: #1F1F1F;
8
+ --mxy-consent-accentFg: #FFFFFF;
9
+ --mxy-consent-border: #E5E5E2;
10
+ }
11
+
12
+ #mxy-consent {
13
+ position: fixed;
14
+ left: 0;
15
+ right: 0;
16
+ bottom: 0;
17
+ z-index: 2147483640;
18
+ background: var(--mxy-consent-bg);
19
+ color: var(--mxy-consent-fg);
20
+ border-top: 1px solid var(--mxy-consent-border);
21
+ box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.06);
22
+ font: 14px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
23
+ padding: 16px 20px env(safe-area-inset-bottom, 16px);
24
+ }
25
+
26
+ #mxy-consent .mxy-consent__inner {
27
+ max-width: 1100px;
28
+ margin: 0 auto;
29
+ display: flex;
30
+ gap: 24px;
31
+ align-items: center;
32
+ flex-wrap: wrap;
33
+ }
34
+
35
+ #mxy-consent .mxy-consent__text {
36
+ flex: 1 1 320px;
37
+ min-width: 240px;
38
+ }
39
+
40
+ #mxy-consent .mxy-consent__title {
41
+ display: block;
42
+ font-size: 15px;
43
+ margin-bottom: 4px;
44
+ }
45
+
46
+ #mxy-consent .mxy-consent__body {
47
+ margin: 0 0 6px 0;
48
+ color: var(--mxy-consent-fg);
49
+ }
50
+
51
+ #mxy-consent .mxy-consent__link {
52
+ color: var(--mxy-consent-fg);
53
+ text-decoration: underline;
54
+ font-size: 13px;
55
+ }
56
+
57
+ #mxy-consent .mxy-consent__actions {
58
+ display: flex;
59
+ gap: 10px;
60
+ flex: 0 0 auto;
61
+ }
62
+
63
+ /* Accept and reject are equal-weighted per ICO guidance — same size,
64
+ * same prominence. Reject is the outlined variant only because the
65
+ * accent fill must indicate the active surface in monochrome palettes;
66
+ * both buttons share padding, font-weight and minimum width. */
67
+
68
+ #mxy-consent .mxy-consent__btn {
69
+ appearance: none;
70
+ -webkit-appearance: none;
71
+ border-radius: 6px;
72
+ font: inherit;
73
+ font-weight: 600;
74
+ padding: 10px 18px;
75
+ min-width: 96px;
76
+ cursor: pointer;
77
+ border: 1px solid var(--mxy-consent-accent);
78
+ transition: opacity 120ms ease;
79
+ }
80
+
81
+ #mxy-consent .mxy-consent__btn:hover { opacity: 0.85; }
82
+ #mxy-consent .mxy-consent__btn:focus { outline: 2px solid var(--mxy-consent-accent); outline-offset: 2px; }
83
+
84
+ #mxy-consent .mxy-consent__btn--reject {
85
+ background: var(--mxy-consent-bg);
86
+ color: var(--mxy-consent-fg);
87
+ }
88
+
89
+ #mxy-consent .mxy-consent__btn--accept {
90
+ background: var(--mxy-consent-accent);
91
+ color: var(--mxy-consent-accentFg);
92
+ }
93
+
94
+ @media (max-width: 560px) {
95
+ #mxy-consent .mxy-consent__actions { width: 100%; }
96
+ #mxy-consent .mxy-consent__btn { flex: 1 1 0; }
97
+ }
@@ -0,0 +1,259 @@
1
+ /* PECR consent banner (Task 375).
2
+ *
3
+ * Loads BEFORE /v.js on any page that wires in the visitor pixel:
4
+ *
5
+ * <link rel="stylesheet" href="/consent.css">
6
+ * <script defer src="/consent.js"></script>
7
+ * <script defer src="/v.js" data-site="<slug>"></script>
8
+ *
9
+ * Responsibility:
10
+ * 1. Reads mxy_consent cookie. If absent → render banner.
11
+ * 2. On accept → POST /v/consent {decision:"yes", v?:<token>}, server sets
12
+ * mxy_consent (12mo) + (if v= present in URL and HMAC-valid) mxy_v.
13
+ * 3. On reject → POST /v/consent {decision:"no"}, banner hides, never re-shows
14
+ * until manageCookies() is called or cookie expires.
15
+ * 4. After accept with ?consent=needed&v=… in URL, calls history.replaceState
16
+ * to strip both params so the token doesn't sit in browser history.
17
+ * 5. Dispatches `mxy:consent-changed` so v.js can react without reloading.
18
+ * 6. Exposes window.mxyManageCookies() to re-open the banner from a footer link.
19
+ * 7. On yes→no transition, purges every visitor-graph storage key the pixel
20
+ * could have written.
21
+ *
22
+ * No build step. Plain JS IIFE. ~2.5 KB.
23
+ */
24
+ (function () {
25
+ if (typeof window === 'undefined' || typeof document === 'undefined') return
26
+
27
+ var COOKIE_NAME = 'mxy_consent'
28
+ var CONSENT_ENDPOINT = '/v/consent'
29
+ var BRAND_CONFIG_ENDPOINT = '/v/brand-config'
30
+ // Storage keys the pixel may write. Mirror v.js literals.
31
+ var PIXEL_STORAGE_KEYS = [
32
+ 'mxy_visitor_id', // localStorage
33
+ 'mxy_session_id', // sessionStorage
34
+ 'mxy_session_pv',
35
+ 'mxy_last_pv',
36
+ 'mxy_scroll_depth',
37
+ 'mxy_dwell_start',
38
+ ]
39
+
40
+ var DEFAULT_COPY = {
41
+ title: 'Before you browse',
42
+ body: 'We use a small first-party script to count page views, scrolls and clicks on this listing. Nothing is shared with third parties. You can change your mind at any time.',
43
+ accept: 'Accept',
44
+ reject: 'Reject',
45
+ manage: 'Manage cookies',
46
+ policyLinkText: 'Read the cookie policy',
47
+ policyHref: '/privacy.html#cookies',
48
+ }
49
+ var DEFAULT_PALETTE = {
50
+ bg: '#FFFFFF',
51
+ fg: '#1F1F1F',
52
+ accent: '#1F1F1F',
53
+ accentFg: '#FFFFFF',
54
+ border: '#E5E5E2',
55
+ }
56
+
57
+ function readCookie(name) {
58
+ var raw = document.cookie || ''
59
+ var parts = raw.split(';')
60
+ for (var i = 0; i < parts.length; i++) {
61
+ var kv = parts[i].trim().split('=')
62
+ if (kv[0] === name) return decodeURIComponent(kv.slice(1).join('='))
63
+ }
64
+ return null
65
+ }
66
+
67
+ function readUrlToken() {
68
+ try {
69
+ var u = new URL(window.location.href)
70
+ if (u.searchParams.get('consent') !== 'needed') return null
71
+ var v = u.searchParams.get('v')
72
+ return v && v.length > 0 && v.length <= 2048 ? v : null
73
+ } catch (_e) {
74
+ return null
75
+ }
76
+ }
77
+
78
+ function stripConsentQuery() {
79
+ try {
80
+ var u = new URL(window.location.href)
81
+ if (!u.searchParams.has('consent') && !u.searchParams.has('v')) return
82
+ u.searchParams.delete('consent')
83
+ u.searchParams.delete('v')
84
+ var next = u.pathname + (u.searchParams.toString() ? '?' + u.searchParams.toString() : '') + u.hash
85
+ history.replaceState(null, '', next)
86
+ } catch (_e) { /* never break the page */ }
87
+ }
88
+
89
+ function purgePixelStorage() {
90
+ for (var i = 0; i < PIXEL_STORAGE_KEYS.length; i++) {
91
+ var k = PIXEL_STORAGE_KEYS[i]
92
+ try { localStorage.removeItem(k) } catch (_e) {}
93
+ try { sessionStorage.removeItem(k) } catch (_e) {}
94
+ }
95
+ }
96
+
97
+ function fireChange(decision) {
98
+ try {
99
+ window.dispatchEvent(new CustomEvent('mxy:consent-changed', { detail: { decision: decision } }))
100
+ } catch (_e) {
101
+ // IE/older Safari fallback — synthesise a plain Event.
102
+ try {
103
+ var ev = document.createEvent('Event')
104
+ ev.initEvent('mxy:consent-changed', true, true)
105
+ ev.detail = { decision: decision }
106
+ window.dispatchEvent(ev)
107
+ } catch (_e2) {}
108
+ }
109
+ }
110
+
111
+ function postConsent(decision, token) {
112
+ var body = JSON.stringify(token ? { decision: decision, v: token } : { decision: decision })
113
+ return fetch(CONSENT_ENDPOINT, {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: body,
117
+ credentials: 'include',
118
+ keepalive: true,
119
+ }).catch(function () { /* server records the decision; UI proceeds regardless */ })
120
+ }
121
+
122
+ function fetchBrandConfig() {
123
+ return fetch(BRAND_CONFIG_ENDPOINT, { credentials: 'omit' })
124
+ .then(function (r) { return r.ok ? r.json() : null })
125
+ .catch(function () { return null })
126
+ }
127
+
128
+ function applyPalette(palette) {
129
+ var root = document.documentElement
130
+ for (var k in palette) {
131
+ if (Object.prototype.hasOwnProperty.call(palette, k)) {
132
+ root.style.setProperty('--mxy-consent-' + k, palette[k])
133
+ }
134
+ }
135
+ }
136
+
137
+ var bannerEl = null
138
+
139
+ function buildBanner(copy) {
140
+ var root = document.createElement('div')
141
+ root.id = 'mxy-consent'
142
+ root.setAttribute('role', 'dialog')
143
+ root.setAttribute('aria-live', 'polite')
144
+ root.setAttribute('aria-label', copy.title)
145
+
146
+ var inner = document.createElement('div')
147
+ inner.className = 'mxy-consent__inner'
148
+
149
+ var text = document.createElement('div')
150
+ text.className = 'mxy-consent__text'
151
+ var h = document.createElement('strong')
152
+ h.className = 'mxy-consent__title'
153
+ h.textContent = copy.title
154
+ var p = document.createElement('p')
155
+ p.className = 'mxy-consent__body'
156
+ p.textContent = copy.body
157
+ var link = document.createElement('a')
158
+ link.className = 'mxy-consent__link'
159
+ link.href = copy.policyHref
160
+ link.textContent = copy.policyLinkText
161
+ link.target = '_blank'
162
+ link.rel = 'noopener'
163
+ text.appendChild(h)
164
+ text.appendChild(p)
165
+ text.appendChild(link)
166
+
167
+ var actions = document.createElement('div')
168
+ actions.className = 'mxy-consent__actions'
169
+ var reject = document.createElement('button')
170
+ reject.className = 'mxy-consent__btn mxy-consent__btn--reject'
171
+ reject.type = 'button'
172
+ reject.textContent = copy.reject
173
+ var accept = document.createElement('button')
174
+ accept.className = 'mxy-consent__btn mxy-consent__btn--accept'
175
+ accept.type = 'button'
176
+ accept.textContent = copy.accept
177
+ // Order: reject first then accept; CSS equal-weights them. ICO requires
178
+ // the reject path to be no harder than the accept path.
179
+ actions.appendChild(reject)
180
+ actions.appendChild(accept)
181
+
182
+ inner.appendChild(text)
183
+ inner.appendChild(actions)
184
+ root.appendChild(inner)
185
+
186
+ accept.addEventListener('click', function () { onDecide('yes') })
187
+ reject.addEventListener('click', function () { onDecide('no') })
188
+
189
+ return root
190
+ }
191
+
192
+ function hideBanner() {
193
+ if (bannerEl && bannerEl.parentNode) {
194
+ bannerEl.parentNode.removeChild(bannerEl)
195
+ }
196
+ bannerEl = null
197
+ }
198
+
199
+ function onDecide(decision) {
200
+ var token = decision === 'yes' ? readUrlToken() : null
201
+ // Optimistic local cookie write so v.js sees consent immediately. The
202
+ // server POST below will refresh the lifetime to the full 12 months;
203
+ // this JS write is ITP-clamped to 7 days on Safari but covers the gap
204
+ // until the server response lands.
205
+ document.cookie = COOKIE_NAME + '=' + decision + '; Max-Age=' + (60 * 60 * 24 * 365) + '; Path=/; SameSite=Lax; Secure'
206
+ if (decision === 'no') purgePixelStorage()
207
+ if (decision === 'yes') stripConsentQuery()
208
+ hideBanner()
209
+
210
+ var p = postConsent(decision, token)
211
+ if (decision === 'yes' && token) {
212
+ // Wait for the server to set HttpOnly mxy_v before letting v.js fire
213
+ // the first pageview. Otherwise the first event is attributed to
214
+ // AnonVisitor and the cross-session merge runs one event later.
215
+ p.then(function () { fireChange(decision) })
216
+ } else {
217
+ // No token to bind → no reason to wait; emit immediately.
218
+ fireChange(decision)
219
+ }
220
+ }
221
+
222
+ function maybeShowBanner() {
223
+ var current = readCookie(COOKIE_NAME)
224
+ if (current === 'yes' || current === 'no') return
225
+ fetchBrandConfig().then(function (cfg) {
226
+ var consent = (cfg && cfg.consent) || {}
227
+ var copy = {}
228
+ for (var k in DEFAULT_COPY) copy[k] = (consent.copy && consent.copy[k]) || DEFAULT_COPY[k]
229
+ var palette = {}
230
+ for (var p in DEFAULT_PALETTE) palette[p] = (consent.palette && consent.palette[p]) || DEFAULT_PALETTE[p]
231
+ applyPalette(palette)
232
+ bannerEl = buildBanner(copy)
233
+ document.body.appendChild(bannerEl)
234
+ })
235
+ }
236
+
237
+ // Withdrawal entry point — footer links call this to re-open the banner
238
+ // when the visitor wants to change their mind. Purge happens in onDecide
239
+ // when the visitor actually flips to reject, not here.
240
+ window.mxyManageCookies = function () {
241
+ if (bannerEl) hideBanner()
242
+ fetchBrandConfig().then(function (cfg) {
243
+ var consent = (cfg && cfg.consent) || {}
244
+ var copy = {}
245
+ for (var k in DEFAULT_COPY) copy[k] = (consent.copy && consent.copy[k]) || DEFAULT_COPY[k]
246
+ var palette = {}
247
+ for (var p in DEFAULT_PALETTE) palette[p] = (consent.palette && consent.palette[p]) || DEFAULT_PALETTE[p]
248
+ applyPalette(palette)
249
+ bannerEl = buildBanner(copy)
250
+ document.body.appendChild(bannerEl)
251
+ })
252
+ }
253
+
254
+ if (document.readyState === 'loading') {
255
+ document.addEventListener('DOMContentLoaded', maybeShowBanner)
256
+ } else {
257
+ maybeShowBanner()
258
+ }
259
+ })()
@@ -5,11 +5,11 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Data — Maxy</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/data-BGbQyufe.js"></script>
8
+ <script type="module" crossorigin src="/assets/data-Ds37mflX.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-DD-I1_y5.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/ChatInput-CJo_77bp.js">
11
- <link rel="modulepreload" crossorigin href="/assets/graph-labels-BRXJHNYE.js">
12
- <link rel="modulepreload" crossorigin href="/assets/page-B4oirCvn.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/graph-labels-Ch2r00Gt.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/page-BcHhJXUt.js">
13
13
  <link rel="stylesheet" crossorigin href="/assets/ChatInput-CJ50oqWt.css">
14
14
  </head>
15
15
  <body>
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Graph — Maxy</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/graph-D-1lRTeL.js"></script>
8
+ <script type="module" crossorigin src="/assets/graph-CmWRhaiS.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-DD-I1_y5.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/ChatInput-CJo_77bp.js">
11
- <link rel="modulepreload" crossorigin href="/assets/graph-labels-BRXJHNYE.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/graph-labels-Ch2r00Gt.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/Checkbox-YrQovXpN.js">
13
- <link rel="modulepreload" crossorigin href="/assets/page-FmJ7PIYx.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/page-BOtNny_4.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/ChatInput-CJ50oqWt.css">
15
15
  </head>
16
16
  <body>
@@ -5,14 +5,14 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Maxy</title>
7
7
  <link rel="icon" href="/favicon.ico">
8
- <script type="module" crossorigin src="/assets/admin-D6IfAzYY.js"></script>
8
+ <script type="module" crossorigin src="/assets/admin-FcRHAL-3.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/chunk-DD-I1_y5.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/ChatInput-CJo_77bp.js">
11
- <link rel="modulepreload" crossorigin href="/assets/graph-labels-BRXJHNYE.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/graph-labels-Ch2r00Gt.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/lib-Bnh57com.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/Checkbox-YrQovXpN.js">
14
- <link rel="modulepreload" crossorigin href="/assets/page-B4oirCvn.js">
15
- <link rel="modulepreload" crossorigin href="/assets/page-FmJ7PIYx.js">
14
+ <link rel="modulepreload" crossorigin href="/assets/page-BcHhJXUt.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/page-BOtNny_4.js">
16
16
  <link rel="stylesheet" crossorigin href="/assets/ChatInput-CJ50oqWt.css">
17
17
  <link rel="stylesheet" crossorigin href="/assets/admin-CWMpccrR.css">
18
18
  <link rel="stylesheet" href="/brand-defaults.css">
@@ -41,11 +41,74 @@
41
41
  </table>
42
42
  <p class="muted">We do not collect: cursor positions, keystrokes, form contents, screen recordings, or anything from third-party tracking networks. The pixel is loaded from the same site as the listing, never from a third party.</p>
43
43
 
44
- <h2>Cookies</h2>
45
- <p>One cookie is set when you arrive via a personalised marketing link from the agent:</p>
46
- <ul>
47
- <li><code>mxy_v</code> — signed token containing your contact identifier. Set <code>HttpOnly</code>, <code>SameSite=Lax</code>, <code>Secure</code>. Lives for 30 days. Lets the site recognise you on later visits without you clicking the marketing link again.</li>
48
- </ul>
44
+ <h2 id="cookies">Cookies and storage</h2>
45
+ <p>Before any of the storage below is used, you see a consent banner and choose to accept or reject. If you reject, nothing in the second table is ever written. The only cookie that is set without consent is the one that remembers your consent decision &mdash; this is permitted under PECR Regulation 6(4) as strictly necessary to deliver the consent service you asked for.</p>
46
+
47
+ <p><strong>Set even when you reject (strictly necessary):</strong></p>
48
+ <table>
49
+ <thead><tr><th>Key</th><th>Type</th><th>Purpose</th><th>Lifetime</th></tr></thead>
50
+ <tbody>
51
+ <tr>
52
+ <td><code>mxy_consent</code></td>
53
+ <td>Cookie (<code>SameSite=Lax</code>, <code>Secure</code>)</td>
54
+ <td>Remembers whether you accepted or rejected, so the banner doesn't re-show on every page.</td>
55
+ <td>12 months</td>
56
+ </tr>
57
+ </tbody>
58
+ </table>
59
+
60
+ <p><strong>Set only after you accept:</strong></p>
61
+ <table>
62
+ <thead><tr><th>Key</th><th>Type</th><th>Purpose</th><th>Lifetime</th></tr></thead>
63
+ <tbody>
64
+ <tr>
65
+ <td><code>mxy_v</code></td>
66
+ <td>Cookie (<code>HttpOnly</code>, <code>SameSite=Lax</code>, <code>Secure</code>)</td>
67
+ <td>Signed token containing your contact identifier &mdash; only set if you arrived via a personalised marketing link. Lets the site recognise you on later visits without you clicking the link again.</td>
68
+ <td>30 days</td>
69
+ </tr>
70
+ <tr>
71
+ <td><code>mxy_visitor_id</code></td>
72
+ <td>localStorage</td>
73
+ <td>Random ID so we can tell two visits from one browser apart from two browsers.</td>
74
+ <td>Until you clear browser storage</td>
75
+ </tr>
76
+ <tr>
77
+ <td><code>mxy_session_id</code></td>
78
+ <td>sessionStorage</td>
79
+ <td>Random ID so one tab's activity stays grouped together.</td>
80
+ <td>Until you close the tab</td>
81
+ </tr>
82
+ <tr>
83
+ <td><code>mxy_session_pv</code></td>
84
+ <td>sessionStorage</td>
85
+ <td>Page-view identifier used to roll scroll milestones up into a single record per page.</td>
86
+ <td>Until you close the tab</td>
87
+ </tr>
88
+ <tr>
89
+ <td><code>mxy_last_pv</code></td>
90
+ <td>sessionStorage</td>
91
+ <td>Deduplicates a single page view across very fast client-side navigations (within 5 seconds).</td>
92
+ <td>Until you close the tab</td>
93
+ </tr>
94
+ <tr>
95
+ <td><code>mxy_scroll_depth</code></td>
96
+ <td>sessionStorage</td>
97
+ <td>Highest scroll milestone already recorded for the current page, so we only send each one once.</td>
98
+ <td>Until you close the tab</td>
99
+ </tr>
100
+ <tr>
101
+ <td><code>mxy_dwell_start</code></td>
102
+ <td>sessionStorage</td>
103
+ <td>Time you arrived on the page, used to compute dwell when you navigate away.</td>
104
+ <td>Until you close the tab</td>
105
+ </tr>
106
+ </tbody>
107
+ </table>
108
+
109
+ <h2>Withdrawing consent</h2>
110
+ <p>To change your mind after accepting or rejecting, click <strong>Manage cookies</strong> in the page footer. The banner re-opens and you can switch your choice. If you flip from accept to reject, every key in the second table is cleared from your browser immediately. The personalised cookie (<code>mxy_v</code>) is cleared by your browser when it next sees a response that overrides it; if you want it removed sooner, clear cookies for this site in your browser settings.</p>
111
+ <p>Withdrawing consent stops new visit data from being written. It does not delete visit data already on our servers from your previous accepted sessions. For that, see &ldquo;Right to access and erasure&rdquo; below.</p>
49
112
 
50
113
  <h2>Cross-session attribution</h2>
51
114
  <p>If you visit anonymously today and later click a personalised marketing link from the agent, prior anonymous visits from the same browser are linked to your contact record. This is the only way an anonymous visit ever becomes named.</p>
@@ -25,6 +25,7 @@
25
25
  var siteSlug = script.getAttribute('data-site') || ''
26
26
  var endpoint = new URL('/v/event', script.src).toString()
27
27
 
28
+ var CONSENT_KEY = 'mxy_consent'
28
29
  var VISITOR_KEY = 'mxy_visitor_id'
29
30
  var SESSION_KEY = 'mxy_session_id'
30
31
  var SESSION_PV_KEY = 'mxy_session_pv'
@@ -32,6 +33,55 @@
32
33
  var SCROLL_KEY = 'mxy_scroll_depth'
33
34
  var DWELL_KEY = 'mxy_dwell_start'
34
35
 
36
+ // ─────────── PECR consent gate (Task 375) ───────────
37
+ // Reads the mxy_consent cookie set by /consent.js (and persisted server-side
38
+ // via POST /v/consent). When the visitor has not accepted, the pixel is a
39
+ // total no-op: no storage writes, no /v/event POSTs, no event listeners
40
+ // attached. A later flip yes ← rebooted via the mxy:consent-changed event.
41
+ function readConsentCookie() {
42
+ var raw = (typeof document !== 'undefined' && document.cookie) || ''
43
+ var parts = raw.split(';')
44
+ for (var i = 0; i < parts.length; i++) {
45
+ var kv = parts[i].trim().split('=')
46
+ if (kv[0] === CONSENT_KEY) return decodeURIComponent(kv.slice(1).join('='))
47
+ }
48
+ return null
49
+ }
50
+ function purgeAll() {
51
+ var keys = [VISITOR_KEY, SESSION_KEY, SESSION_PV_KEY, LAST_PV_KEY, SCROLL_KEY, DWELL_KEY]
52
+ for (var i = 0; i < keys.length; i++) {
53
+ try { localStorage.removeItem(keys[i]) } catch (_e) {}
54
+ try { sessionStorage.removeItem(keys[i]) } catch (_e) {}
55
+ }
56
+ }
57
+ var started = false
58
+ function start() {
59
+ if (started) return
60
+ started = true
61
+ boot()
62
+ }
63
+ // React to consent changes from the banner: yes→start (emit deferred
64
+ // pageview), yes→no→purge storage and detach listeners by setting a flag
65
+ // that all emit-paths check before sending.
66
+ var revoked = false
67
+ window.addEventListener('mxy:consent-changed', function (ev) {
68
+ var decision = (ev && ev.detail && ev.detail.decision) || readConsentCookie()
69
+ if (decision === 'yes') {
70
+ revoked = false
71
+ start()
72
+ } else if (decision === 'no') {
73
+ revoked = true
74
+ purgeAll()
75
+ }
76
+ })
77
+ if (readConsentCookie() !== 'yes') {
78
+ // Banner / server will fire mxy:consent-changed when the visitor accepts.
79
+ return
80
+ }
81
+ start()
82
+
83
+ function boot() {
84
+
35
85
  function randomId() {
36
86
  if (window.crypto && window.crypto.getRandomValues) {
37
87
  var buf = new Uint8Array(16)
@@ -71,6 +121,7 @@
71
121
  var sessionId = getSessionId()
72
122
 
73
123
  function send(payload) {
124
+ if (revoked) return
74
125
  payload.site = siteSlug
75
126
  payload.vid = visitorId
76
127
  payload.sid = sessionId
@@ -98,6 +149,7 @@
98
149
 
99
150
  // ─────────── pageview (idempotent within 5s for SPA nav) ───────────
100
151
  function emitPageview() {
152
+ if (revoked) return
101
153
  try {
102
154
  var last = sessionStorage.getItem(LAST_PV_KEY)
103
155
  if (last) {
@@ -188,4 +240,5 @@
188
240
 
189
241
  // Boot.
190
242
  emitPageview()
243
+ } // end boot()
191
244
  })()