Scope Ecommerce down to Lib helpers; prune stale Done entries in ISSUES.md

Replace the single Ecommerce entry with three composable Lib\ pieces
(Money, Cart, payment gateway) instead of a framework-owned catalog/
checkout/order-admin system — a project's idea of a "product" is too
site-specific to standardize, so it builds its own on Lib\Db like any
other feature.

Also removes four Done entries (In-house comments, Email verification,
Media/file manager, User deletion & email addresses) that nothing open
still depends on or references, per this file's own stated retention
policy — their history remains in git log and the shipping commits.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
code
2026-07-15 19:49:13 +00:00
parent 64defe7f74
commit 8540c6d9ea
+63 -185
View File
@@ -58,52 +58,85 @@ expected vs. actual behavior. For features, include the motivating use case.>
Suggested build order (foundations first):
1. **In-house comments** — admin login & user management (Done
2026-07-14) unblocked this; comments are tied to real user accounts,
not anonymous.
2. **Ecommerce functionality** — its dependencies (SQLite groundwork,
session handling for the cart, admin login for order/product admin)
are all done now.
3. **Paywall functionality** — needs everything Ecommerce needs, plus
Ecommerce itself for the recurring-billing/payment-gateway plumbing;
build after it rather than in parallel — also now has a concrete
precedent to follow for the "gated content must skip the static cache"
part of its design (see Draft pages (admin-only preview) in Done, and
the caching/auth standing rule in `AGENTS.md`), which was still an open
question when this entry was originally written.
1. **Ecommerce: Lib\Money** — no dependencies, and the other two Ecommerce
pieces below both need it.
2. **Ecommerce: Lib\Cart** — needs Lib\Money.
3. **Ecommerce: payment gateway helper** — needs Lib\Money; independent of
Lib\Cart, so it could also go before it.
4. **Paywall functionality** — needs the payment gateway helper above for
its recurring-billing/payment plumbing; build after it rather than in
parallel — also now has a concrete precedent to follow for the "gated
content must skip the static cache" part of its design (see Draft
pages (admin-only preview) in Done, and the caching/auth standing rule
in `AGENTS.md`), which was still an open question when this entry was
originally written.
Session handling (with flash sessions), Draft pages (admin-only preview),
Admin login & user management, and Media/file manager all shipped (see
Done) — 2026-07-14 for the first three, 2026-07-15 for Media/file manager.
and Admin login & user management all shipped (see Done) — every open
entry above still depends on at least one of them. The original single
"Ecommerce functionality" entry was scoped down into the three
Lib\-helper pieces above on 2026-07-15 — see Lib\Money's entry for why.
See **Won't Do** below for 404 tracking, dropped in favor of Matomo.
### Ecommerce functionality
### Ecommerce: Lib\Money
- **Type:** Feature
- **Status:** Backlog
- **Priority:** Low
- **Depends on:** SQLite groundwork (Done), Session handling (with flash sessions) (Done), Admin login & user management (Done)
- **Added:** 2026-07-12
- **Added:** 2026-07-15
Product catalog, cart, checkout, and order storage — a `products` /
`orders` table in SQLite, a session-based cart (rides on the flash-session
work above), and a payment gateway integration for actually taking money.
Given the project's no-Composer/no-vendored-SDK philosophy, prefer calling
a payment provider's HTTP API directly (e.g. Stripe's REST API via cURL)
over vendoring a full SDK, same reasoning as vendoring only Twig's `src/`
rather than pulling in a package manager. Needs a decision on which
provider(s) to support first. Order/product management rides on admin
login above. Large feature — likely worth its own sub-breakdown (catalog,
cart, checkout, order admin) once it's actually picked up rather than
planning it all up front here.
First and smallest piece of the former "Ecommerce functionality" entry —
scoped down (2026-07-15) from a full catalog/cart/checkout/order-admin
system to a handful of composable `Lib\` helpers, since a project's idea
of a "product" is too site-specific to standardize; the project builds
its own catalog and admin UI on `Lib\Db` the same way it would for any
other feature, the same split `Lib\Comments` already makes for what a
"page" is. `Lib\Money`: integer-cents arithmetic (add/subtract/multiply
by a quantity) and formatting, avoiding the classic float-rounding bugs
of storing prices as floats. No dependencies — the foundation `Lib\Cart`
and the payment gateway helper below both need a non-lossy way to
represent an amount before either can be built.
### Ecommerce: Lib\Cart
- **Type:** Feature
- **Status:** Backlog
- **Priority:** Low
- **Depends on:** Ecommerce: Lib\Money, Session handling (with flash sessions) (Done)
- **Added:** 2026-07-15
Second piece of the former "Ecommerce functionality" entry (see Lib\Money
above for the scoping note). A session-based cart primitive — add/remove/
update line items, line and cart totals via `Lib\Money` — riding on
`Lib\Session` the same way `Lib\Csrf`/`Lib\AdminAuth` lazily touch the
native session. Generic on purpose: the cart holds id/qty/price entries a
sidecar hands it, with no opinion on what a "product" is or where its
catalog data comes from.
### Ecommerce: payment gateway helper
- **Type:** Feature
- **Status:** Backlog
- **Priority:** Low
- **Depends on:** Ecommerce: Lib\Money
- **Added:** 2026-07-15
Third piece of the former "Ecommerce functionality" entry (see Lib\Money
above for the scoping note). A driver-dispatched `Lib\` class for taking
a payment, mirroring `Lib\Mailer`'s `mail_driver` config-key pattern —
Stripe first (one `charge()`-shaped call plus webhook signature
verification), calling the provider's REST API directly via cURL rather
than vendoring an SDK, same reasoning as vendoring only Twig's `src/`
rather than pulling in a package manager. Adding a second provider later
means one more driver case, same as `Lib\Mailer::sendMail()`.
### Paywall functionality
- **Type:** Feature
- **Status:** Backlog
- **Priority:** Low
- **Depends on:** Ecommerce functionality (recurring billing/payment plumbing), SQLite groundwork (Done), Session handling (with flash sessions) (Done), Admin login & user management (Done)
- **Depends on:** Ecommerce: payment gateway helper, SQLite groundwork (Done), Session handling (with flash sessions) (Done), Admin login & user management (Done)
- **Added:** 2026-07-12
Subscription/membership content gating, similar to OnlyFans/Patreon:
@@ -130,161 +163,6 @@ _Nothing yet._
## Done
### In-house comments
- **Type:** Feature
- **Status:** Done
- **Priority:** Medium
- **Depends on:** Admin login & user management (Done), SQLite groundwork (Done)
- **Added:** 2026-07-14
- **Shipped:** 2026-07-15
A self-hosted comment thread — no third-party service (Disqus, Commento,
etc.) — `Lib\Comments` (`novaconium/lib/Comments.php`), a plain static
class any sidecar can call to attach comments to any page, not just blog
posts, the same way `Lib\SpamGuard`/`Lib\FormValidator` are reusable
across any form rather than hardcoded to the contact page. Comments are
tied to a real logged-in account, never anonymous name/email fields —
`App\AdminAuth::currentUser()` already excludes disabled and unverified
accounts (see Email verification below), so there's nothing further to
check before accepting a comment from whoever it returns.
**Framework-level, not `App/migrations/`.** The original backlog draft
sketched the migration under `App/migrations/`, but comments — like Admin
auth and Media manager — is reusable framework functionality, not project
content, so the migration (`novaconium/migrations/0004_create_comments.sql`),
the class, and the moderation UI (`novaconium/pages/admin/comments/`) all
live under `novaconium/`, consistent with every other feature of this
shape.
**Moderation model: auto-approve, then moderate.** A comment is visible
the instant it's posted — no pending queue — since only a verified
account can post one at all, there's no anonymous-spam vector to pre-vet
against. An admin can hide (excluded at the query level, not just
visually) or permanently delete any comment after the fact at
`/admin/comments`, mirroring how `/admin/users` disables rather than
pre-vets accounts. Submission still runs through `Lib\SpamGuard`'s
honeypot/timing check and `Lib\Csrf`, same as the contact form.
**A page needs its own sidecar to use it.** This codebase has no
client-side JS/fetch anywhere — every dynamic feature is a plain
server-rendered POST form — so a comment thread is no different: giving a
page a sidecar is what excludes it from the static HTML cache (only
sidecar-less pages are ever cached), an explicit per-page tradeoff the
same as `/admin/media` and `/search`. `App/pages/blog/comments-demo/` is
the one post under `App/pages/blog/` with a sidecar, added specifically
to demonstrate the pattern without disturbing the other posts (several of
which reference `hello-world` by name as the canonical *sidecar-less*
example — retrofitting it instead would have made those references
wrong). The reusable Twig half is
`novaconium/pages/_partials/comments/thread.twig` — a `_`-prefixed,
never-routable partial, included with `{% if comments is defined %}` so
a layout shared by comment-enabled and sidecar-less pages alike can
include it unconditionally. Documented at `/admin/docs/comments`.
### Email verification for user accounts
- **Type:** Feature
- **Status:** Done
- **Priority:** Medium
- **Depends on:** Admin login & user management (Done)
- **Added:** 2026-07-14
- **Shipped:** 2026-07-15
Every user created after the first must click a 24-hour link
(`bin2hex(random_bytes(32))`, same pattern as `Lib\Csrf::token()`) emailed
to their address before `AdminAuth::attempt()` lets them log in at all —
folded into the same generic pass/fail as a disabled account or a wrong
password, no oracle revealing which. Two exemptions, both following the
last-active-admin guard's reasoning that a safety mechanism can't depend on
something that might not be configured: the very first user (auto-verified
at creation, since there's no other admin to vouch for them, and they're
auto-logged-in the same instant) and every row that predates this migration
(`novaconium/migrations/0003_add_users_verification.sql` backfills
`verified_at` from `created_at`). `create-admin-user.php` also auto-
verifies, since lockout recovery can't depend on a working mail transport
either.
`/verify-email` follows `/admin/logout`'s GET-confirms/POST-mutates shape
(the content-index crawl forces a GET on every route, and mail-security
scanners prefetch links — either would burn a GET-mutates token); an
invalid/expired/already-used token never touches the database either way.
Changing an account's email at `/admin/users` resets verification and
re-sends to the new address; `/admin/users` also grew a per-row **Resend
verification** button, visible only when unverified.
`Lib\Mailer` grew a second method, `sendMail()`, for this and future
transactional mail (password-reset-by-email later, per the original spec)
— separate from `send()`, which the contact form keeps using unchanged.
`sendMail()` is driver-dispatched via a new `mail_driver` config key:
`'log'` (default) reuses the same `contact-log.txt` fallback so a fresh
checkout can create and verify accounts with zero external dependency;
`'mailjet'` sends through MailJet's Send API v3.1 (`mailjet_api_key`/
`mailjet_api_secret`/`mail_from_email`/`mail_from_name` in `App/config.php`
— not gitignored, same as every other config key, per the user's decision
that deployer-owned secrets are acceptable here). Adding another provider
later means one more `match` case in `sendMail()` — callers never change.
Documented in `/admin/docs/admin-auth`'s new "Email verification" section.
### Media/file manager
- **Type:** Feature
- **Status:** Done
- **Priority:** Medium
- **Added:** 2026-07-12
- **Shipped:** 2026-07-15
An upload/browse/delete UI for media (images, PDFs, etc.) at `/admin/media`
so sidecars and Twig templates have a consistent place to reference
uploaded files from — e.g. a blog post's header image — instead of authors
manually copying files into `public/`. Covered by the existing
`/admin/*` auth gate the moment the page was added, no extra wiring
needed — as planned, there's no separate `*_enabled` flag (unlike admin
auth/content index) since it has no SQLite dependency to gate. Backed by a
plain directory, `public/uploads/` (gitignored per-file with a tracked
`.gitkeep`, same convention as `data/`), rather than a database — no
metadata store (alt text, captions) yet; that's flagged as a natural
SQLite fit if wanted later, not built now. Also removed the top-level
`images/` scaffolding directory/volume (reserved for a future
image-upload feature, per the prior `AGENTS.md` wording): nothing had
ever consumed it, and `public/uploads/` now covers the use case it was
held open for. Safety handling: an extension
allowlist and max upload size, both configurable
(`media_upload_extensions`/`media_upload_max_bytes` in `App/config.php`),
plus filename sanitization (`basename()` + safe-charset reduction,
collision-safe via a `-1`/`-2`/... suffix) and a `realpath()` re-check on
delete to confirm the resolved path still lands inside `public/uploads/`
before unlinking. Documented at `/admin/docs/media-manager` (new topic,
linked from the docs nav/index and the admin dashboard), with a matching
README feature/docs-index entry.
### User deletion & email addresses
- **Type:** Feature
- **Status:** Done
- **Priority:** Medium
- **Depends on:** Admin login & user management (Done)
- **Added:** 2026-07-14
- **Shipped:** 2026-07-14 (b882c30)
Second same-day follow-up to Admin login & user management (below),
also pre-commit — so the `email` column went into the existing
`0002_create_users.sql` like the roles change before it. `/admin/users`
gained a hard-delete action (username/email become reusable; any live
session dies on its next request via the same per-request row re-check
disabling uses; the last-active-admin guard covers delete as well as
disable/demote) and a change-email action. Every account now requires a
unique email address — validated and normalized (trim + lowercase) via
the existing `Lib\Validate::isEmail()`, so uniqueness is
case-insensitive by construction (verified: `BOB@example.com` collides
with `bob@example.com`) — on the create form, the change-email action,
and `bin/create-admin-user.php` (now `<username> <email>`). Not used
for login or any mail yet; it exists so the planned email-verification
flow (new Backlog entry above) has an address for every account that
predates it. Delete stays deliberately distinct from disable in the UI
(inside a confirm-style `<details>` with a warning): disable is
keep-but-shut-out, delete is gone-for-good.
### User roles, groups & page access control
- **Type:** Feature