[{"data":1,"prerenderedAt":608},["ShallowReactive",2],{"site-settings":3,"project-sonic-artistes-agency-app":76,"project-logs-sonic-artistes-agency-app":196,"project-articles-sonic-artistes-agency-app":607},{"global":4,"header":11,"footer":27},{"brandName":5,"brandStamp":6,"brandTagline":7,"communityUrl":8,"defaultSeoDescription":9,"defaultOgImage":10},"Useful Labs","est. 2025 · UK","Serious software, built out loud.","https:\u002F\u002Fcommunity.usefullabs.io","A product-led lab building production-grade platforms in the open — Sonic Artistes, Castora and more. A few times a year we take that same craft to a client build. Real software, shipped at product-team pace, shared honestly.",null,{"navLinks":12,"scarcityPulse":10,"showClock":25,"ctaLabel":26,"ctaUrl":8},[13,17,20,23],{"label":14,"url":15,"external":16},"Projects","\u002Fprojects",false,{"label":18,"url":19,"external":16},"Writing","\u002Fblog",{"label":21,"url":22,"external":16},"Tools","\u002Ftools",{"label":24,"url":8,"external":25},"The Hub",true,"Join the community",{"tagline":7,"aboutParagraph":28,"copyrightLine":29,"columns":30,"legalLinks":63},"A UK product lab, built in the open. Real production software, documented honestly — no hype, no highlights reel.","© Useful Labs {year} · Made in the UK with unusual patience.",[31,41,50],{"heading":14,"links":32},[33,36,39],{"label":34,"url":35,"external":25},"Sonic Artistes","https:\u002F\u002Fapp.sonicartistes.com",{"label":37,"url":38,"external":25},"Castora","https:\u002F\u002Fgetcastora.com",{"label":40,"url":15,"external":16},"Archive",{"heading":18,"links":42},[43,44,47],{"label":18,"url":19,"external":16},{"label":45,"url":46,"external":16},"RSS feed","\u002Frss.xml",{"label":48,"url":49,"external":16},"Newsletter","#newsletter",{"heading":51,"links":52},"Elsewhere",[53,54,57,60],{"label":24,"url":8,"external":25},{"label":55,"url":56,"external":25},"GitHub","https:\u002F\u002Fgithub.com\u002Fpaulwilliams-us",{"label":58,"url":59,"external":25},"X \u002F Twitter","https:\u002F\u002Fx.com",{"label":61,"url":62,"external":16},"Email","mailto:hello@usefullabs.io",[64,67,70,73],{"label":65,"url":66,"external":16},"Privacy","\u002Fprivacy",{"label":68,"url":69,"external":16},"Terms","\u002Fterms",{"label":71,"url":72,"external":16},"AI Policy","\u002Fai-policy",{"label":74,"url":75,"external":16},"Colophon","\u002Fcolophon",{"id":77,"slug":77,"name":78,"idLabel":79,"tagText":80,"description":81,"accent":82,"meta":85,"modules":95,"chips":111,"progress":117,"followLabel":120,"followHref":121,"siteUrl":35,"siteLabel":122,"status":123,"isFeatured":25,"isHeroFeatured":16,"publishedIso":124,"modifiedIso":125,"hero":-1,"contentHtml":126,"featuredImage":10,"screenshots":127},"sonic-artistes-agency-app","Sonic Artistes Agency App","01 \u002F SA","Live in production","Full-stack musician management platform for cruise line entertainment. Document compliance, scheduling, chat, repertoire management, support tickets and multi-sided portals — all in one.",{"token":83,"rgb":84},"accent","6, 182, 212",[86,89,92],{"value":87,"label":88},"15+","Modules",{"value":90,"label":91},"400+","Musicians",{"value":93,"label":94},"3","Portals",[96,97,98,99,100,101,102,103,104,105,106,107,108,109,110],"Multi-sided portals","Document compliance","Contracts & scheduling","Workflow automation","Realtime chat","Community Spaces","Repertoire management","Notifications engine","Email & templates","Support tickets","HR touchpoints","Analytics & reporting","DSAR \u002F GDPR module","PDF generation","The Hub integration",[112,113,114,115,116],"Nuxt 3","Supabase","TypeScript","shadcn-vue","Claude Code",{"label":118,"percent":119},"Build progress",100,"Follow the build","\u002Fprojects\u002Fsonic-artistes-agency-app","app.sonicartistes.com","live","2026-04-23T10:49:28","2026-04-23T11:43:20","\u003Cp>The Sonic Artistes Agency App is bespoke software that runs an entire cruise-line entertainment agency. It manages 400+ musicians, every contract and ship rotation, document compliance across multiple cruise operators, and three distinct portals &mdash; one each for the agency's staff, the performers, and the cruise-line clients they supply. It has been live in production for six months.\u003C\u002Fp>\n\u003Cp>This is the kind of system most businesses are quoted a year and six figures to build. We built it as a small team working hand-in-hand with AI, and it is not a prototype or a thin MVP. It is a hardened, multi-tenant platform with row-level security, realtime everything, a 51-type notification engine, and a full GDPR data-request module that most SaaS companies never get round to writing. We are showing it here because it is the honest ceiling of what we build &mdash; and it is already running every day.\u003C\u002Fp>\n\u003Ch3>Three portals, one source of truth\u003C\u002Fh3>\n\u003Cp>Staff, musicians and clients each get their own purpose-built portal, governed by a four-role access model enforced at the database layer. The agency's schedulers run the whole operation from the admin side; musicians get a clean self-service space for their assignments, documents and check-ins; and cruise-line clients see only the performers shared with them, down to individual document level. Nobody sees a row they shouldn't &mdash; and that boundary is enforced in the database, not just the interface.\u003C\u002Fp>\n\u003Ch3>Contracts, scheduling and compliance\u003C\u002Fh3>\n\u003Cp>The assignment builder lets schedulers create and manage contracts with smart musician recommendations based on role and availability, automated status transitions, offer letters and extensions, and a bespoke rotation pipeline for the cruise operator's approval process. Document compliance is fully dynamic: document types are configured in-app with their own forms, expiry rules and verification workflows, grouped into compliance phases, with branded PDFs generated automatically on completion. Expiry reminders chase the right people at the right time and stop the moment a document is renewed.\u003C\u002Fp>\n\u003Ch3>Communication and community\u003C\u002Fh3>\n\u003Cp>A complete realtime messaging suite covers direct messages, group chats and broadcasts, with read receipts, reactions, reply-to quoting, @mentions, pinning and a shared inbox for the support team. Gated community Spaces give cohorts of musicians their own rooms for posts, comments and shared resources, with seen-by tracking throughout. A separate integration wires the platform into The Hub, the agency's wider musician community.\u003C\u002Fp>\n\u003Ch3>The engines underneath\u003C\u002Fh3>\n\u003Cp>Beneath the features sit the systems that make a platform feel finished. A 51-type notification engine with per-role email, digest and push delivery, every channel admin-configurable without touching code. A database-driven email system with merge-tag templates and a full audit log. Configurable workflow checklists with step dependencies and automations. An analytics module with fourteen reporting views, six dashboards and a saved-query builder that exports to PDF and CSV. Support tickets, RAG-rated HR touchpoints across the recruitment lifecycle, repertoire management with per-song proficiency, bookmarks, announcements &mdash; the breadth a real operations tool needs and a demo never has.\u003C\u002Fp>\n\u003Ch3>Built to last\u003C\u002Fh3>\n\u003Cp>The whole thing runs on self-hosted Supabase, strictly typed end to end, with a disciplined security model and a single consistent design system across every portal. It is the sort of platform you would normally hand to an agency, write a &pound;100k cheque for, and wait the best part of a year to see. We would rather just show you it working &mdash; and talk about what we could build for you next.\u003C\u002Fp>",[128,136,142,149,155,161,167,175,182,190],{"id":129,"image":130,"captionHtml":135},"f8cab1be-9e1f-4745-bbb3-b7055d6761b8",{"src":131,"alt":132,"width":133,"height":134},"https:\u002F\u002Foffice.usefullabs.io\u002Fassets\u002F69315bde-a605-494c-809f-cd03c43f30e8","Sonic Artistes App 01 Login Screen",1908,1358,"\u003Cp>Login Screen\u003C\u002Fp>",{"id":137,"image":138,"captionHtml":141},"e5a0e509-9a11-477b-b6c3-a7e3369826ff",{"src":139,"alt":140,"width":133,"height":134},"https:\u002F\u002Foffice.usefullabs.io\u002Fassets\u002F64296933-03eb-400d-a697-4e2fe382ea96","Sonic Artistes App 02 Client Portal Dashboard","\u003Cp>Dashboard for Client Portal. Clients are granted access to only the musicians they need. Expiry can be set or toggled manually by admin and manager.\u003C\u002Fp>",{"id":143,"image":144,"captionHtml":148},"70b4b66a-1542-4a19-9ef0-069f9ef0bac3",{"src":145,"alt":146,"width":147,"height":134},"https:\u002F\u002Foffice.usefullabs.io\u002Fassets\u002F06b3a1e7-3525-49a9-ae58-3ea649fd2b32","Sonic Artistes App 04 Client Portal   Single Musician View",1974,"\u003Cp>Client single musician view. Document status and expiry (if appropriate) show against each document. Clients can download all documents at once, preview or download individual document types.\u003C\u002Fp>",{"id":150,"image":151,"captionHtml":154},"6ea37642-90a3-4491-a588-b12d1ba02ff2",{"src":152,"alt":153,"width":147,"height":134},"https:\u002F\u002Foffice.usefullabs.io\u002Fassets\u002F2c29b2a4-6071-4065-90e7-5ec7682f39e0","Sonic Artistes App 05 Client Portal   Single Musician View With Multi Docs","\u003Cp>Client document view - where multiple uploads per document exist, Clients can preview\u002Finspect or each upload individually or as a complete document bundle.\u003C\u002Fp>",{"id":156,"image":157,"captionHtml":160},"731f8f3b-4bed-4f9c-8ddf-19756132e919",{"src":158,"alt":159,"width":133,"height":134},"https:\u002F\u002Foffice.usefullabs.io\u002Fassets\u002F63cedfd5-4964-4815-86c8-dd45be0fb9fb","Sonic Artistes App 05 Musician Dashboard","\u003Cp>Musician Dashboard: overview of assignments, document compliance, \"spaces\" (gated groups), workflows, notifications. Musicians are directed to accept assignments, complete contract check-ins when they are due.\u003C\u002Fp>",{"id":162,"image":163,"captionHtml":166},"c9a8240e-f6ba-44d7-861b-ec0c29b4f6b2",{"src":164,"alt":165,"width":133,"height":134},"https:\u002F\u002Foffice.usefullabs.io\u002Fassets\u002F71505616-f11d-4d9f-9a05-97fde889ab2d","Sonic Artistes App 06 Musician Documents","\u003Cp>\u003Cstrong>Musician Document Compliance Overview:\u003C\u002Fstrong> Musicians can see the current documents we hold on file for them including expiry dates if the document requires them. Their progress towards compliance is shown overall and per \"phase\".\u003C\u002Fp>",{"id":168,"image":169,"captionHtml":174},"1c43b6b4-d65d-4e90-873b-fb24df5c1b4c",{"src":170,"alt":171,"width":172,"height":173},"https:\u002F\u002Foffice.usefullabs.io\u002Fassets\u002F1045375a-710f-4711-898f-ee8dd944579e","Sonic Artistes App 07 Admin   Assignment Builder",1907,1758,"\u003Cp>Admin Portal: The assignment builder allows schedulers to create and manage contracts. Core details are tracked for completeness. The builder also provides&nbsp;\u003Cem>\u003Cstrong>smart recommendations\u003C\u002Fstrong>\u003C\u002Fem> of musicians based on their \u003Cstrong>role type and availability.&nbsp;\u003C\u002Fstrong>\u003C\u002Fp>",{"id":176,"image":177,"captionHtml":181},"09fbbba3-118a-4072-aaf1-94c4d873eaeb",{"src":178,"alt":179,"width":147,"height":180},"https:\u002F\u002Foffice.usefullabs.io\u002Fassets\u002Ffaa19294-f8bc-4279-b479-5bf1d2228e59","Sonic Artistes App 08 Admin   Musician Profile",1349,"\u003Cp>Admin Portal: Musician Profile\u003C\u002Fp>",{"id":183,"image":184,"captionHtml":189},"e1d5f98a-8f91-4ded-aa97-9a347dcf2cf9",{"src":185,"alt":186,"width":187,"height":188},"https:\u002F\u002Foffice.usefullabs.io\u002Fassets\u002F79665694-6212-4b3d-badf-3430e75480f8","Sonic Artistes App Admin 09   Profile Touchpoints",1906,1353,"\u003Cp>Admin Portal: HR touchpoints that are managing in the app setting. RAG rated and for amber\u002Fred points, comments are required.\u003C\u002Fp>",{"id":191,"image":192,"captionHtml":195},"67b44848-58ca-4c76-a7b5-31aeac28e0dc",{"src":193,"alt":194,"width":187,"height":188},"https:\u002F\u002Foffice.usefullabs.io\u002Fassets\u002F46acd66a-f5dc-44aa-a1e0-7803fc064033","Sonic Artistes App 10 Profile Documents Tab","\u003Cp>Admin Portal: Admins and Managers can upload documents, add\u002Fedit data and assign new documents. Documents are dynamic in the app can be created if new documents are necessary. They can be required and added to either the Core or Additional phases.\u003C\u002Fp>",[197,205,210,217,222,227,232,237,242,247,253,259,264,269,274,279,284,289,295,300,306,311,316,321,326,331,336,342,347,353,358,363,369,374,379,384,389,394,399,404,409,414,419,424,429,435,440,445,450,455,460,466,472,478,484,491,497,503,509,516,522,528,535,541,547,553,559,564,570,576,582,588,595,601],{"id":198,"slug":199,"projectId":77,"kind":200,"title":201,"contentHtml":202,"dateLabel":203,"publishedIso":204,"commitUrl":-1,"image":-1},"a57167bf-0daa-4675-9657-f6b7b78014a5","saa-526-assignment-column-link","shipped","The Assignment column on the contracts tab is now a link straight to the assignment","","1d ago","2026-06-25T11:00:00",{"id":206,"slug":207,"projectId":77,"kind":200,"title":208,"contentHtml":202,"dateLabel":203,"publishedIso":209,"commitUrl":-1,"image":-1},"9ce1fd5e-e572-49f3-acd9-1f867ee752cc","saa-524-self-hosted-audio-all-editors","Self-hosted audio upload now works in every editor","2026-06-25T09:00:00",{"id":211,"slug":212,"projectId":77,"kind":213,"title":214,"contentHtml":202,"dateLabel":215,"publishedIso":216,"commitUrl":-1,"image":-1},"265879a1-bbc6-4cf6-8bfc-ab8516b13cf5","saa-522-kb-audio-upload","fixed","Audio upload switched on for the Knowledge Base editor","2d ago","2026-06-24T17:00:00",{"id":218,"slug":219,"projectId":77,"kind":200,"title":220,"contentHtml":202,"dateLabel":215,"publishedIso":221,"commitUrl":-1,"image":-1},"354efe61-3100-4d52-8057-26a56b16a5cd","saa-520-post-third-party-embeds","Space posts can now embed third-party media","2026-06-24T16:30:00",{"id":223,"slug":224,"projectId":77,"kind":200,"title":225,"contentHtml":202,"dateLabel":215,"publishedIso":226,"commitUrl":-1,"image":-1},"c51555b8-869b-4fd6-9798-0f96c139ef8a","saa-518-post-rich-content","Space posts get the rich-content treatment: colour, highlight and callout cards","2026-06-24T16:00:00",{"id":228,"slug":229,"projectId":77,"kind":200,"title":230,"contentHtml":202,"dateLabel":215,"publishedIso":231,"commitUrl":-1,"image":-1},"e9ed9ac4-ec7c-4445-b93f-7594c3eeae6d","saa-516-contract-check-ins","The dashboard's stale attention card is now a live Contract Check-ins feed","2026-06-24T15:30:00",{"id":233,"slug":234,"projectId":77,"kind":213,"title":235,"contentHtml":202,"dateLabel":215,"publishedIso":236,"commitUrl":-1,"image":-1},"8c8f0f47-4670-4062-a321-6fdc0dc100e8","saa-514-repertoire-mobile-clipping","Repertoire tables stop clipping rows on mobile portal pages","2026-06-24T15:00:00",{"id":238,"slug":239,"projectId":77,"kind":200,"title":240,"contentHtml":202,"dateLabel":215,"publishedIso":241,"commitUrl":-1,"image":-1},"6c985eeb-d97e-47c1-9b62-f2f3f1e10b2f","saa-512-notes-rich-editor","Notes get the rich-content editor and a visible comment toolbar","2026-06-24T14:30:00",{"id":243,"slug":244,"projectId":77,"kind":200,"title":245,"contentHtml":202,"dateLabel":215,"publishedIso":246,"commitUrl":-1,"image":-1},"77549ba7-edb0-44a5-a1d6-906ea372e94b","saa-510-mona-sans","The whole UI moves to self-hosted Mona Sans","2026-06-24T14:00:00",{"id":248,"slug":249,"projectId":77,"kind":250,"title":251,"contentHtml":202,"dateLabel":215,"publishedIso":252,"commitUrl":-1,"image":-1},"15222c02-acd3-45db-aec2-366a848c4728","saa-508-hide-broken-audio-item","noted","Pulled the broken 'Upload audio' menu item while we tracked down the cause","2026-06-24T13:30:00",{"id":254,"slug":255,"projectId":77,"kind":213,"title":256,"contentHtml":202,"dateLabel":257,"publishedIso":258,"commitUrl":-1,"image":-1},"58b242e3-97d4-46e1-a9a5-14182420d572","saa-506-audio-upload-chokepoint","Audio uploads now flow through one shared chokepoint, so every entry point behaves the same","3d ago","2026-06-23T16:00:00",{"id":260,"slug":261,"projectId":77,"kind":213,"title":262,"contentHtml":202,"dateLabel":257,"publishedIso":263,"commitUrl":-1,"image":-1},"485092da-c2eb-4475-be8b-6afa4f05e4b5","saa-503-mp3-upload-letterbox","MP3s upload cleanly into the audio node and embeds stop letterboxing","2026-06-23T15:30:00",{"id":265,"slug":266,"projectId":77,"kind":200,"title":267,"contentHtml":202,"dateLabel":257,"publishedIso":268,"commitUrl":-1,"image":-1},"a40facb8-a112-4283-a433-895a2c45bd2c","saa-501-rich-content-media-embeds","Rich-content gains media embeds and self-hosted audio","2026-06-23T15:00:00",{"id":270,"slug":271,"projectId":77,"kind":200,"title":272,"contentHtml":202,"dateLabel":257,"publishedIso":273,"commitUrl":-1,"image":-1},"0c99ca6d-842b-4f78-975b-92a20b61a298","saa-499-inline-image-presets","Inline images get loading skeletons and size\u002Falignment presets","2026-06-23T14:30:00",{"id":275,"slug":276,"projectId":77,"kind":200,"title":277,"contentHtml":202,"dateLabel":257,"publishedIso":278,"commitUrl":-1,"image":-1},"a4befd32-e46a-4c23-8e52-d87111d3cde7","saa-498-editor-polish","Editor polish: an autosave timestamp, sticky toolbar, reader-to-edit jump and balanced search","2026-06-23T14:00:00",{"id":280,"slug":281,"projectId":77,"kind":200,"title":282,"contentHtml":202,"dateLabel":257,"publishedIso":283,"commitUrl":-1,"image":-1},"6235b01d-34a2-4d51-a5b5-86a3794c584d","saa-496-rich-text-editor-basics","The rich-text editor gains headings, alignment, callouts and checkboxes","2026-06-23T13:30:00",{"id":285,"slug":286,"projectId":77,"kind":200,"title":287,"contentHtml":202,"dateLabel":257,"publishedIso":288,"commitUrl":-1,"image":-1},"63210bd3-43e9-4daa-9c46-b1afd746547f","saa-494-spaces-audience-membership","Spaces membership now follows the audience — all active musicians — with guardrails on announcement emails","2026-06-23T13:00:00",{"id":290,"slug":291,"projectId":77,"kind":200,"title":292,"contentHtml":202,"dateLabel":293,"publishedIso":294,"commitUrl":-1,"image":-1},"2ff695fb-b659-445e-963b-63cb62369d22","saa-493-notifications-card-unread","The portal notifications card shows only unread items and disappears when there are none","4d ago","2026-06-22T17:00:00",{"id":296,"slug":297,"projectId":77,"kind":200,"title":298,"contentHtml":202,"dateLabel":293,"publishedIso":299,"commitUrl":-1,"image":-1},"999b6ff5-6e50-4b63-8fea-18a7e1c45ee6","saa-491-my-dashboard-refresh","The musician's dashboard gets a refresh: Knowledge Base surfaced, cards reordered, a More menu","2026-06-22T16:30:00",{"id":301,"slug":302,"projectId":77,"kind":303,"title":304,"contentHtml":202,"dateLabel":293,"publishedIso":305,"commitUrl":-1,"image":-1},"7542347d-39eb-4e50-a6f0-835b7a3d4806","saa-490-global-rail-ia","refactored","Tidied the global rail into Operations, Workspaces and a pinned Settings","2026-06-22T16:00:00",{"id":307,"slug":308,"projectId":77,"kind":200,"title":309,"contentHtml":202,"dateLabel":293,"publishedIso":310,"commitUrl":-1,"image":-1},"00728954-7fc3-4be2-ad33-3d5f5b9cd05c","saa-487-clients-workspace-rail","Clients moves into its own workspace rail","2026-06-22T15:30:00",{"id":312,"slug":313,"projectId":77,"kind":200,"title":314,"contentHtml":202,"dateLabel":293,"publishedIso":315,"commitUrl":-1,"image":-1},"092f001b-c66a-4e11-ab21-4e4a9f0c8211","saa-485-hub-workspace-rail","The Hub moves into its own workspace rail","2026-06-22T15:00:00",{"id":317,"slug":318,"projectId":77,"kind":200,"title":319,"contentHtml":202,"dateLabel":293,"publishedIso":320,"commitUrl":-1,"image":-1},"370281bf-bb5f-4945-95bc-858fb350fb5f","saa-482-analytics-workspace-rail","Analytics moves into its own workspace rail","2026-06-22T14:30:00",{"id":322,"slug":323,"projectId":77,"kind":200,"title":324,"contentHtml":202,"dateLabel":293,"publishedIso":325,"commitUrl":-1,"image":-1},"ac003de6-5e6b-4df6-af06-8e2c7c277c79","saa-480-repertoire-workspace-rail","Repertoire moves into its own workspace rail","2026-06-22T14:00:00",{"id":327,"slug":328,"projectId":77,"kind":200,"title":329,"contentHtml":202,"dateLabel":293,"publishedIso":330,"commitUrl":-1,"image":-1},"9bc9a219-134f-429c-a733-cd208cc44869","saa-478-rails-brand-footer","Workspace rails get the brand and user footer back, and auto-close on mobile","2026-06-22T13:30:00",{"id":332,"slug":333,"projectId":77,"kind":200,"title":334,"contentHtml":202,"dateLabel":293,"publishedIso":335,"commitUrl":-1,"image":-1},"3f860a37-79a9-457b-ac0f-4e8e94ebffd9","saa-476-kb-back-to-dashboard","The Knowledge Base reader gets a role-aware 'Back to dashboard' escape hatch","2026-06-22T13:00:00",{"id":337,"slug":338,"projectId":77,"kind":200,"title":339,"contentHtml":202,"dateLabel":340,"publishedIso":341,"commitUrl":-1,"image":-1},"8a9554a0-76f4-4f80-a73b-ae680f98a8d2","saa-474-musician-learning-tab","Admin musician profiles get a Learning tab","5d ago","2026-06-21T11:00:00",{"id":343,"slug":344,"projectId":77,"kind":200,"title":345,"contentHtml":202,"dateLabel":340,"publishedIso":346,"commitUrl":-1,"image":-1},"1a65f722-954d-4851-bf50-fab071a90179","saa-472-per-image-download-toggle","Authors can now decide per image whether readers may download it","2026-06-21T10:00:00",{"id":348,"slug":349,"projectId":77,"kind":200,"title":350,"contentHtml":202,"dateLabel":351,"publishedIso":352,"commitUrl":-1,"image":-1},"6c8f16bf-2fd1-4cac-a5b4-a1b4185b2284","saa-470-image-lightbox-pdf-preview","Readers get an in-app image lightbox and PDF preview","6d ago","2026-06-20T15:00:00",{"id":354,"slug":355,"projectId":77,"kind":200,"title":356,"contentHtml":202,"dateLabel":351,"publishedIso":357,"commitUrl":-1,"image":-1},"6492c220-a74f-4b8e-a67e-201cab89bd8a","saa-468-kb-settings","Knowledge Base Settings lands, with an access audit and icon polish","2026-06-20T14:00:00",{"id":359,"slug":360,"projectId":77,"kind":200,"title":361,"contentHtml":202,"dateLabel":351,"publishedIso":362,"commitUrl":-1,"image":-1},"2513dedd-172d-468c-b537-393da982d8fe","saa-467-kb-shared-reader","One shared Knowledge Base reader now serves both musicians and staff","2026-06-20T13:00:00",{"id":364,"slug":365,"projectId":77,"kind":200,"title":366,"contentHtml":202,"dateLabel":367,"publishedIso":368,"commitUrl":-1,"image":-1},"786a484d-5914-4862-9fa8-1c1fcc4febaa","saa-465-kb-module-foundation","The Knowledge Base module begins: database foundation and admin workspace","1w ago","2026-06-19T14:00:00",{"id":370,"slug":371,"projectId":77,"kind":213,"title":372,"contentHtml":202,"dateLabel":367,"publishedIso":373,"commitUrl":-1,"image":-1},"4301e0b8-9112-41c7-a84f-804432071054","saa-463-lesson-attachment-cards","Lesson attachments now render as full-width download cards","2026-06-19T13:00:00",{"id":375,"slug":376,"projectId":77,"kind":213,"title":377,"contentHtml":202,"dateLabel":367,"publishedIso":378,"commitUrl":-1,"image":-1},"f169312e-e4c5-42cb-b921-c5816fa45ea2","saa-461-course-player-controls","Course player controls make sense again: contents on the left, close on the right","2026-06-19T12:00:00",{"id":380,"slug":381,"projectId":77,"kind":200,"title":382,"contentHtml":202,"dateLabel":367,"publishedIso":383,"commitUrl":-1,"image":-1},"d15f9b1c-635c-41d7-bc1a-3292836e972e","saa-459-course-rich-content","Course content gains colour, highlight and cards, and the link dialog is fixed","2026-06-19T11:00:00",{"id":385,"slug":386,"projectId":77,"kind":200,"title":387,"contentHtml":202,"dateLabel":367,"publishedIso":388,"commitUrl":-1,"image":-1},"86a7a1f1-be51-4c1a-b6c6-141843c05cbb","saa-457-course-overview-page","Courses get an overview page, cover images and rich cards","2026-06-18T16:00:00",{"id":390,"slug":391,"projectId":77,"kind":200,"title":392,"contentHtml":202,"dateLabel":367,"publishedIso":393,"commitUrl":-1,"image":-1},"11169a70-fdbe-4c80-b2af-40fdb85a62bc","saa-456-musician-learning-player","Musicians get a focused Learning player, surfaced across the dashboard and nav","2026-06-18T15:30:00",{"id":395,"slug":396,"projectId":77,"kind":200,"title":397,"contentHtml":202,"dateLabel":367,"publishedIso":398,"commitUrl":-1,"image":-1},"0f2ce120-c96e-4386-8f91-c6ca10a7f674","saa-454-admin-courses-workspace","The admin Courses workspace lands: list, builder, tracking and settings","2026-06-18T15:00:00",{"id":400,"slug":401,"projectId":77,"kind":200,"title":402,"contentHtml":202,"dateLabel":367,"publishedIso":403,"commitUrl":-1,"image":-1},"b02d5c06-b870-4ebe-8729-2a86a66f9c0f","saa-452-courses-module-foundation","The Courses\u002FLearning module begins with its database foundation and types","2026-06-18T14:30:00",{"id":405,"slug":406,"projectId":77,"kind":303,"title":407,"contentHtml":202,"dateLabel":367,"publishedIso":408,"commitUrl":-1,"image":-1},"8e8b0fc9-7359-4b9c-9e8d-37940fb0a02a","saa-450-appsidebar-refactor","Broke the 715-line AppSidebar into nav groups, down to 478","2026-06-18T14:00:00",{"id":410,"slug":411,"projectId":77,"kind":200,"title":412,"contentHtml":202,"dateLabel":367,"publishedIso":413,"commitUrl":-1,"image":-1},"6777a296-5709-4fde-ab4e-1a2cd14494ba","saa-448-settings-workspace","Settings moves into its own workspace","2026-06-18T13:30:00",{"id":415,"slug":416,"projectId":77,"kind":200,"title":417,"contentHtml":202,"dateLabel":367,"publishedIso":418,"commitUrl":-1,"image":-1},"d9c05de6-d295-4d39-af86-a42c61dac6f7","saa-446-workspaceshell-primitive","A new WorkspaceShell layout primitive lays the groundwork for workspaces","2026-06-18T13:00:00",{"id":420,"slug":421,"projectId":77,"kind":213,"title":422,"contentHtml":202,"dateLabel":367,"publishedIso":423,"commitUrl":-1,"image":-1},"2faf58db-4f82-4cab-94df-f8cb687d0e54","saa-444-notifications-spacing","Notifications lose a redundant meta row and tighten up their spacing","2026-06-17T11:00:00",{"id":425,"slug":426,"projectId":77,"kind":200,"title":427,"contentHtml":202,"dateLabel":367,"publishedIso":428,"commitUrl":-1,"image":-1},"27f9aaa1-da72-4bd6-8937-25443ecfdd1a","saa-443-contract-type","Contracts gain a type — onboarding vs land rehearsal — and rehearsals drop out of the analytics","2026-06-16T11:00:00",{"id":430,"slug":431,"projectId":77,"kind":200,"title":432,"contentHtml":202,"dateLabel":433,"publishedIso":434,"commitUrl":-1,"image":-1},"bab1755b-a90c-4545-9097-833827041e32","saa-441-contract-length-distribution","Analytics gains a contract-length distribution and a corrected average-length KPI","2w ago","2026-06-11T11:00:00",{"id":436,"slug":437,"projectId":77,"kind":200,"title":438,"contentHtml":202,"dateLabel":433,"publishedIso":439,"commitUrl":-1,"image":-1},"c31da2d7-2a46-46c5-84e5-3d571bef7df4","saa-439-return-rate-stats","Analytics now tracks musician return rate and re-engagement","2026-06-11T10:00:00",{"id":441,"slug":442,"projectId":77,"kind":213,"title":443,"contentHtml":202,"dateLabel":433,"publishedIso":444,"commitUrl":-1,"image":-1},"eb1b1d48-e0ea-4e80-8a87-f31672162a6f","saa-437-chat-notification-thread","Chat notifications stop opening the wrong thread and leaking messages across channels","2026-06-10T17:00:00",{"id":446,"slug":447,"projectId":77,"kind":213,"title":448,"contentHtml":202,"dateLabel":433,"publishedIso":449,"commitUrl":-1,"image":-1},"8dc54e32-7d40-49e8-888b-ef1cc60b4f1d","saa-434-ssr-viewport-teleports","Squashed more hydration gremlins: an SSR viewport hint and mount-gated dialog teleports","2026-06-10T16:00:00",{"id":451,"slug":452,"projectId":77,"kind":213,"title":453,"contentHtml":202,"dateLabel":433,"publishedIso":454,"commitUrl":-1,"image":-1},"f9dadedc-0c06-4750-91fb-731f69fb493b","saa-433-hydration-followups","Hydration follow-ups: timezone greeting, plugin promise idiom, sidebar default and AppHeader gating","2026-06-10T15:00:00",{"id":456,"slug":457,"projectId":77,"kind":213,"title":458,"contentHtml":202,"dateLabel":433,"publishedIso":459,"commitUrl":-1,"image":-1},"3409d7c4-0f22-4c6d-b97a-f523c6e4457b","saa-432-admin-hydration-mismatches","Eliminated the admin hydration mismatches, and retired the sidebar Cmd+B shortcut","2026-06-10T14:00:00",{"id":461,"slug":462,"projectId":77,"kind":303,"title":463,"contentHtml":464,"dateLabel":433,"publishedIso":465,"commitUrl":-1,"image":-1},"a7d96aa3-6672-49ab-b2bd-4d2c06ba2de9","tab-counters-one-shape","Tab counters had grown eight different shapes. Now there is one.","\u003Cp class=\"wp-block-paragraph\">The little number chips that sit beside tab labels — &#8220;All 24&#8221;, &#8220;Unread 3&#8221; — had been built eight slightly different ways across the app as features piled up, and they had drifted into eight slightly different sizes. A couple were tall enough to nudge their tab out of line with its neighbours. I pulled them all onto one shared chip with a fixed height and tightened the tabs themselves into a cleaner, compact pill, so every tab strip in the app now lines up and reads the same. The sort of thing nobody notices when it is right and everybody feels when it is wrong.\u003C\u002Fp>","2026-06-10T11:00:00",{"id":467,"slug":468,"projectId":77,"kind":213,"title":469,"contentHtml":470,"dateLabel":433,"publishedIso":471,"commitUrl":-1,"image":-1},"a69b1b83-9be2-4922-8597-5e75cec89fae","shared-repertoire-looks-read-only","Shared repertoire did not look read-only — so people kept trying to edit it.","\u003Cp class=\"wp-block-paragraph\">When a musician shares their repertoire ratings with someone, the viewer gets a read-only copy. The trouble was it did not look read-only — the rating dots were identical to the editable ones, so viewers kept tapping them and waiting for something to happen. I made the view-only state unmistakable: static dots, no hover affordances, and a clear marker that this is someone else&#8217;s repertoire. A small change that removes a confusing dead end.\u003C\u002Fp>","2026-06-10T09:00:00",{"id":473,"slug":474,"projectId":77,"kind":200,"title":475,"contentHtml":476,"dateLabel":433,"publishedIso":477,"commitUrl":-1,"image":-1},"e89b409b-6daa-4e09-b30b-68eb81beb157","bookmarks-proper-list","Bookmarks finally read like a proper list.","\u003Cp class=\"wp-block-paragraph\">Bookmarks shipped recently as a way to save any musician, contract, document or note for later, but the list itself was a bit scrappy — uneven rows, no way to sort, and the note you had jotted squeezed for space. I rebuilt it as a proper table: consistent rows, a note column that flexes to the room available, and sortable headers so you can order by type or date. Now it works the way the &#8220;save for later&#8221; surface was always meant to.\u003C\u002Fp>","2026-06-09T09:00:00",{"id":479,"slug":480,"projectId":77,"kind":213,"title":481,"contentHtml":482,"dateLabel":433,"publishedIso":483,"commitUrl":-1,"image":-1},"f9c45dd3-d678-42de-ba09-14d9749ae849","expiry-reminder-once-per-window","Musicians were getting the same expiry reminder every other day. Now it is once.","\u003Cp class=\"wp-block-paragraph\">Our document-expiry reminders had a subtle bug. Instead of one nudge when a document entered its warning window, musicians were getting the same reminder every couple of days for the whole window — dozens of emails for a single passport over its renewal period. The cause was a timing guard sized for the wrong interval. I rewrote it so a reminder fires once when a document reaches its threshold and then stays quiet until the document is actually renewed. Far fewer emails, and the ones that do land mean something.\u003C\u002Fp>","2026-06-06T09:00:00",{"id":485,"slug":486,"projectId":77,"kind":200,"title":487,"contentHtml":488,"dateLabel":489,"publishedIso":490,"commitUrl":-1,"image":-1},"d1485801-008c-4818-998c-63f6bae138e5","announcements-one-banner-everyone-sees","Announcements: one banner, everyone sees it.","\u003Cp class=\"wp-block-paragraph\">Added announcements: a dismissable banner the team can put across the top of any of the three portals — staff, musicians or clients — to flag a maintenance window, a policy change, or anything everyone needs to see at once. Each one targets whichever audiences it is meant for, sits inside a date window so it shows and hides itself, and a reader can dismiss it once and never see it again. The kind of small, boring tool you reach for constantly the moment it exists.\u003C\u002Fp>","3w ago","2026-06-05T13:00:00",{"id":492,"slug":493,"projectId":77,"kind":200,"title":494,"contentHtml":495,"dateLabel":489,"publishedIso":496,"commitUrl":-1,"image":-1},"7228cd06-05c9-4243-b11f-396986a088c5","dsar-data-request-one-button","A data request used to mean hunting down files by hand. Now it is one button.","\u003Cp class=\"wp-block-paragraph\">Under GDPR anyone can ask for a copy of every piece of data you hold on them, and you have a month to deliver it. That used to mean an admin manually tracking down a musician&#8217;s documents and exporting their records one by one. I wired the whole thing into a single action: the platform now gathers the person&#8217;s held documents and a full export of their records into one bundle, fronted by a branded cover letter that lists exactly what is included. A legal obligation most companies quietly dread, turned into a button.\u003C\u002Fp>","2026-06-05T09:00:00",{"id":498,"slug":499,"projectId":77,"kind":200,"title":500,"contentHtml":501,"dateLabel":489,"publishedIso":502,"commitUrl":-1,"image":-1},"2954cf3a-d6a8-4431-9790-1c24910a10dd","analytics-query-builder-stacked-filters","The analytics query builder could only filter one thing at a time. Now it stacks.","\u003Cp class=\"wp-block-paragraph\">The analytics query builder lets managers slice the roster — by position, status, nationality, document status and so on — but every filter could only hold a single value, so a question as simple as &#8220;pianists or guitarists&#8221; was impossible. I made every filter multi-select: pick several values in one field and it matches any of them, combine different fields and they narrow together. While I was in there I added saved queries, so a useful slice can be named, kept and shared instead of rebuilt from scratch each time. It turns the builder from a one-shot lookup into something you actually live in.\u003C\u002Fp>","2026-06-04T09:00:00",{"id":504,"slug":505,"projectId":77,"kind":200,"title":506,"contentHtml":507,"dateLabel":489,"publishedIso":508,"commitUrl":-1,"image":-1},"6bbccf7a-6281-4506-8dd9-68be40aa4a5c","expiry-reminders-were-chasing-musicians-who-had-already-left","Expiry reminders were chasing musicians who had already left.","\u003Cp class=\"wp-block-paragraph\">Two notification cleanups landed together. First, the daily document-expiry reminder was emailing, and pestering managers about, musicians who are inactive, archived or no longer with us; scoping it to active musicians only cut the daily noise roughly in half. Second, I enriched the offer- and extension-letter notifications so they carry the full set of details any email template might want, rather than only what today’s template happens to use. That second one is purely preventive: it means wiring up a richer email later will not render with half the merge fields blank, which is exactly the kind of silent breakage that looks awful in front of a musician.\u003C\u002Fp>","2026-05-30T09:00:00",{"id":510,"slug":511,"projectId":77,"kind":213,"title":512,"contentHtml":513,"dateLabel":514,"publishedIso":515,"commitUrl":-1,"image":-1},"8ff7d79b-de86-418a-9bdf-d99b03c595be","the-workflow-builder-could-never-actually-save-a-step","The workflow builder could never actually save a step.","\u003Cp class=\"wp-block-paragraph\">It turned out the workflow template builder had never been driven from the UI: the existing steps were all seeded directly, and the save path was writing to fields that did not exist, so every save silently failed. Fixing it was a peel-the-onion exercise because the data layer only complains about the first unknown field at a time, so I had to diagnose the whole payload at once rather than one error at a time. Then I found the runtime side was dropping step dependencies and skipping due-date calculation for every date type but one, so I rebuilt provisioning to carry blocks, dependencies and all the due-date variants through properly. A feature that looked finished on the surface and was quietly broken from end to end.\u003C\u002Fp>","4w ago","2026-05-29T09:00:00",{"id":517,"slug":518,"projectId":77,"kind":213,"title":519,"contentHtml":520,"dateLabel":514,"publishedIso":521,"commitUrl":-1,"image":-1},"bdf1a29a-8808-47c5-bc34-b9ea8257f275","one-document-upload-was-firing-two-emails","One document upload was firing two emails.","\u003Cp class=\"wp-block-paragraph\">A musician submitting a document in a single action was somehow triggering both an uploaded and a submitted notification to every manager, so everyone got two emails for one event. The cause was subtle: completing the form on upload crosses two state thresholds in the same write, and the notification logic treated each crossing as its own event. I added a guard so when both transitions happen together only the meaningful one fires, while the genuine upload-now-finish-later case still sends both. While I was in there I also fixed the wording so when an admin acts on a musician’s behalf the message says so, instead of pretending the musician did it themselves.\u003C\u002Fp>","2026-05-28T13:00:00",{"id":523,"slug":524,"projectId":77,"kind":303,"title":525,"contentHtml":526,"dateLabel":514,"publishedIso":527,"commitUrl":-1,"image":-1},"89a2de0f-ab4b-4574-a23c-981681f77e84","two-badge-systems-doing-the-same-job-now-there-is-one","Two badge systems doing the same job. Now there is one.","\u003Cp class=\"wp-block-paragraph\">The app had quietly grown two parallel ways of rendering status pills, an older custom badge style and the newer shared component, and they had drifted just enough to look inconsistent next to each other. Worse, the hand-rolled ones kept leaking raw values like not_verified straight into the UI instead of a clean Not Verified. I collapsed everything into a single badge system so every status pill goes through the same label-mapping path. Tedious chasing down every last usage, but now there is exactly one place to change how a badge looks or reads.\u003C\u002Fp>","2026-05-28T09:00:00",{"id":529,"slug":530,"projectId":77,"kind":200,"title":531,"contentHtml":532,"dateLabel":533,"publishedIso":534,"commitUrl":-1,"image":-1},"14c87183-3a9e-4c78-bf03-08349dade4ae","i-kept-losing-the-musician-i-was-looking-at-five-minutes-ago","I kept losing the musician I was looking at five minutes ago.","\u003Cp class=\"wp-block-paragraph\">Managers bounce between musicians, contracts, documents and notes all day, and there was no quick way to pin the handful you are actively working on. So I added personal bookmarks: a toggle on each detail page, a count badge in the top bar, and a dedicated page with per-type tabs and an optional note on each one. The fiddly bit was making a freshly added bookmark show its full details immediately instead of a blank placeholder until refresh; an optimistic insert only hands you the bare row, so I had to re-fetch the enriched version right after saving. Bookmarks are strictly private too, so even admins cannot see anyone else’s.\u003C\u002Fp>","1mo ago","2026-05-27T13:00:00",{"id":536,"slug":537,"projectId":77,"kind":200,"title":538,"contentHtml":539,"dateLabel":533,"publishedIso":540,"commitUrl":-1,"image":-1},"8a56c992-84d7-4945-a311-212811b074de","recruiters-were-tracking-musician-follow-through-in-their-heads-now-it-has-a-colour","Recruiters were tracking musician follow-through in their heads. Now it has a colour.","\u003Cp class=\"wp-block-paragraph\">Our recruiters judge musicians across the whole hiring journey: did they reply quickly, show up on time for the video call, get their compliance paperwork in. All of that lived in people’s heads or scattered notes. So I built a green\u002Famber\u002Fred rating across seven lifecycle touchpoints, with the worst rating bubbling up as an at-a-glance overall on the musician profile and the list. The one rule I cared about was that you cannot mark someone amber or red without writing why, enforced both in the form and at the data layer so a rushed rating can never skip the context.\u003C\u002Fp>","2026-05-27T09:00:00",{"id":542,"slug":543,"projectId":77,"kind":213,"title":544,"contentHtml":545,"dateLabel":533,"publishedIso":546,"commitUrl":-1,"image":-1},"8d53e132-3476-4d61-8fba-74545227118a","you-couldnt-revoke-a-document-mid-review-now-you-can","You couldn’t revoke a document mid-review. Now you can.","\u003Cp class=\"wp-block-paragraph\">A document sitting in review was effectively frozen — if a manager uploaded the wrong file or realised a mistake, the only option was to reject it and start over, which created noise in the audit trail and an unnecessary rejection notification. I extended the revoke action to cover in-review documents, with a status-aware dialog that shows “Reset Review” instead of the standard revoke label so the intent is clear. The behaviour is deliberately different: revoking an in-review doc preserves any review notes that were added during the review pass, whereas revoking a verified or rejected doc wipes them — because the use cases are different. I also added an inline notes editor to the admin document sidebar so managers can annotate a document directly without hunting for a separate notes panel, and that same note surfaces as read-only on the musician portal so they can see any context left for them. The activity timeline view was rebuilt to include a distinct “Review Reset” entry, which matters because the database trigger that fires on status changes would otherwise swallow the revoke action and lose the audit event.\u003C\u002Fp>","2026-05-15T11:00:00",{"id":548,"slug":549,"projectId":77,"kind":213,"title":550,"contentHtml":551,"dateLabel":533,"publishedIso":552,"commitUrl":-1,"image":-1},"f4d3eec7-aa1b-4753-ab7b-674c683f7772","documents-in-review-can-now-be-revoked-and-verification-notes-are-editable-inline","Documents in review can now be revoked, and verification notes are editable inline","\u003Cp class=\"wp-block-paragraph\">Two small but meaningful improvements to the document verification workflow. Previously, admins could only revoke documents that were already verified or rejected — if a document had just entered the review queue and something was wrong, you were stuck. Now revocation works on in-review documents too, with a status-aware dialog that shows “Reset Review” instead of the generic revoke label so the intent is clear. We also added an inline notes field directly on the document sidebar — admins can attach a note without going through the full notes system, the note persists in the activity trail even through status changes, and musicians see it in read-only form on their portal. It’s the kind of thing where you notice the absence more than the presence once it’s there.\u003C\u002Fp>","2026-05-15T09:00:00",{"id":554,"slug":555,"projectId":77,"kind":303,"title":556,"contentHtml":557,"dateLabel":533,"publishedIso":558,"commitUrl":-1,"image":-1},"fdf1f762-85d3-499e-904f-bc5e05cfb953","the-musician-profile-page-was-1200-lines-the-page-itself-is-now-under-100","The musician profile page was 1,200 lines. The page itself is now under 100.","\u003Cp class=\"wp-block-paragraph\">The admin musician detail page had grown to over 1,200 lines of mixed concerns — data loading, UI state, tab logic, Heartbeat provisioning, compliance calculations, all tangled together. We extracted everything into a dedicated \u003Ccode>useMusicianPageOrchestrator\u003C\u002Fcode> composable, which now owns the full data lifecycle: loading contracts, documents, notes, compliance maps, and phase progress in one coordinated place. After the relocation was stable, we went back and fixed four reactivity violations that had been lurking in the remaining code — places where state was being mutated outside of Vue’s reactive system, which could cause the UI to show stale data after a status change or provisioning action. The page file delegates to the orchestrator and handles only the UI shell.\u003C\u002Fp>","2026-05-14T09:00:00",{"id":560,"slug":561,"projectId":77,"kind":303,"title":562,"contentHtml":563,"dateLabel":533,"publishedIso":558,"commitUrl":-1,"image":-1},"0f1fd514-7d1b-4d3f-ae47-0405e9b5e962","the-musicians-detail-page-was-1400-lines-now-its-600","The musicians detail page was 1,400 lines. Now it’s 600.","\u003Cp class=\"wp-block-paragraph\">The admin musician detail page had accumulated everything in one file — data fetching, form state, hub wiring, repertoire, email composition, availability. Extracted all of it into a dedicated orchestrator composable. The template is byte-identical; nothing visible changed. One near-miss during cleanup: a grep for an unused import matched component names that happened to contain the same string, and I nearly dropped an icon still being used in the template. Code review caught it.\u003C\u002Fp>",{"id":565,"slug":566,"projectId":77,"kind":200,"title":567,"contentHtml":568,"dateLabel":533,"publishedIso":569,"commitUrl":-1,"image":-1},"ea761cc9-fbd8-43e7-b2db-880b2b6238ae","built-gdpr-data-request-management-from-scratch","Built GDPR data request management from scratch","\u003Cp class=\"wp-block-paragraph\">The app needed a proper way to handle data subject requests — access, erasure, rectification, portability — under GDPR\u002FUK-GDPR. Built it admin-only: log incoming requests, verify identity, generate a data export, manage correspondence across multiple channels, and track the 30-day compliance clock with automated overdue alerts. Erasure requires a type-to-confirm gate before anything destructive runs. Took a spec-first approach — two rounds of fresh-eye review before a line of code was written, because the scope was wide enough that finding a design flaw mid-build would have been expensive.\u003C\u002Fp>","2026-05-12T09:00:00",{"id":571,"slug":572,"projectId":77,"kind":200,"title":573,"contentHtml":574,"dateLabel":533,"publishedIso":575,"commitUrl":-1,"image":-1},"b3879fdc-95a2-41e3-a6b1-b98484fe9e20","client-portal-documents-now-have-a-preview","Client portal documents now have a preview","\u003Cp class=\"wp-block-paragraph\">Clicking the preview icon on a document row in the client portal opens it inline without navigating away. Dropped some stale inline form data that had been cluttering the row metadata while I was in there.\u003C\u002Fp>","2026-05-05T09:00:00",{"id":577,"slug":578,"projectId":77,"kind":200,"title":579,"contentHtml":580,"dateLabel":533,"publishedIso":581,"commitUrl":-1,"image":-1},"2cdf0c55-df2f-4a1f-92cc-fdc6fadc412b","let-users-turn-off-dms","Let users turn off DMs","\u003Cp class=\"wp-block-paragraph\">Managers and admins can now set their DM availability to ‘internal team only’ or ‘not accepting DMs’. It’s asymmetric — restrictions are based on the recipient’s setting and the sender’s role, so the same block applies differently depending on who’s trying to send. The UI gates the interaction, but there’s server-side enforcement as a backstop in case of stale caches or anything bypassing the front end. One thing I’d do differently: the role comparison was case-sensitive in a way that caused a quiet failure in one path — server enforcement was catching it silently before we spotted it.\u003C\u002Fp>","2026-05-03T09:00:00",{"id":583,"slug":584,"projectId":77,"kind":200,"title":585,"contentHtml":586,"dateLabel":533,"publishedIso":587,"commitUrl":-1,"image":-1},"bf9d9139-233c-4dcb-8d6b-30ebb2047a96","added-seen-by-to-space-posts","Added ‘Seen by’ to Space posts","\u003Cp class=\"wp-block-paragraph\">Space posts now track who’s read them, mirroring the same approach already used in chat. There’s a compact indicator on post cards and a full popover with reader names on the detail view. The read happens silently when posts load. One deliberate call: authors are always excluded from their own ‘Seen by’ list — you obviously read what you just wrote.\u003C\u002Fp>","2026-04-30T09:00:00",{"id":589,"slug":590,"projectId":77,"kind":200,"title":591,"contentHtml":592,"dateLabel":593,"publishedIso":594,"commitUrl":-1,"image":-1},"6b3c11fb-c2ad-4eb1-9127-65027548269e","documents-can-now-be-revoked-while-in-review","Documents can now be revoked while in review","\u003Cp class=\"wp-block-paragraph\">Previously a document stuck in ‘in review’ state couldn’t be revoked — you had to wait for the review to complete first. Closed that gap. Also added editable inline notes to document rows while I was in there.\u003C\u002Fp>","2mo ago","2026-04-26T09:00:00",{"id":596,"slug":597,"projectId":77,"kind":250,"title":598,"contentHtml":599,"dateLabel":593,"publishedIso":600,"commitUrl":-1,"image":-1},"c81bdb89-6f0e-43cf-9aa3-0f5d027ddf98","decided-against-merging-the-marketing-site-and-the-app","Decided against merging the marketing site and the app","\n\u003Cp class=\"wp-block-paragraph\">Was tempted to unify the marketing site and the Sonic Artistes app under one Nuxt SSR deployment. Thought about it for an hour — it’s a bad idea. The marketing site is fully static (Cloudflare cache hits 100%); the app is heavy SSR behind auth. Merging them costs the static cache and buys nothing. They stay apart.\u003C\u002Fp>\n\n\n\n\u003Cp class=\"wp-block-paragraph\">\u003C\u002Fp>\n","2026-04-24T16:38:51",{"id":602,"slug":603,"projectId":77,"kind":303,"title":604,"contentHtml":605,"dateLabel":593,"publishedIso":606,"commitUrl":-1,"image":-1},"23e246e7-8887-4b8c-beba-a53ab8f7af49","pulled-document-compliance-logic-into-a-composable","Pulled document compliance logic into a composable","\n\u003Cp class=\"wp-block-paragraph\">The compliance panel had grown a fourth “is this document valid” branch and the component was creaking. Extracted \u003Ccode>useDocumentStatus()\u003C\u002Fcode> — the same logic now drives the list, the detail modal, and the cruise-line export. No behaviour change, but the unit tests finally cover the whole thing.\u003C\u002Fp>\n\n\n\n\u003Cp class=\"wp-block-paragraph\">\u003C\u002Fp>\n","2026-04-24T16:37:41",[],1782519037277]