Changelog — Site Builder
All notable changes to this app are recorded here. Newest entries on top.
[2026-05-02]
- Stripped back the media-library overrides — round 2. Previous attempt to switch the list to a vertical flex stack also broke the layout: 300×118 cards were getting forced into a single tall column, the hover-preview popover was positioned outside the modal, and action buttons overlapped the file name. The original media.css uses
display: inline-block; width: 300px; height: 118px cards floating side-by-side — that layout works fine, it just has the wrong colors. Reverted to a minimal cosmetic overlay that only changes background / border / text colors / focus rings on .filemanager .data li, plus quiets the bright Bootstrap btn-outline-primary / btn-outline-danger action buttons. Zero touches to display, position, width, height, or any layout property.
[2026-05-02]
- Fixed media-library thumbnail overlap. The previous facelift forced
ul.data into a CSS grid with cards (grid-template-columns: repeat(auto-fill, minmax(120px, 1fr))), but VvvebJS structures every <li class="files"> as a horizontal row containing an icon, name + size, action buttons, and a hover-only preview popover. The grid override smashed all those inline children into the same cell, so the row icon + the hover-preview's image rendered on top of each other. Reverted to the original list-style layout (vertical stack, one row per file), kept only the cosmetic dark theme — row background, accent-green selected state, themed action icons (rename / delete / zoom-preview), small 36×36 thumbnail at the left, name + details on a single line, dark preview popover. No structural changes; just CSS that respects the markup it's styling.
[2026-05-02]
- Fixed broken side panel — Templates / Pages / Elements / Stock tabs were not clickable. Last change introduced a JS syntax error:
console.warn('… VvvebJS didn\\'t clear it'). I escaped the apostrophe with a double backslash (PHP HEREDOC convention I'd been using elsewhere), but _editor-core.html is a plain HTML file — JS sees \\' as an escaped backslash followed by a string-terminator quote, breaks parsing of the rest of the script (SyntaxError: missing ) after argument list), and every IIFE after that point dies — including the tab click handlers. Fixed by dropping the apostrophe and using "did not" in the log message. Audited the rest of _editor-core.html for the same bug — every other apostrophe escape in there was already correct (\' produced from PHP HEREDOC \\' is valid JS).
[2026-05-02]
- Fixed canvas spinner stuck after creating a new page. The previous safety net that hid VvvebJS\'s
loading-message overlay only ran ONCE — at initial editor boot. So when a user created a new page mid-session, the iframe started loading the new draft URL, and if VvvebJS\'s init chain hit any snag in _frameLoaded (or just took its time waiting on Bootstrap\'s CDN to finish loading inside the iframe), the spinner stayed visible forever. New behaviour: a MutationObserver watches the canvas iframe\'s src attribute and re-arms a 5-second safety net every time it changes, plus an explicit iframe.load listener clears the spinner ~600ms after each load fires regardless of what VvvebJS does internally. Console logs a warning ([SB] loading-message safety net fired) when the safety net actually has to do anything, so we know if there\'s a real bug to chase. Initial-boot safety stays at 6 seconds.
- Confirmed: upload-project saves files in the correct user folder. Verified that the existing path resolution (
sb_user_root($userId) → users/{uuid}/sitebuilder/drafts/{siteId}/) is what upload-project.php uses, and the static-passthrough route serves those exact paths back. No path leakage possible — the realpath check at write time refuses any file that ends up outside realpath($draftRoot).
- Moved the "view page" arrow icon out from under the demo countdown badge. When a user had a long site name (e.g. "Ironwood-Lock-and-Key"), the platform\'s demo banner overlapped the site-indicator group and hid the external-link icon next to it — making it impossible to click "open the page in a new tab". Moved the icon out of the site-indicator group and into the action-buttons group on the right (immediately before the Publish button) where there\'s no overlap with the demo banner regardless of site name length.
- VvvebJS Media library facelift. The built-in image picker (clicked when you want to swap an image\'s source) was Bootstrap-default light theme — bright blue "Browse Files" button, white modal, etc. — totally clashing with the editor\'s dark theme. Cosmetic-only refresh: dark gradient modal surface, accent-green dashed drop zone with a halo icon, accent-green primary buttons (Browse Files, Add Selected) with the same gradient as our other primary CTAs, dark cards in the file grid with accent border on hover/selected, accent-green focus ring on the search input, dark footer. Every rule scoped to
body.sb-editor #MediaModal ... so it can\'t leak into other Bootstrap modals on the page. Zero markup or JS changes — drop the CSS block to revert.
- Fixed two upload-flow bugs that left users stuck after a successful upload:
- Editor was showing the "Welcome — what would you like to build today?" overlay on top of the uploaded site. The welcome overlay only hides when
sites.template is non-empty, and the upload was creating sites with template = NULL (correct in the literal sense — there's no template), so the editor treated each upload like a fresh blank canvas. Fix: upload now writes template = '_uploaded' (underscore-prefix sentinel that can't collide with a real template id) so the welcome overlay stays hidden and the user lands directly on their imported site, ready to edit.
- Auto-redirect to the new site's editor was unreliable. Browsers can block programmatic top-window navigation triggered by an XHR callback (no user-gesture context), and the modal had no visible escape hatch when the redirect quietly failed. Replaced the silent redirect with a success card that swaps in after upload: green check-circle icon, "Got it — your site is ready to edit", a big primary "Open {site name} in the editor" button (real
<a target="_top"> so it always works as a user-clicked navigation), and a small "Stay here for now" link. The card still attempts auto-redirect 400ms after success, but the button is the source of truth — if browser policy or anything else blocks the auto-nav, the user has an obvious one-click next step that can't be blocked.
- Upload modal — non-technical copy + actionable error messages. First-cut version was full of jargon ("HTML file or ZIP archive — saved as the index of a new site... extracted into the new site\'s draft folder"). Rewrote everything for a small-business owner who hired a designer and just wants to bring their site in:
- Title: "Open a website you already have"
- Subtitle: "Already got a site from a designer, an old export, or somewhere else? Bring it here and edit it just like one of our templates."
- Drop zone: "Drop your website file here / or click anywhere in this box to pick one / Web page (.html) or zipped folder (.zip) — up to 25 MB"
- How this works (numbered): "Pick your file" → "We open it for you" → "Publish when you\'re ready", each with one sentence of plain English.
Also fixed the drop zone being left-aligned within the modal — the <label> was inline by default; now it\'s display: block; width: 100%; box-sizing: border-box so it fills the card.
- **Every upload error now explains why and *what to do next*** instead of dumping a one-line technical message. Each rejection comes back as a
{ title, hint } pair the modal renders as two lines (red title + lighter actionable hint). Examples:
- PHP code in HTML → "We can\'t accept this file because it has server code in it. / Your page contains PHP code (the bit that starts with
<?php). For security, we only accept regular web pages. Open the file in a text editor, remove the <?php blocks, and try again."
- Executable in ZIP → "There\'s a program file in your ZIP that we can\'t accept: shell.php / For security, we only accept content files (HTML, CSS, JavaScript, images, fonts, videos). Files like .php are program code that runs on the server and could be unsafe to host. Remove that file from the folder and re-zip."
- Missing index.html → "Your ZIP doesn\'t have an index.html file. / Every website needs a home page called index.html. Make sure that file exists in your folder (or one folder deep) and re-zip. If the file is named differently — like home.html or main.html — rename it to index.html first."
- Path traversal, hidden files, server-config files (.htaccess / .env / web.config), zip-bomb size caps, too-many-files, file > 10 MB, total > 50 MB, corrupted ZIP, network failure, server save failure — all now spell out the user-actionable fix. Same goes for client-side rejections (wrong extension, > 25 MB) before the upload even starts. New
SBUploadError class carries title + hint cleanly through the catch block.
[2026-05-01]
- Upload an existing site as a new project. New cloud-arrow icon in the toolbar (right next to the Properties gear) opens a drag-and-drop modal where you can upload a single HTML file or a ZIP archive of a static site. The upload becomes a brand-new site in your library with the original filename as the site name; the editor reloads onto the new site as soon as the upload completes. Security: 25 MB upload cap, extension whitelist (.html / .htm / .zip), and a strict ZIP entry walk that rejects path traversal, executable file types (.php / .phtml / .phar / .exe / .sh / .bat / .cgi / .pl / .py / .asp / .jsp / .dll / etc.), server-config files (.htaccess / .htpasswd / .env / web.config / php.ini), hidden dot-folders (.git / .ssh), per-file size > 10 MB, total uncompressed > 50 MB (zip-bomb defence), or > 1000 entries. Every HTML file (top-level OR inside the ZIP) goes through
sb_sanitize_html() to drop any embedded PHP. If index.html is buried inside a single subfolder of the ZIP, that folder is treated as the site root. Final realpath check on every written file confirms it actually lives inside the user's draft folder. Smoke-tested end-to-end: HTML, ZIP, embedded PHP shell rejected, path traversal rejected, executable extension rejected, missing index.html rejected.
- Stock tab cleanup. Removed the category chip strip (Trending / Wallpapers / 3D Renders / Nature / etc.) — the search bar and image/video toggle alone are enough; the chips ate vertical space and added a fiddly horizontal scrollbar. Search and the Images/Videos toggle now sit on a single row, and the toggle is icon-only (
🖼 / 🎬) so the search field gets the full remaining width. Cards reach further down the panel.
- Properties panel facelift + always-visible gear. Two small polish passes:
- The Properties (gear) toolbar button is now visible from page load instead of hiding until the first element click. Less surprising for users who already know they want to edit something.
- Gave the Properties panel a cosmetic refresh to match the rest of the editor's dark theme. Content / Style / Advanced tabs use our muted-uppercase typography with an accent-green underline on the active one. The "No selected element!" empty-state alert is now a dark card instead of Bootstrap's default warning yellow. Section headers (LINK / GENERAL / DIMENSIONS / etc.) match the small uppercase metadata style we use elsewhere. Form inputs (text, select, range, color, checkboxes) all use the dark
--sb-card surface with an accent-green focus ring. Custom dropdown chevron on <select> so it matches our other panels. Scrollbar styling tightened to thin grey on transparent. Every rule is scoped to body.sb-editor #left-panel #right-panel ... so it only affects the relocated panel — no chance of leaking into other panels or the canvas. No structural HTML changes, no component-handler changes; if anything looks broken, dropping just this CSS block reverts cleanly.
- Properties panel actually appears now when you click an element or the toolbar gear. The previous attempt repositioned
#right-panel over the left-panel area via CSS position: fixed; left: 0 — but VvvebJS's editor.css has many hardcoded rules on #right-panel (right: 0, float: right, width tied to --builder-right-panel-width, gradient backgrounds, box shadows) that fought my overrides and left the panel either invisible or rendered with zero width. Switched to a cleaner approach: physically move #right-panel into #left-panel on boot so it sits as a sibling of .pages-pane / .tpl-pane / .notes-pane, then style it to fill the panel area normally (position: static; flex: 1; width: auto). Show/hide follows the exact same body[data-sb-tab="properties"] pattern as every other tab pane — no fighting position: fixed, no z-index issues. VvvebJS still finds the panel by ID, so its auto-populate-on-selectNode behavior keeps working untouched.
- Properties / Notes panel: stop the Pages tree from leaking through. Switching to the Properties or Notes view used to show the new pane PLUS the Pages tree (Index / assets / etc.) underneath it, because the rule that hides VvvebJS's
#filemanager only listed the Pages, Templates, Elements, and Stock tabs — not the new Notes/Properties tabs. Added them to the hide list, then moved the same rule into editor.php's inline inject so we don't have to wait for the user's 24h editor.css cache to expire to see the fix. Also added a ?v={mtime} cache-buster to the editor.css link so future CSS edits roll out immediately on next reload instead of being held by the browser.
- Multi-page editing actually works now. Creating a new page from the editor's "+" button used to leave you with a 0-byte file and a Firefox "Can't Open This Page" error in the canvas iframe. Two bugs:
- VvvebJS's new-page form set
data.url to a bare filename like "about.html". The iframe's src resolved that against the parent's <base href="/app/sitebuilder/editor-assets/">, so the iframe tried to load /app/sitebuilder/editor-assets/about.html?... — which doesn't exist, so the route 404'd with the platform's default X-Frame-Options: DENY and Firefox blocked it. Patched Vvveb.FileManager.addPage to rewrite any non-absolute data.url to the full /app/sitebuilder/drafts/{siteId}/... path.
- The save endpoint wrote 0 bytes when VvvebJS submitted with
startTemplateUrl=new-page-blank-template.html and no html field (its convention for "seed this new page from a template"). Now apps/sitebuilder/api/drafts.php detects that case and writes a minimal HTML skeleton (<!doctype html> + bootstrap link + empty body) using the user's chosen page title. Existing files are protected — duplicate names return HTTP 409. Parent directories are auto-created so nested pages like blog/post.html work too.
Saving an already-existing page (the normal save path) is unchanged.
- Notes + Properties moved from the left tab strip to the top toolbar. The left strip was getting crowded; "STOCK" was sitting almost on top of the canvas. Notes and Properties are no longer 5th/6th tabs — they're toolbar buttons (sticky-note + gear icons) right next to "Toggle left column". The matching panes still render in the left-panel area when activated; only the trigger moved up. Each toolbar button toggles its mode on/off (clicking again returns you to whatever tab you were on before), shows an "active" pressed state when its pane is open, and the Properties button stays hidden until the user first clicks an element on the canvas.
- Notes save now shows visible feedback. Previously the save was silent — no way to tell if a note was actually saved. Now each note card flashes a small accent-green "Saving…" badge during the round-trip, switches to a "Saved ✓" badge with a green border-tint flash on success, then fades after ~1.6s. On failure, a red "Couldn't save" badge stays put + a toast surfaces the error. Skips the round-trip entirely when the textarea content hasn't actually changed since the last load.
- Notes are back — multi-note edition. The old Site Builder had a single-textarea-per-site notes feature stored over FTP. Brought it back as a proper multi-note scratchpad: a Notes tab in the editor's left tab strip (sticky-note icon, between Stock and Properties) where you can keep as many notes as you want about a site — bullet lists of TODOs, draft copy, color hex codes, design references, anything. Each note is its own card stacked newest-first, auto-saves on blur, shows an "Updated 3:42pm" timestamp, and has a small Delete button. Empty notes auto-delete to keep things tidy. New
site_notes table in the per-user app DB scoped per site (so each site's notes follow the site, not the user account). Server endpoints: GET /api/sitebuilder/notes/list?site_id=, POST create / update / delete. CSRF enforced; ownership joined through sites.created_by_user_id on every write so notes can't be guessed across users.
- Properties moved to a left-side tab — no more squished canvas. Earlier today the element-properties panel lived on the right side and was either always-visible (squishing the canvas, ugly divider, scrollbar) or auto-toggling (which annoyed users since they rarely click empty space to dismiss it). The right side is now retired entirely. New behaviour: a 5th icon-only Properties tab (gear glyph) appears in the left tab strip the first time the user clicks an element on the canvas, and auto-activates. Pages / Templates / Elements / Stock stay one click away — switching tabs no longer "closes" properties; it's just a tab. Implementation:
#right-panel is now positioned fixed over the left-panel area below the tab strip, only visible when body[data-sb-tab="properties"], and the right-side slot stays at 0 width so the canvas always gets the full remaining width. Removed the now-redundant "Toggle right column" toolbar button.
- Slimmer header. The top toolbar dropped from 56px to 44px to match the original Site Builder. Publish + Save Page were chunky two-line buttons that crowded the bar — they're now sized to match the My sites pill (5px×12px padding, 12.5px font, single line). Toolbar icon-only buttons (toggle file mgr, undo/redo, etc.) tightened to 4px×8px so the slimmer header doesn't crop them. Left-panel + canvas top offsets adjusted from 56 → 44px to match.
- Right properties panel now auto-shows / auto-hides instead of staying open all the time. Previously default-open, which squished the canvas and showed an ugly divider + scrollbar during regular template browsing. Now the panel stays closed until the user clicks an element on the canvas — then it slides in with that element's Content / Style / Advanced tabs — and slides back out when nothing is selected. Hooked by wrapping
Vvveb.Builder.selectNode() (the single chokepoint VvvebJS uses for both click-on-canvas and programmatic selection) so the auto-toggle fires consistently in every selection path. Manual toggle via the toolbar's right-column button is honored as a sticky session preference: once the user clicks it, the auto-behavior backs off so it doesn't fight them. Body/html selections (which VvvebJS does internally for coarse focus changes) are filtered out so the panel doesn't flicker on every canvas click.
- Removed the bundled "AI assistant" button that VvvebJS auto-injected into the inline element toolbar (the wand-icon action that showed a tooltip "AI assistant"). It was wired to a feature we don't ship and just confused users when they clicked an element. Done by commenting out
<script src="libs/builder/plugin-ai-assistant.js"> in _editor-core.html. The plugin file itself is left in the vendored libs folder so future VvvebJS upgrades don't break the diff. No other code referenced it.
- Element properties panel now opens by default on the right. Clicking an element on the canvas already populated the right-panel's Content / Style / Advanced tabs (this is a built-in VvvebJS feature) — but the panel was being closed on init via
Vvveb.Gui.toggleRightColumn(false), so users had to discover the toolbar's "Toggle right column" button before they could edit anything. With our custom 4-tab left panel (Pages / Templates / Elements / Stock) covering up VvvebJS's old in-left-panel Properties tab, users had no way to reach element editing on click. Fix: dropped the init-time toggleRightColumn(false) and pre-flipped the toolbar toggle's active / aria-pressed="true" state so the button reads "on" from the start.
- Stock swap — slot dimensions are now actually preserved. The previous "preserve dimensions" pass copied
width/height HTML attributes onto the new node, but most templates style images via CSS (width: 100%; height: auto on a flexbox child) — so swapping a square 800x800 image for a portrait 4000x6000 stock photo let the new image's natural intrinsic ratio take over and the slot reflowed to portrait. Real fix: before swapping src, measure the old element's rendered getBoundingClientRect() and pin the box's shape with inline aspect-ratio + object-fit: cover (also width/height in px and max-width: none). The new image fills the same visual slot at the same shape, content cropped to fit, and the surrounding layout doesn't move a pixel. Same logic applied to the cross-kind path (image ↔ video). Idempotent — won't override styles the author already set explicitly.
- Stock swap now preserves the original's dimensions and styling. When you replace an image (or swap an image for a video) on the canvas, the new media now inherits every attribute of the one it replaces —
width, height, class, inline style, id, anything else — so a 4000x6000 stock photo dropping into a styled hero slot keeps the slot's dimensions instead of blowing up to natural resolution and breaking the layout. Same-kind swap was already in-place (so it kept everything by definition); the cross-kind path (img ↔ video) now copies attributes onto the new node before replaceWith(). The only attributes deliberately NOT copied are the ones that came from the new media (src, srcset, poster, alt) and the temporary highlight marker the drag-target outline used.
- Stock tab — second pass of fixes after the first user test:
- Click on a stock card now actually replaces a selected image instead of silently dropping nothing. Previously, when an image element was selected on the canvas, the code did
selectedEl.appendChild(node) — but <img> is a void element and can't have children, so the insert silently failed. Rewrote the placement logic: if you have an existing image or video selected, clicking a same-kind stock card swaps the src (and alt/srcset) in place — so all the surrounding wrapper classes, sizing, and layout stay intact. Mixing types (image → video or vice versa) does a replaceWith(). Clicking on a non-media element drops the new node after it as a sibling so it lands beside what you pointed at instead of getting buried in the child list. Empty-canvas behaviour (append to body) is unchanged.
- Drag-and-drop now tracks the actual drop target. Dragging a stock card over an existing image and releasing now replaces it. Added an iframe-body
dragover listener that records the element directly under the cursor, paints a 2px dashed accent-green outline on it as a drop indicator, and passes that target into the same swap/replace path as click. Drop on a section / div → new media is appended next to it. Drop on empty canvas → still appends to body. The outline is cleared on dragleave (when leaving the iframe entirely) and on dragend.
- Stock pane now fills the panel. The Pages tree (Index / content / css / js / …) was visible underneath the Stock pane because the
#filemanager hide-rule in editor.css only listed templates and elements — not the new stock tab. Added stock to that selector so the tree is hidden in stock mode, and changed .stock-pane from height: 100% to flex: 1; min-height: 0 so it fills the remaining vertical space the same way .tpl-pane does. Cards now fill the whole panel from search bar to the bottom Pexels attribution line.
- Tab labels no longer jammed against the canvas edge. With four tabs (Pages / Templates / Elements / Stock) sharing the same panel width that three used to, "STOCK" was sitting right on the right edge of the left panel. Tightened the tab typography: font-size 12 → 11, letter-spacing .04em → .02em, horizontal padding 14 → 8, gap 6 → 5. Now every tab has comfortable breathing room and "STOCK" doesn't look like it's about to run into the canvas.
- Stock tab — three first-day fixes:
- Cards no longer overlap each other. The grid was using
aspect-ratio: 4/3 on .stock-card with <img height: 100%> inside. Some browsers/contexts let the natural intrinsic image height win over the box's aspect ratio (especially with portrait video thumbnails), pushing each card taller than 4:3 and bleeding the bottom of one card under the top of the next. Switched to the bulletproof padding-top: 75%; height: 0 box and absolute-positioned the <img> / <video> over it with width: 100% !important; height: 100% !important; object-fit: cover. Now every card is a clean 4:3 tile that crops the image, with zero vertical bleed regardless of the source media's natural aspect ratio.
- Click-to-insert now also dismisses the welcome overlay. When the canvas was empty, the "Welcome — what would you like to build today?" card was covering the canvas, so clicking a Stock card silently inserted the image behind the welcome card. The toast said "Image added to canvas" but the user saw nothing change. The insert flow now calls
window.SB_DISMISS_WELCOME() (the editor's existing dismiss helper) before appending so the inserted image is the first thing the user sees on the now-revealed canvas.
- Closed the active-tab underline bleed. The accent-green underline beneath the active tab appeared to extend into the canvas area on some renders. Added
overflow: hidden to .pt-tabs and to the panel surface in stock-tab mode so nothing in the tab strip can paint past the right edge of the left panel.
- Added a Stock tab to the editor — browse free Pexels photos and videos right from the canvas. No more leaving the editor, finding a photo elsewhere, uploading it, then coming back. The new fourth tab on the left panel (Pages | Templates | Elements | Stock) opens a dedicated stock-media browser:
- Search at the top — type "ocean", "team meeting", "neon sign" — debounced 300ms so each keystroke isn't a request.
- Images / Videos toggle pills — flip between photos and short video loops.
- Category chips — Trending (default), Wallpapers, 3D Renders, Nature, Architecture, Minimal, Business, Technology, People. Click a chip to swap the current query.
- Two-column grid of thumbnails with the photographer credit visible on hover and a small "Video" badge on video cards.
- Click a card to insert the full-resolution image (or
<video controls>) into the canvas — drops at your current cursor position if you have something selected, otherwise at the bottom of the page. Triggers VvvebJS's undo/save cascade so it's part of the next save.
- Drag a card onto the canvas — same insertion path, native HTML5 drag.
- Load more button at the bottom for paginated browsing — preserves what's already loaded so you don't lose your place.
- Photographer attribution is shown on every card and links back to their Pexels profile, satisfying the Pexels TOS the old stock page didn't comply with.
- The Pexels API key never reaches the browser. Requests go through a new
/app/sitebuilder/api/stock/{search|curated} server proxy that adds the auth header on the server, normalizes the response shape, and applies a 5-minute private cache to keep us under Pexels' rate limit. The old Site Builder hardcoded the key client-side; we don't repeat that.
- Simplified the publish flow. Hitting Publish in the editor used to pop a "Publish your latest changes?" confirmation dialog before doing anything — an unnecessary speed bump for a non-destructive action (the button itself is the confirmation). It now goes straight to publishing: the button shows a spinner + "Publishing…" while the API call runs, and when it succeeds you get a beautifully styled success card instead of the old "You're live! 🎉" sheet. The new card has a clean accent-green check-circle icon (no emoji), a soft radial halo behind it, the title "Site published", a one-line body, and two buttons: Continue editing (ghost style) closes the card; Open in new tab opens the live URL in a new window. No site URL text is shown — the button does the work, and the URL itself is noise the user doesn't need to read out. Backdrop and Esc both close the card.
- Fixed three billing-page bugs after upgrade:
- "Next charge on Apr 30, 2026" showed the LAST day of the current cycle instead of the actual charge date.
current_period_end is stored as the last second of the cycle (e.g. 2026-04-30 23:59:59); Pancho bills on the 1st of every month, so the real next-charge date is +1 day. Added a dedicated format_plan_next_charge_date() helper that does the +24h shift, and switched the desktop + mobile billing views from format_plan_period_end() to it. Now reads "Next charge on May 1, 2026".
- "Free trials" section + "Site Builder · 0m left · Upgrade" still showed up on the billing page after a user upgraded to Premium. Belt: the section is now hidden when
plan.status='active' && plan.plan_id!='free' && !plan.paused_at. Braces: the actual fix is in /api/plan/upgrade — on successful upgrade, all of the user's demo / expired / cancelled install rows for premium apps are promoted to type='premium', status='active' in a single UPDATE, so has_app_access() and the marketplace card stop treating them as expired demos. Backfilled in place for the 1 already-upgraded user who was caught between releases.
- "Upgrade to Premium" header showed even when the user was already on Premium (the page below correctly read "You're on Premium" — the header just contradicted it). Now the header switches to "Your Premium plan" / "You're already on Premium — every app's unlocked." when
$isPaidPlan. Same fix applied on mobile (header → "Your plan").
- Cleaned up stale
app_installs rows where type='free' but the app had since been promoted to tier='premium' — Site Builder rows for two seeded users still claimed type='free' from before the tier change, which would have caused weird mismatches between the install record and the apps table. Updated 2 affected rows in place (type='free' → 'premium') and updated [seed.php](seed.php) so freshly-reset databases create the rows correctly going forward.
- Toned down the Premium label on the App Marketplace cards. Was a loud accent-colored heading at 18px / weight 800 — same size as the "Free" price next to it — which made every premium card scream "Premium" louder than the app name. Now it's a subtle uppercase metadata chip (11px, muted text color, 1px border, soft secondary background), reading like a tier tag rather than a price. Same restyle applied on mobile.
- Added a 4-demo cap on the platform's "Try Free" flow. Previously a user could restart the same app's demo as many times as they wanted (or end the demo, which also wiped the count, allowing infinite resets). Now
app_installs.demo_count (new column) increments on every demo start, and case 'demo': in extensions/billing/api/apps.php rejects the 5th attempt with HTTP 402 + a friendly message: "You've already used your 4 free demos for {app}. Upgrade to Premium to keep using it." The cap is configurable via the platform-wide demo_max_attempts setting (defaults to 4). The end-demo action now sets status='expired' instead of deleting the row, so the count survives between demo cycles. Verified end-to-end with the dev server: attempts 1–4 return 200 + redirect to the app, attempt 5 returns 402 with code: 'E_DEMO_LIMIT_REACHED' and redirect: /upgrade?app=…&reason=demo_limit.
- I tested the user's "Try Free taking me to upgrade" report end-to-end and the demo-restart API actually works fine — both attempts after force-expiry returned
success: true and /app/sitebuilder loaded. Most likely the user clicked the Upgrade to use button next to "Try Free" by mistake (both render side-by-side after expiry). Worth a future UX pass to make Try Free more visually distinct.
- The My Sites page now lays out 5 cards per row at desktop widths (was a
minmax(280px, 1fr) auto-fill that capped at 4 with a 1280px max-width). Drops to 4 columns under 1500px, 3 under 1180px, 2 under 860px, and a single column on phones — so the grid stays comfortable at every breakpoint without ever clipping a card mid-thumbnail.
- The My Sites page now opens with the platform sidebar collapsed by default, matching the editor's behaviour. You can still expand it any time with the sidebar toggle. Bouncing between My Sites and the editor no longer makes the sidebar flicker open and shut.
- Removed the dark-mode toggle from the editor's top toolbar — the editor is dark-themed by design and the toggle didn't visibly do anything for users. One less mystery icon at the top.
- Tightened the active-tab background on the four icon tabs (Sections / Components / Properties / Styles) at the top of the Elements panel — the soft accent-tinted square was too tall + too wide; reduced padding from
8px 12px to 4px 8px and dropped the corner radius from 8 to 6 so the tint hugs the icon instead of looking like a giant button.
- Removed the three-dots menu from the cards on the My Sites page entirely — clicking a card now just opens it in the editor (which is what almost everyone wanted to do anyway). Site management moved into the editor itself.
- Made the editor's site-name pill a real site-options menu. Click it (top right of the toolbar) to get a clean dropdown with Rename, Duplicate, and Delete site. Each one hits the same
/api/sites/rename / /duplicate / /delete endpoints the dashboard used to use, so behaviour is identical: rename updates in place, duplicate copies the draft and opens the new copy, delete confirms then drops you back at the dashboard. The menu auto-closes on outside click or Esc and is right-aligned under the pill so it can't blow past the iframe edge.
- Fixed the three-dots menu on the My Sites page. Clicking the three dots on a site card now opens the action sheet (Open in editor / Rename / Duplicate / Delete site) instead of doing nothing — an old
event.stopPropagation() on the menu button was blocking the card-level click handler from running.
- Added inline rename to the editor toolbar. The site name shown at the top right is now a clickable pill ("🌐 My site name ✎") — click it to rename the site without leaving the editor. Saves via the same
/api/sites/rename endpoint the dashboard uses, and the displayed name updates instantly without a page reload.
- Removed the duplicate "Site Indicator" + "Site Management cog" pair in the editor's top toolbar — they both opened the same flyout. Replaced with the single click-to-rename pill above. The Site Management flyout itself stays in the markup (Clone / Change Domain / Delete / Notes are still wired) but is no longer reachable from the toolbar; rename — by far the most common action — is now one click instead of three.
- Polished the Elements panel with a real cosmetic facelift to match the rest of the dark editor theme. Top icon tabs (Sections / Components / Properties / Styles) now use rounded pill buttons with a soft accent-tinted active state and an accent-green icon highlight; Sections / Page Sections sub-tabs use a clean accent-green underline; the search input grew to a 34px tap target with the dark form style and accent focus ring; the +/- zoom controls match the panel surface; the "All Design Styles" select now has a custom dark chevron icon in place of the default OS triangle; the category accordions (Hero, Essentials, Features, …) get a subtle accent-tinted background when open and a soft hover highlight; section thumbnail tiles now lift on hover with an accent border + shadow + grab cursor, and their captions use the editor's display + monospace fonts. All cosmetic — zero changes to the underlying VvvebJS markup, click handlers, or drag-and-drop behaviour. Scoped tightly to
body[data-sb-tab="elements"] so nothing leaks into other tabs.
[2026-04-30]
- Owner-aware caching on published sites. The published-site routes (
/p/{uuid}/sitebuilder/site/… and custom domains) now choose the Cache-Control header based on who's viewing: the site owner gets no-store, no-cache, must-revalidate, max-age=0 for every file type — HTML, images, video, everything — so a freshly published change shows up the moment they refresh the live URL. Visitors get normal caching: HTML revalidates on every request (public, no-cache, must-revalidate), CSS/JS cache for 1 hour, images/fonts/video cache for 1 day. Owner detection is via $_SESSION['user_id'] matching the URL's owner UUID (or the custom domain's user_id row), so the same user account hitting the site from a logged-in tab gets fresh content while every other visitor on the planet gets a fast cache. Also strips PHP's auto-emitted Pragma/Expires headers from session_start so they don't conflict with the explicit Cache-Control. Replaces the previous flat 5-minute cache that confused owners during the rapid edit-publish-refresh loop.
- Replaced the editor's custom upgrade modal with the platform's core upgrade sheet. The Site Builder was hardcoding "Upgrade to Premium / Premium turns on publishing, custom domains, and priority support across every Pancho app / $9.99 / month" — but the actual price is whatever's configured in the platform's plan config, NOT $9.99. The custom modal also reimplemented the no-card-on-file capture flow and the charge step, duplicating logic the platform's
PS.requestUpgrade() already does correctly. Now sbStartUpgradeFlow() is a 6-line wrapper that calls top.PS.requestUpgrade({ planId: 'premium', returnAppId: 'sitebuilder', onSuccess: …}) — the platform fetches the live plan name + price from /api/plan/request-upgrade, opens its standard sheet in the top window (consistent across every Pancho app), handles the no-card flow, and calls back our onSuccess to flip SB.isPremium and re-trigger the publish. Net change: 60 lines deleted, 23 added.
- Exposed
PS as window.PS in core/assets/js/app.js. Top-level const PS = {…} creates a lexical binding only — window.PS was undefined, so child iframes (like the Site Builder editor running inside the platform shell) couldn't reach back via top.PS. Added window.PS = PS; at the bottom of the file. Existing in-page callers are unaffected (they reference PS.toast(…) etc. by lexical name).
- Found and fixed the real "Use this design" bug (and the matching "Upgrade to publish" bug). Both buttons share the same modal component (
.sb-modal-wrap), and both were doing nothing for users — but a programmatic puppeteer click DID succeed. Root cause: a CSS specificity inversion in editor.css. The base rule was body.sb-editor .sb-modal-wrap, .sb-modal-wrap { opacity: 0 } (specificity 0,2,1,0), and the activation rule was .sb-modal-wrap.show { opacity: 1 } (specificity 0,2,0,0). The base rule WINS, so adding the .show class never made the modal visible — it stayed at opacity: 0 despite being display: flex with z-index: 99999 and a full-viewport rect. The modal was technically clickable through the DOM (which is why programmatic tests passed), but invisible to humans. Removed the redundant body.sb-editor qualifier from the base rule so the .show rule now wins by source order.
- Fixed Use this design — the apply API was working perfectly server-side (verified end-to-end with curl: POST /templates/apply returns
{"success":true} and the template files land in the user's draft folder), so the bug had to be in the post-success page reload. Switched the reload from window.location.reload() (reloads only the inner editor iframe) to target.location.href = '/app/sitebuilder/editor?site=…' on the top window. Some browsers refuse to reload a same-origin iframe from inside it without a fresh user gesture in nested-iframe contexts, which is why the old reload silently no-op'd. Hitting the top window with an explicit URL navigation always works. Same fix applied to startFromBlank when it does need to wipe an existing template.
- Smarter Start from blank: if the canvas is already empty (the site has never had a template applied), the button skips the "are you sure?" modal entirely and routes the user straight to the Elements tab with a brief panel pulse + "Blank canvas ready — drag a section from Elements" toast. Only when there's an actual template to wipe does the confirmation prompt appear; after confirming, the editor reloads and lands on the Elements tab automatically (we set a
sb-open-elements-{siteId} sessionStorage flag before reload, and the editor consumes it on the next paint).
- The welcome card's Start from blank button reuses the same flow, so clicking it on a fresh site now visibly switches the panel to Elements (with a pulse) instead of "doing nothing" because the canvas was already blank behind the welcome.
- Fixed the editor hanging at load (round 2). The previous round's empty-canvas card had two root elements (
<style>html,body{…}</style> plus the placeholder <div>). The injection logic uses template.content.firstElementChild — so only the <style> was actually being inserted, the EMPTY_ID div was lost, and the MutationObserver kept re-firing because the placeholder it expected to find was never there. The observer kept appending <style> tags forever, locking the main thread (Chrome's "Page is busy" dialog). Removed the standalone style tag — the placeholder div now covers the full viewport via position: fixed; top:0; left:0; right:0; bottom:0 with its own dark gradient, which gives the same visual result without the second root element.
- Fixed the editor hanging at load. Root cause was the dev-mode error toast I added a few iterations back — its
document.body.appendChild(el) call ran synchronously, but the script lives in <head> so document.body was still null early in the load. That made the toast itself throw, which fired window.onerror again, which re-entered the toast, which threw, looping forever and freezing the page. The error handler now defers DOM access until DOMContentLoaded, wraps every step in try/catch, and never throws back into the handler.
- Reverted the "Use this design" + "Start from blank" flows to a simple API succeeds →
window.location.reload() pattern. The clever in-place iframe-swap was fighting nested-iframe quirks and adding bugs without obvious upside; a single editor-frame reload after the apply API call lands you back on the freshly applied template every time. (Confirmation modal stays — only the "after confirm" plumbing was simplified.) The custom canvas loading-overlay helpers (sbShowApplyOverlay / sbHideApplyOverlay) are no longer needed and were removed.
- Fixed a white strip at the bottom of the empty canvas. The placeholder card was sized with
min-height: 90vh, which left a 10% gap at the bottom showing the iframe's default white body. Switched the card to position: fixed; inset: 0 and seeded the canvas iframe's <html><body> with background: #0b0d10 so the dark surface covers the entire canvas, edge to edge.
- Beefed up the "Use this design" confirm modal: it's now styled visible at append-time (
opacity: 1 inline, no rAF dependency) and every interaction is logged — modal-opened (with computed opacity / display / rect), backdrop click, Cancel click, OK click, Escape press, and final close-with-value. If the apply still feels like nothing happens, the console will tell us exactly which event fired (or didn't) instead of guessing.
- Polished the empty-canvas placeholder card. The wording was misleading ("Open the Sections or Components panel" — those panel names don't exist anymore) — it now reads "Open the Elements tab on the left and drag a section onto the page", which matches the actual UI. The card itself was also a bright-white slab in the middle of an otherwise dark editor; it's now styled to match: dark panel surface with the editor's accent-green icon tile, soft accent gradients on the canvas background, and the same monospace "drag & drop to build" caption used elsewhere.
- Cleaned up the Describe-your-site form. The currency dropdown now sits beside the "Or type your own industry" input on the same row (2:1 grid) instead of taking a full row of its own. The whole Save draft / Load draft / "Draft saved a moment ago" auto-save pipeline is gone — generating a prompt is fast enough that drafts were just clutter; you fill it in, hit Generate, copy the result, done. The Open Claude button has been replaced with a Share button that uses the OS native share sheet when available and falls back to copy-to-clipboard everywhere else, so the prompt isn't tied to one specific AI vendor.
- Added a
console.log trace to sbConfirmApply so when a template Use modal opens, the console now records "[SB] sbConfirmApply: opening modal for {id}" plus the modal's bounding rect — if the modal is rendering off-screen or its rect is zero, that's now visible.
- Reordered the Elements tab section list so Hero is first, with Essentials right after — the order now reads Hero → Essentials → Features → Payments → Forms → About → Team → Testimonials → Gallery → Counter → Step Box → Pricing → Call To Action → FAQ → Footer.
- Gave the Elements tab a small dark-theme facelift to match the rest of the editor: panel and accordion backgrounds use the editor's
--sb-panel ink, the sub-tab indicator switches from default Bootstrap blue to the editor's accent green, search and select inputs use the dark form style with the matching focus ring, and section item cards pick up the accent border on hover. All scoped to body[data-sb-tab="elements"] so the underlying VvvebJS markup is untouched.
- Added a dev-mode error toast: any uncaught JS error or unhandled promise rejection now pops a red banner at the bottom of the editor with the exact error message and the file/line. Replaces the silent failures that had us guessing why "nothing happens" — if "Use this design" still does nothing, the toast will show the actual reason.
- Fixed the "Toggle left column" toolbar button — clicking it once would hide the left panel, and a second click did nothing because our
--builder-left-panel-width override used !important, which made VvvebJS's togglePanel() always read the original computed value (420px) and re-enter the "hide" branch. Removed !important (the source-order cascade is enough), so toggle now hides on first click and restores on second.
- Restored the Sections and Components panel as a third "Elements" tab in the left panel, alongside Pages and Templates. Click the new Elements tab to drag any of VvvebJS's pre-built sections, blocks, and components onto the canvas. Previously the panel was hidden entirely by a
display: none !important override left over from the original Cloud Design mockup.
- Renamed the templates URL to the simpler
/app/sitebuilder/templates/{id}/… and dropped the legacy /templates-asset/… alias entirely (per dev-mode policy: no fallbacks).
- Removed the silent 6-second safety timeouts from the apply / start-from-blank flows. If the canvas iframe doesn't fire its
load event the loading overlay now stays up so you SEE the problem instead of having it papered over. Apply errors (no template found, API failure, missing iframe element) now pop a red alert modal with the exact reason instead of silently returning.
- Hardened the "Use this design" template-apply flow. The canvas iframe is now navigated by a direct
iframe.src = … assignment instead of via VvvebJS's loadUrl() helper (which had an if (iframe.src != url) guard that could edge-case no-op and leave the canvas showing the old template). Same hardening applied to the Start-from-blank flow.
- Silenced the
unload is not allowed in this document console violation by emitting Permissions-Policy: unload=(self) on the editor responses, so VvvebJS's iframe-navigation tracker registers cleanly.
- Renamed the templates URL from
/app/sitebuilder/templates-asset/{id}/… to the simpler /app/sitebuilder/templates/{id}/… — the old -asset suffix only existed to avoid a route collision that never actually applied. The legacy URL still routes to the same files for backwards compatibility, so any bookmarks keep working.
- Welcome screen buttons now actually do something visible when clicked. Browse templates dismisses the welcome card, scrolls the templates panel to the top, briefly pulses the panel with an accent ring, and pops a small "Pick a design from the panel on your left" toast — previously the templates panel was already visible behind the welcome, so the click felt like nothing happened. Start from blank now triggers the actual blank-canvas flow (with its confirmation prompt) instead of just dismissing the welcome card silently.
- The editor now opens inside the Pancho platform shell instead of taking over the whole window. The platform sidebar stays visible on the left (collapsed by default for maximum canvas room) so you can jump to Home, Apps, Billing, or any of your other Pancho apps without leaving the editor. Click the sidebar's expand toggle any time to see the full nav. Authoring still feels full-screen — the editor itself fills everything to the right of the (collapsed) sidebar.
- Removed the "Website Apps" button (and the slide-out panel it opened) from the editor's top toolbar — it pointed at an integration that wasn't being used and only added clutter.
- Replaced the Pancho logo in the editor's top-left corner with a clean "← My sites" back link, so there's an obvious one-click way out of the editor and back to the dashboard from anywhere in the canvas.
- Made the "Loading {template name}…" canvas overlay much more visible while a template is being applied — it now uses a solid backdrop instead of a barely-there transparent layer, so you can actually see something is happening during the swap.
- Polished the Describe-your-site form. Industry: the "Or type your own" box is always visible under the chips with its own helper label, so non-technical users don't have to guess where to type. Currency: now a compact, narrow dropdown instead of a full-width field — it's just a currency picker, not a hero. Social handles (Instagram, Facebook, X, TikTok, YouTube, LinkedIn): each input now shows the URL prefix (
facebook.com/, x.com/, etc.) as a fixed label inside the field, so users only type their handle / page name. Voice & tone: the three options are now full cards with one-line descriptions explaining when to pick each — no more guessing what "Bold" means.
- Fixed an issue where applying a template (or starting from a blank canvas) could leave the editor looking like it had frozen — the canvas now shows a tidy "{template name} is loading…" overlay that clears automatically when the new design is ready, with a 6-second safety net so it can never get stuck.
- Reworded the toolbar magic-wand tooltip and the in-editor overlay header so it's obvious the feature builds a prompt you paste into any AI, not a fully-built website. The tooltip now reads "Generate a prompt to build your site with AI" and the overlay reads "Generate a website prompt for AI" with a one-line subtitle.
- Fixed the editor's chrome alignment. The toolbar's Publish and Save Page buttons were being clipped at the bottom by the canvas, the templates panel had a thin gap above it, and the canvas itself was extending under the panel. All three were the same root cause — a CSS variable scope issue — and now the toolbar, panel, and canvas line up cleanly.
- Picking a template from the panel now shows a "Loading {template name}…" animation on the canvas while the new design is being applied. Previously the canvas just sat there during the swap.
- Moved "Describe your site" from a big button at the bottom of the templates panel to a small magic-wand icon in the editor's top toolbar (next to Preview, Fullscreen, Layers, Media). Hover for the tooltip; click to open the prompt generator over the canvas. Reclaims the space at the bottom of the templates panel for actual templates.
- Fixed the in-editor "Describe your site" overlay — it was failing to load with a "127.0.0.1 refused to connect" error because the platform's security policy was blocking iframes. Now the overlay loads the prompt generator correctly and fits exactly to the canvas area (no longer overlapping the templates panel on the left).
- Removed the standalone "Describe your site" entry from the app sidebar. The toolbar icon inside the editor is the single entry point.
[2026-04-29]
- Cleaned up the "My sites" page header. Dropped the breadcrumb line, the explanatory paragraph, and the status toolbar so the welcome heading "Hi {name}, what are we building today?" stands on its own. When you have sites, a small site count appears under the heading and the "New site" button stays in the top right.
- The empty "no sites yet" message is now a card the same size and shape as a real site tile, sitting in the grid with a soft accent gradient — no more giant centered panel below the page. Click the card to start your first site; once you have one, the card disappears.
- Renamed the "Prompt forge" feature to "Describe your site" — same magic prompt generator, but a name a non-technical user can act on. Moved it from the editor's top toolbar to a pinned button at the bottom of the templates panel.
- Clicking "Describe your site" inside the editor now opens the prompt builder right where the canvas is, instead of opening a separate page in a new tab. The Pages | Templates panel stays put on the left so you don't lose your place. Press Esc or "Back to editor" to return to the canvas.
- Removed the placeholder "Browse marketplace" and the disabled "Describe your site (coming soon)" links from the bottom of the templates panel — they were noise and there was no marketplace behind the link. Templates list now uses that vertical space for actual templates.
- Fixed the editor's top toolbar: the Publish and Save Page buttons were being clipped because the toolbar was too short. Bumped it to 56px so every button shows in full, and shifted the side panels and canvas down to match.
[2026-04-28]
- Publishing a site now requires an active premium plan. Free-plan users can still build, edit, and preview sites — when you press Publish, a one-tap upgrade sheet pops up so you can start Premium without leaving the editor.
- Added "Prompt forge" — a guided, one-page form that turns a few answers about your business (name, industry, sections you want, contact info) into a long, detailed prompt you can paste into Claude (claude.ai) to generate a complete one-page website. Pick from 20 ready-made section cards (hero, about, pricing, FAQ, gallery, booking form, map, social bar, and more), choose your tone, hit Generate, then click Open Claude. Drafts save automatically in your browser so you can come back to a half-filled form anytime.
- The generated prompt instructs Claude to build the site with inline-only styles and no shared rules — that way every element pastes into the Site Builder editor as its own independently editable piece.
- Added a "Prompt forge" button in the editor's top toolbar so you can jump from the editor straight to the prompt generator (opens in a new tab so your draft stays open).
- Fixed the editor's top toolbar — Publish, Save Page, and the new Prompt forge button no longer get clipped by the canvas underneath. The toolbar is now tall enough for full buttons and the panels and canvas shift down to match.
- Clicking "Start from blank" on the welcome screen now confirms with a small toast so it's clear the action took effect (previously the welcome dismissed silently and felt like nothing happened).
[2026-04-23]
- Site Builder is live. Drag-and-drop editor with 76 starter templates, multiple sites per account, and one-click publish.
- Added a beautiful Templates panel in the editor — browse, search, filter by category, and one click swaps your site to the chosen design.
- "My sites" dashboard shows all your sites with live thumbnails, status badges, and quick rename / duplicate / delete.
- Publish writes a clean static copy. Share it at a platform preview link or point your own domain at it.
- Opening the editor on a phone shows a friendly "use desktop" prompt with a "Continue anyway" option.
Internal
- Vendored the VvvebJS editor under
apps/sitebuilder/editor/ and bundled all 76 templates under apps/sitebuilder/templates/.
- New
sites table; new API surfaces for sites, drafts, uploads, templates, publish, and dynamic component loading.
- Added four static passthroughs in
core/index.php: /app/sitebuilder/editor-assets/, /app/sitebuilder/templates-asset/, owner-only /app/sitebuilder/drafts/, and public /p/{uuid}/sitebuilder/site/{slug}/.
- A fetch shim in the editor view retargets VvvebJS's hardcoded
save.php / upload.php / scan.php calls to the new platform endpoints and attaches CSRF + site id.
[2026-04-20]
- Demo and subscribe now work out of the box — finished migrating the app to Pancho's UUID identity model so Try Demo no longer errors with 'We could not start the demo'.
Internal
- Added the
tenant_members table to schema.sql (backed by the platform's _per_user_app_bootstrap_owner() hook).
[Baseline] — 2026-04-16
- Existing feature set captured as the baseline. Subsequent changes will be prepended above this entry.