Changelog
All notable changes to the proofing & approval app. Format based on
Keep a Changelog. Dates are 2026.
[Unreleased]
_Add new changes here as they land. Move them into a new dated version when you cut a release,
and add a plain-English summary to CHANGELOG-PUBLIC.md at the same time._
Fixed
- Outbound email restored (production) — notifications stopped being delivered after 2026-07-03 while the queue still reported success. Cause was environmental, not code: DreamHost's unauthenticated
mail()path began silently discarding messages, andmail_send_now()treatsmail()returningtrueas sent, so 22 rows were markedsentand never arrived (last_errorempty, nothing to alert on). Fixed by switching productionmail.methodtosmtpagainstsmtp.dreamhost.com:587(TLS, authenticated asnotifications@priflow.org).smtp_send()raises on protocol errors, so future rejections land inemail_queue.last_errorand flip the row tofailedinstead of vanishing. The 22 undelivered rows were left as-is rather than requeued (stale content).
Changed
- Changelog split in two —
/changelognow showsCHANGELOG-PUBLIC.md(plain-English highlights) to everyone; this full technical log is only viewable by signed-in org members via/changelog?full=1. Keeps implementation detail (schema, fixed bugs, internals) off the public site.
[1.2.0] — 2026-07-03
Final approval sign-offs, external client reviewers, automatic org-admin manage rights, and a
dead-schema cleanup — all confirmed live in production.
Added
- Landing page redesign (
landing/index.html) — repositioned around "open-source proofing for small teams priced out of premium tools". New premium dark design (Inter + Instrument Serif, animated hero, scroll-reveal sections, cursor-tracking card glows), an interactive hero demo where visitors can pin comments on mock artwork and approve the proof (with confetti), a cost-comparison table vs paid proofing SaaS, an open-source/self-hosting section with mock terminal, a pricing section (free self-hosted + "done-for-you setup" contact card), and an FAQ. All "Open the app" CTAs replaced with Download on GitHub (repo URL is a placeholder — updategithub.com/priflow/priflowwhen the repo exists); live-demo link to proofs.priflow.org kept as secondary CTA. Contact mailto useshello@priflow.org(placeholder — swap for a real inbox). - External reviewer invites — invite a named outside client by email (People tab) to review one proof without joining the org (
invitations.kind='proof', previously unused). They accept via/join(7-day, single-use link), get a guest account (org_id NULL), and land straight on the proof. Access = their reviewer participant row; Revoke sets the newparticipants.revoked_at(migration013_external_reviewers.sql) — history kept, access cut, re-checked on every API and file request so a live session can't outlast a revocation. Externals see a redacted workspace: no team chips, no activity log, no approver names or final-approval notes (stripped server-side). Their dashboard lists only invited proofs. Logout now also drops per-proof session identities. - Final approval stage — once a file's amends are all resolved and it's closed, the creator (or a proof admin) can send it to a chosen org member for a formal sign-off. The approver gets an email, opens the proof signed in, and either approves or sends it back with amends needed (optional notes) — the latter reopens the file so the normal amend cycle restarts. Requests are pinned to the version they were made against: uploading a new version supersedes a pending request, and reopening a file withdraws it. New proof pill "Awaiting approval" when every file is closed but a sign-off is still pending; file tabs show ⏳ (awaiting) and ★ (final approved) badges. Unanswered requests get one reminder email after 3 days (config
mail.approval_reminder_days), and every step lands in the activity log. New mail eventsapproval_requested/approval_decided; migration011_final_approval.sql. /guidepage — an in-app how-to, rendered fromGUIDE.md(same renderer as/changelog, now generalised tomarkdown_to_html()), linked from the footer.- Footer now shows on the dashboard (previously hidden there along with every other fullbleed page) so the Guide/Changelog links are reachable —
manage.php/review.php/users.phpstay footer-free, unchanged. - Tag someone in to review — separate from proof admins. Creator/admins can tag an org member on a proof's People tab to flag them and send a "you've been tagged to review" email. Purely additive — it does not grant or restrict access; any org member can already review any proof via its link, tagged or not.
- Dashboard "Reviewing" now includes tagged-but-not-yet-reviewed proofs — it's the union of proofs you've actually left an amend on *or* proofs you've been tagged in to review, so you see it before you've done anything, not just after.
Removed
- Dead-schema cleanup (migration
014_dead_schema_cleanup.sql): droppedparticipants.decision/decided_at(the removed per-reviewer verdict feature) andparticipants.guest_token(guest reviewers, superseded by login + external invites); narrowedproofs.statustoopen/awaiting_approval/closed/approved(dropping never-setchanges_requestedandarchived). Swept the matching dead code:add_reviewer(),get_reviewer_by_guest_token(), theguest_cookie_*()helpers, the uncalledadmin_totals(), the always-zero verdict counters inaggregate_proof_rows()/dashboard, the amend-"approved" remnants inproof_stats()/set_annotation_status(), orphaned.pill-changes_requested/.dot-changes_requested/.btn-changesCSS, and theadmin_passwordconfig key (README and go-live notes updated — they still described the old guest-link flow).
Fixed (pre-release)
- Deleting a user no longer erases their final-approval sign-off records — the
final_approvalsFK wasON DELETE CASCADE, silently deleting the very records the feature exists to keep. Migration015_preserve_approval_records.sqlsnapshots the approver's name onto each row (backfilled), makesapprover_user_idnullable, and relaxes the FK toSET NULL— matching howaudit_logandparticipantsalready preserve history.
Changed
- Org admins can manage any proof without being tagged — opening a review link as an org admin (or the proof's creator, or a tagged proof admin) now lands on the manage view with full rights.
?as=reviewerforces the plain reviewer view; a pending final approval naming you keeps you in the reviewer view so you can decide. Manage actions are now audited under the signed-in account's name instead of always the proof creator's (fixes proof-admin actions being attributed to the owner). - Proof rollup now distinguishes a formal sign-off from a plain close: "Approved" only when every file carries a granted final approval for its latest version; all-files-closed without that reads "Closed" (new
closedproof status, migration012_proof_closed_status.sql, which also demotes pre-existing 'approved' proofs that never had a sign-off). - Removed the dead
verdictmail-event config key (the reviewer-verdict feature it referred to was already removed) and the matching dead audit-log entries.
Fixed
- "Close file" was closing every file in the proof — closing while viewing one file's version silently closed *all* files in the proof, since "closed" lived on
proofs.status(proof-wide) instead of per file. Now closing/reopening, the amend-blocking guard, and "upload a new version reopens it" are all scoped to the specific file (version_group), never affecting other files in the same proof. The dashboard status pill becomes a rollup — a proof shows Closed only once *every* file in it is closed. Renamed "Close version"/"Reopen version" → "Close file"/"Reopen file" throughout, since "version" was the source of the original confusion (a file can have several versions, all sharing one closed state). - Removed the dead
set_decision()/recompute_proof_status()functions (0 callers — leftover from the removed reviewer-verdict feature).
Migrations
009_reviewer_tags.sql—proof_reviewer_tagstable.010_file_status.sql—file_statustable (per-file closed state), with a backfill so any proof already closed under the old proof-wide system stays consistent under the new model.011_final_approval.sql—final_approvalstable +awaiting_approvalproof status.012_proof_closed_status.sql—closedproof status; demotes all-closed proofs without a full sign-off.013_external_reviewers.sql—participants.revoked_at(external reviewer grants).014_dead_schema_cleanup.sql— drops dead columns/enum values (run BEFORE the code update).015_preserve_approval_records.sql— approver-name snapshot +SET NULLFK onfinal_approvals.
[1.1.0] — 2026-07-01
Organisation workspace, invite-only accounts, and a reworked admin/reviewer permission model —
all confirmed live in production.
Added
- Organisation workspace — setup now creates an organisation (name + first admin). Admins manage the team from
/users(renamed "Team"). - Org invitations (
invitationstable) — admins invite members by email (or add directly). Accept via/join?token=…, which sets a password and creates the account. Pending invites can be revoked. - Proof admins — a creator can add other org members as admins of a specific proof (People tab → "Proof admins"), giving them the same manage rights as the creator on that proof only. Shows on their dashboard under "Admin of".
- Forgot-password reset —
/forgotemails a single-use, 1-hour reset link (/reset?token=…). The request page gives the same response whether or not the email exists (no enumeration). - "Reviewing" dashboard section — proofs you've added amends to, but don't own or admin, now show on your dashboard so you can find them again. A proof appears in exactly one section (owned > admin > reviewed) so it's never listed twice; reviewed-only rows correctly link to the review link, not the manage link, since you don't have manage rights on those.
- Versions column on the dashboard — shows the total number of versions uploaded across a proof's files, next to the existing file count.
/changelogpage — renders this file as a styled page, linked from the footer. The raw.mdfile stays blocked from direct access; footer (and the link) only show on account/auth-style pages, not on the fullbleed dashboard/manage/review workspace.
Changed
- Accounts are invite-only — open self-signup removed (
/signupredirects to login;allow_signupsconfig dropped). - Guests removed — reviewing requires an account. Any signed-in org member can review any proof via its link (no per-proof invite needed) — they just can't manage it unless also made a proof admin. Non-members are blocked from review links entirely (no external reviewers).
- Proofs and users are scoped to the organisation (
org_id).
Fixed
- Closed versions could still take new amends — adding an amend used to silently reopen a closed version. Now the server rejects new amends while closed (409, clear message), and the viewer disables click-to-add and shows "This version is closed…" instead. Replying to existing amend threads is unaffected.
Deployed
- Live on production (DreamHost) — email confirmed working end to end (
config.phpmail settings set, per-minute cron running with the correct PHP path, a real "new version" notification sent and delivered). Follow-up: confirm DKIM is enabled forpriflow.orgso deliverability holds up over time.
Migrations
007_organisations.sql— organisations + invitations,users.org_id/proofs.org_id; backfills the existing team + proofs into a founding org.008_password_resets.sql— password reset tokens.
[1.0.0] — Pre-launch (2026-06)
First complete build: a self-hosted, Ziflow-style proofing & approval tool in
PHP 8 + MySQL for shared hosting (DreamHost), with no Node/Docker.
Added — core proofing
- Proofs containing multiple files; image and multi-page PDF upload (drag-and-drop, 50 MB default cap).
- Pin-point amends — click anywhere on artwork or a PDF page to drop a numbered marker; positions stored as % so they're resolution-independent.
- Threaded comments / replies on each amend.
- Amend states: Open ⇄ Resolved (creator-controlled; see Changed for the simplification).
- Activity log / audit trail of every upload, amend, reply, status change, etc.
- Two link types per proof: a private manage link (creator) and a shareable review link (reviewers).
Added — versions
- File versioning — upload v2, v3 of a file into the same "slot"; viewer shows a version dropdown, defaults to the latest, and shows a banner on older versions. Amends stay attached to the version they were made on.
Added — accounts & access
- User accounts with admin and member roles; first-run
/setupcreates the first admin, then/login,/logout. - Self-registration toggle (
allow_signups) with a/signuppage. - Account settings (
/account) — users change their own display name and password. - Admin user management (
/users) — add, promote/demote, enable/disable, reset password, and delete users (with self / last-admin guards). - Dashboard (
/dashboard) split into My proofs and Tagged in; admins get an Everyone view. - Tag teammates into a proof (collaborators) via a People tab — tagged accounts see it under "Tagged in".
Added — reviewing
- Guest review links — reviewers need no account; just a name.
- Returning guests are remembered via a persistent per-proof cookie (90 days); "not you?" switch to re-choose.
- Sign-in-or-guest chooser on the review link — review under your account (recognised on return) or continue as a guest.
- Close version — the creator closes a version, enabled only once all amends are resolved; reopens automatically on a new amend or new version.
Added — email notifications
- Outbound email queue + cron worker (
cron.php), with retries. - Mailer supporting
mail(PHP/Exim),smtp(built-in, no PHPMailer dependency), andlog(dry-run tostorage/maillog/). - New-version notifications to reviewers, with a signed one-click unsubscribe / mute link. Other events (new amend / reply / verdict) are built and config-gated.
Added — branding & site
- Product name Primordial Oneness (configurable
app_name); footer tagline "From many eyes, one approval." - Marketing landing page (
landing/index.html) forpriflow.org, separate from the app.
Changed
- UI reworked to a full-bleed workspace: full-width upload bar → toolbar → viewer (~76%) with the comments panel (~24%).
- Viewer fit — "Fit" fits the whole page (binding dimension wins): tall PDFs fill height, wide banners fit width. Zoom "+" is capped at full column width.
- Reviewers can no longer cast a verdict (Approve / Request changes removed) — they only add amends and comments. The creator's "Close version" replaces the per-reviewer verdict.
- Amend states simplified to Open/Resolved (the separate "Approved" amend state was removed).
- PDF.js is self-hosted (
assets/vendor/pdfjs/) instead of loaded from a CDN. - Proof status now reads In review → Closed, controlled by the creator.
Fixed
- Tiny PDF / wide-banner rendering — viewer was boxed in a 760px container and wide images overflowed and clipped to the left; now full-bleed, centered, and fit-to-whole-page.
- Large file uploads failing — documented/raised PHP
post_max_size/upload_max_filesizeand added a clear over-limit message. - "Tag in" button off-screen in the narrow People panel (
<select>min-width trap). - Stale CSS/JS after deploys — assets now load with a
?v=<filemtime>cache-buster.
Performance
- Large PDFs —
file.phpnow supports HTTP Range (206), immutable caching + 304, and the viewer loads pages on demand (range requests). The 38 MB sample catalogue went from downloading ~38 MB to ~7.8 MB for page 1 (~80% less). Render DPR capped at 1.5 and a loading progress bar added.
Security
- Session cookies marked
HttpOnly,SameSite=Lax, andSecureon HTTPS. - CSRF tokens on all state-changing requests; output HTML-escaped throughout.
- Access-controlled file streaming — uploads live behind
storage/.htaccessand are served only throughfile.phpafter a permission check. - Passwords hashed with
password_hash(bcrypt); signed tokens for unsubscribe links.
Infrastructure / deployment
- DreamHost-specific go-live guide (GO-LIVE.md) — MySQL hostname (not
localhost), SFTP upload,phprclimits, panel/SSH cron, SPF/DKIM forpriflow.org. - Config target domain
proofs.priflow.org, email fromnotifications@priflow.org.
Database migrations
Run in order on an existing DB (a fresh schema.sql already includes all of them):
001_users.sql— accounts +proofs.owner_user_id002_guest_token.sql— returning-guest cookie token003_collaborators.sql—proof_collaborators(tagging)004_participant_user.sql— link a reviewer to an account005_email.sql—email_queue+participants.notify_muted006_amend_status.sql— amend states reduced to open/resolved
Notes / not yet built
- Forgot-password reset email (the mail plumbing exists; small add when wanted).
- Other notification events are wired but off by default (one config flip each).
- Server-side PDF→image rasterising (the biggest perf lever) needs Ghostscript / a VPS — intentionally avoided to stay on shared hosting.