Release notes

Changelog

Every notable change since v0.1, in reverse chronological order. Each release is anchored — link to a specific version with /landing/changelog/#v0-13-0.

v0.17.0

Phase 14 — Realtime via Django Channels. Apex now ships an ASGI WebSocket layer that pushes notifications and presence updates to every connected tab without HTTP polling. Dev needs zero infra (the in-memory channel layer); production upgrades to Redis with a single env var.

Phase 14 — Realtime via Django Channels. Apex now ships an ASGI

WebSocket layer that pushes notifications and presence updates to

every connected tab without HTTP polling. Dev needs zero infra (the

in-memory channel layer); production upgrades to Redis with a single

env var.

Added — Infra

  • Channels 4 + Daphne added as dependencies. Daphne replaces gunicorn's runserver in dev so WebSocket upgrades work natively.
  • apex/asgi.py routes HTTP through Django and WebSocket through the realtime app's URL router, wrapped in AuthMiddlewareStack so consumers see scope["user"] populated from the session cookie, and AllowedHostsOriginValidator so cross-origin sockets are rejected at the perimeter.
  • CHANNEL_LAYERS defaults to InMemoryChannelLayer (dev/test). prod.py upgrades to channels_redis.core.RedisChannelLayer when REDIS_URL is set; install with uv sync --extra realtime to pick up the optional channels-redis dep.

Added — Consumers

  • NotificationConsumer (/ws/notifications/) — per-user fan-out channel (notify.user.<id>). Receives notify.message (new row) and notify.count (unread badge bump) events from the layer. Anonymous connects close with code 4401 so the client stops retrying.
  • PresenceConsumer (/ws/presence/) — global presence channel. Tracks per-process connection set; broadcasts a count + the joined/left username on every transition.
  • Both built on AsyncJsonWebsocketConsumer so consumers can await ORM lookups via database_sync_to_async.

Added — Server-side dispatch hooks

  • apps/realtime/dispatch.push_notification(user_id, payload) and push_unread_count(user_id, count) — sync wrappers around channel_layer.group_send for use from any view, signal, or management command. No-op if no layer is configured.
  • apps.notifications.dispatch.notify() now fans out to the layer after every Notification.objects.create() — every existing notification source (orders, invoices, mail, chat, mentions, realtime test) becomes live with no per-call wiring.

Added — Client

  • static/js/realtime.js exposes apexNotifyStream() and apexPresence() Alpine factories. WebSocket auto-reconnects with exponential backoff (cap 30s); 4401 close is honored as "anonymous, stop trying". apexNotifyStream re-renders the bell badge inline AND triggers an HTMX refresh so the dropdown body stays in sync.
  • Header presence pill — green pulse + count of online users, shown only when count > 0 so first paint stays clean.
  • Bell badge rendered with data-bell-badge so the realtime client can update the count without a DOM rebuild.

Added — Demo surface

  • /realtime/ — opens both sockets, shows the live count, and ships a "Fire test notification" button that POSTs to /realtime/fire/. Open in two tabs — both update instantly.
  • Sidebar entry under Apps; palette-searchable via the realtime, websocket, presence, live, channels keywords.

Tests

  • 16 new tests: 8 async consumer tests using WebsocketCommunicator (anonymous reject, accept, group fan-out, per-user isolation, presence count + join/leave broadcast), 4 sync dispatch tests (helper drains, end-to-end notify→layer fan-out, no-layer no-op), 4 view tests (demo render, anon redirect, fire-test creates Notification, GET rejected). Total now 1000.
  • New dev dep: pytest-asyncio (strict mode; opt-in per test via @pytest.mark.asyncio, no impact on existing sync tests).

Notes

  • Single-process scaling stays trivial. Multi-process needs Redis or the layer's broadcasts are per-worker. The presence count is per-process by design — fine for the demo, swap to a Redis ZSET if you need cluster-wide accuracy.
  • Chat broadcast and Kanban broadcast are intentionally NOT wired yet — the foundation is in place, follow-up commits can subscribe those views to the layer without further infra changes.
  • WebRTC, voice/video, and message-history search are explicitly out of scope.
v0.16.0

Phase 16 — Organizations + RBAC. Apex is now multi-tenant. Users belong to one or more organizations; one is "active" per session and drives any model that opts into org scoping. Roles (owner > admin > billing > member > viewer) gate sensitive actions through a small mixin layer.

Phase 16 — Organizations + RBAC. Apex is now multi-tenant. Users

belong to one or more organizations; one is "active" per session and

drives any model that opts into org scoping. Roles

(owner > admin > billing > member > viewer) gate sensitive actions

through a small mixin layer.

Added — Models

  • Organization — name, auto-generated slug (with collision suffixing), optional logo, plan choice (free / pro / enterprise), created_by, timestamps.
  • Membership — joins User × Organization with a role, unique on (user, org), indexed for fast lookup.
  • Invitation — email + role + opaque secrets.token_urlsafe(32) token, 14-day TTL, accepted_at timestamp. Idempotent accept() creates the Membership and tolerates double-redeem.

Added — Request middleware

  • OrganizationMiddleware resolves request.organization, request.memberships, request.organization_role for every authenticated request. Precedence: session-stored slug → first membership alphabetically → None. Failures are swallowed so a tenant lookup never breaks the request.
  • set_active_organization(request, org) helper for views that switch tenants; refuses non-members.

Added — View mixins

  • OrgRequiredMixin — bounces to /orgs/ when no active org.
  • HasRoleMixin — enforces a minimum role (raises 403 otherwise).
  • OrgScopedMixin — opt-in queryset filter for any list view whose model has an organization FK. Existing apps aren't migrated yet — this ships ready for incremental rollout.

Added — UI surfaces

  • /orgs/ — list of memberships with switch / settings actions + create-new form (owner role auto-assigned).
  • /orgs/<slug>/settings/ — name + plan editor with an owner-only Danger Zone delete confirmation.
  • /orgs/<slug>/members/ — member roster with inline role change + remove buttons (admin/owner only). Pending invitations panel with expiry countdown and cancel action.
  • /invitations/<token>/ — public accept page, handles unauthenticated, wrong-account, expired, already-accepted, and happy-path states.
  • Header org switcher — auth-only dropdown showing all memberships, role label, active checkmark, and a "Manage organizations" link.
  • Sidebar entry — Organizations under the Account group.

Added — Demo data

  • seed_demo now creates 3 sample orgs (Apex Demo Co. / Side Project / Acme Holdings) with the demo user as owner of each, three teammate memberships on the primary org (admin / member / billing), plus one pending invitation so every UI state is covered.

Tests

  • 46 new unit tests across models (slug + initials + role rank + invitation TTL/idempotency), middleware (anonymous / no-membership / session override / non-member rejection), view mixins (OrgRequired redirect, HasRole 403/200, OrgScoped filter + none-fallback), and view flows (list/create/switch/settings/ members/invite/role-change/remove/cancel/accept). Total now 984.

Notes

  • Per-org permission *matrix* (Role + Permission tables, editable per org) is intentionally deferred — hardcoded role checks via role_at_least() are clean enough to swap to a matrix later without view rewrites.
  • Org-scoping every existing model (Customer, Invoice, Order, …) is also deferred. The OrgScopedMixin ships ready for incremental rollout — apply per app as schema migrates to add the FK.
  • Invitation links surface in the UI toast (no email backend wired yet); the workflow is fully testable end-to-end without SMTP.
v0.15.0

Phase 18 — Marketing polish. Adds the public surfaces a buyer actually checks before buying: a real changelog page, a Now/Next/Later roadmap, a side-by-side comparison table, a one-page showcase index, plus full SEO meta tags + sitemap.xml + robots.txt.

Phase 18 — Marketing polish. Adds the public surfaces a buyer actually

checks before buying: a real changelog page, a Now/Next/Later roadmap,

a side-by-side comparison table, a one-page showcase index, plus full

SEO meta tags + sitemap.xml + robots.txt.

Added — Marketing pages

  • /landing/changelog/ — renders CHANGELOG.md directly with per-release anchors (link with #v0-13-0). Custom-built tiny markdown parser (no extra dep) covering H3 / lists / inline code / bold / links — that's all the changelog uses.
  • /landing/roadmap/ — public Now / Next / Later board sourced from RoadmapView.NOW / .NEXT / .LATER. Hand-curated so we can edit it without a deploy via PR; admin model can come later.
  • /landing/compare/ — Apex vs hand-rolled vs typical premium template, grouped into Foundation / UI surfaces / Integrations / Operations sections.
  • /landing/showcase/ — one-page index of every demo surface, organized into 8 cards (Dashboards, Components, Forms, Datatable, Charts, Productivity, API, Pages).

Added — SEO

  • **<meta name="description">, og:title, og:description, og:image, og:url, og:type** — every marketing page; per-page override blocks ({% block og_title %}, etc.).
  • Twitter card (twitter:card, twitter:title, twitter:description).
  • <link rel="canonical"> auto-derived from request URL.
  • JSON-LD SoftwareApplication block (overridable per page).
  • /sitemap.xml — Django sitemaps for all 11 marketing routes. No django.contrib.sites dep — uses RequestSite so SITE_ID isn't required.
  • /robots.txt — allows /landing/, /blog/, /help/; disallows /admin/, /api/, /accounts/, /settings/.

Updated

  • Marketing footer — gains a Resources column linking to Showcase, Compare, Changelog, Roadmap. Old Legal column folded into Company.

Tests

  • 20 new unit tests across the changelog parser, page renders, SEO meta presence, sitemap structure, robots.txt rules. Total now 938.

Notes

  • The changelog renderer is a hand-rolled markdown subset, not a full CommonMark parser. If we start needing tables, fenced code, or footnotes inside the changelog, swap to the markdown package (one-line view change).
  • The roadmap data lives in code so PRs trigger CI; an admin-editable RoadmapItem model is a fine follow-up if non-engineers need to edit it.
v0.14.0

Phase 17 — Settings depth. Promotes /settings/ from "profile + password + 2FA + appearance" to enterprise-tier completeness with device sessions, API token management, webhook subscriptions, audit log, GDPR-style data export, and confirmable account deletion.

Phase 17 — Settings depth. Promotes /settings/ from "profile +

password + 2FA + appearance" to enterprise-tier completeness with

device sessions, API token management, webhook subscriptions, audit

log, GDPR-style data export, and confirmable account deletion.

Added — Models

  • SessionMetadata (sidecar for django.contrib.sessions.Session) — tracks user, user_agent, ip_address, last_seen_at per active session so the Sessions pane can list "iPhone · Safari · 192.0.2.1 · last seen 2 minutes ago".
  • AuditEvent — append-only per-user security log (sign-in, sign-out, login_failed, password_changed, two_factor_enabled, api_key_created, session_revoked, data_export_requested, account_deletion_requested / canceled).
  • User.pending_deletion_at — soft-delete flag with 30-day grace period before hard delete.

Added — Settings panes

  • Active sessions (/settings/sessions/) — list devices currently signed in, "sign out other sessions" button, per-row sign-out.
  • API tokens (/settings/api-tokens/) — UI for the Phase 15 APIKey model. Create with a name, raw key revealed exactly once via a copy-to-clipboard banner, revoke any time.
  • Webhooks (/settings/webhooks/) — list / create / delete UI for the Phase 15 Webhook model. Event picker grid, secret revealed once on create, recent-deliveries log surfaces success/failure + response codes.
  • Audit log (/settings/audit-log/) — last 200 security events with type-specific iconography.
  • Export your data (/settings/data-export/) — synchronous ZIP build containing user.json + notifications.json + audit_events.json + api_keys.json (no secrets) + webhooks.json + README.
  • Delete account (/settings/account-deletion/) — typed-confirm flow ("delete my account"), immediate sign-out + is_active=False, cancellable from the same page during the grace period.

Added — Middleware + signals

  • SessionMetadataMiddleware — populates SessionMetadata on every authenticated request. Throttled to one DB write per minute per session via a session-stored timestamp.
  • apps.accounts.signals — registers handlers for user_logged_in, user_logged_out, user_login_failed that record matching AuditEvent rows. Failed logins for unknown usernames are dropped (we don't leak username existence via the audit trail).
  • record_audit(user, kind, request=...) — convenience helper for views that perform security-relevant actions; pulls IP + user-agent from the request when present.

Added — Management commands

  • process_pending_deletions — hard-deletes users whose pending_deletion_at + grace-days has elapsed. --grace-days N override + --dry-run flag.
  • cleanup_session_metadata — drops SessionMetadata rows whose underlying Session is gone. Pair with Django's clearsessions in nightly cron.

Updated

  • Settings layout nav — reorganized into 4 sections (Account / Security / Integrations / Privacy) so the new panes have a home. Notification preferences (Phase 13) gets a link from Integrations.

Tests

  • 32 new unit tests covering each pane + audit signals (login + login failed wired automatically) + both management commands. Total now 918.

Notes

  • The data export is generated synchronously — fine at demo scale. For large accounts, swap to a Celery task that emails a download link when ready.
  • Account deletion is a *soft* delete during the grace period (is_active=False blocks login but data is intact). The process_pending_deletions command performs the actual hard delete after the grace window — run it nightly in cron.
v0.13.0

Phase 15 — Django Ninja API. Production-quality REST API over the core domain models with bearer-token auth, cursor pagination, signed outbound webhooks, and auto-generated OpenAPI/Swagger docs.

Phase 15 — Django Ninja API. Production-quality REST API over the core

domain models with bearer-token auth, cursor pagination, signed

outbound webhooks, and auto-generated OpenAPI/Swagger docs.

Added — App + dependency

  • apps/api/ new app + django-ninja>=1.3 runtime dep.
  • /api/v1/ mounted in apex/urls.py. Swagger UI at /api/v1/docs, raw OpenAPI schema at /api/v1/openapi.json.

Added — Models

  • APIKey — per-user bearer token. Raw value shown exactly once on creation; only SHA-256 hash + non-secret prefix stored (Stripe / GitHub convention). Optional expires_at, manual revoke(), auto-tracks last_used_at.
  • Webhook — outbound subscription owned by a user with url, events (comma-joined), and a server-generated secret for HMAC signing.
  • WebhookDelivery — per-attempt audit log (status, response code, body, timestamp).

Added — Endpoints (CRUD + filter + cursor pagination)

| Resource | Operations |

|---|---|

| /customers/ | list (q, status filter) · create · retrieve · patch · delete (soft) |

| /products/ | list (q, status, category filter) · create · retrieve · delete |

| /orders/ | list (status, customer filter) · retrieve · patch status · delete |

| /invoices/ | list (status, customer filter) · retrieve · send · pay · void |

| /notifications/ | list (category, unread, archived filter) · mark read · mark all read · archive — scoped to authed user |

| /webhooks/ | list · create (returns secret once) · retrieve · delete — scoped to authed user |

All list endpoints use cursor pagination via ?cursor=<id>&limit=<N>

(default 25, max 100). All endpoints require `Authorization: Bearer

apex_<token>`.

Added — Webhook dispatch

  • apps.api.dispatch.dispatch_webhook(event, data) — best-effort POST to every active subscriber whose events includes the name. Signs body with HMAC-SHA256, sends X-Apex-Signature: sha256=<hex> + X-Apex-Event: <name> + canonical JSON encoding (sorted keys, no whitespace) so signatures reproduce.
  • Wired into Invoice transitions: mark_sent / mark_paid / mark_void each fire invoice.{sent,paid,void} events.
  • Invoice.transition_to(target) — convenience dispatcher used by the API endpoints.

Added — Management command

  • python manage.py create_api_key <username> --name "label" — creates an APIKey and prints the raw token once (with a curl example for immediate use).

Added — Auth

  • KeyAuth Ninja security class — looks up the bearer token in APIKey, attaches the matched key + owner to the request as request.api_key / request.auth_user, and bumps last_used_at on every successful authentication.

Updated

  • /pages/api-docs/ — keeps the static code-sample reference but now opens with a banner pointing at the live Swagger UI + the raw OpenAPI schema URL.

Tests

  • 52 new unit tests (auth, every CRUD endpoint, filter + cursor pagination, webhook dispatch + signing, schema served). Total now 886.

Notes

  • Webhook delivery is synchronous — fine for low volume; a real queue (Celery / RQ / arq) is a follow-up for production scale.
  • Real Web Push delivery (Phase 13) reuses the same dispatch pattern but lives in the notifications app, not here.
v0.12.0

Phase 13 — Notification center. Promotes notifications from "bell + dropdown" to a real category-aware center with per-user, per-channel preferences and browser-push opt-in scaffolding.

Phase 13 — Notification center. Promotes notifications from "bell +

dropdown" to a real category-aware center with per-user, per-channel

preferences and browser-push opt-in scaffolding.

Added — Models

  • Notification gains: - category (system / billing / mention / comment / security) — the new dimension that preferences key off - actor FK (optional) — for "Sara mentioned you" rows with avatars - archived_at — soft-archive that excludes from default views - target_url alias on url for forward-compat - extended kind enum (mention, comment, security)
  • NotificationPreference — per-user × per-category × per-channel toggle (in_app / email / push). Lazy defaults via CHANNEL_DEFAULTS so users don't need a row to receive sensible defaults (in_app on for everything; email on for billing + security; push always off until explicitly enabled).
  • PushSubscription — Web Push subscription endpoint with endpoint / p256dh / auth keys + user-agent.
  • NotificationQuerySet gains .active(), .archived(), .for_category() helpers.

Added — Central dispatcher

  • apps.notifications.dispatch.notify(...) — single entry point for emitting notifications. Honors NotificationPreference per channel: in_app creates a row, email sends via the configured backend, push fires via pywebpush when configured.
  • notify_many(...) — fan-out helper.
  • All legacy helpers (notify_invoice_sent, etc.) now route through notify() so preference checks apply uniformly.
  • get_effective_pref(user, category, channel) — resolution helper; falls back to defaults for users who haven't customized.

Added — Surfaces

  • /notifications/ rewritten with: - Category filter pills (with per-category counts) - Active / Archived scope tabs - Day-bucketed grouping (Today / Yesterday / Earlier this week / Earlier this month / Older) - Per-row archive button (and restore from the Archived view) - Actor avatar + colored category pill on each row
  • /notifications/preferences/ — category × channel toggle grid with descriptive copy for each category and a browser-push opt-in panel below the form.
  • /notifications/<pk>/archive/ + restore.
  • /notifications/push/{subscribe,unsubscribe}/ — JSON endpoints consumed by the browser's PushManager subscription flow.

Added — Service worker + Alpine factory

  • static/sw.js — minimal service worker handling push and notificationclick events. Phase 19 (PWA) extends this same file with offline shell + asset caching.
  • apexPushOptIn() Alpine factory in static/js/shell.js — registers the service worker, requests permission, subscribes via PushManager, POSTs the subscription to the server. Detects unconfigured VAPID keys and shows a friendly status instead of attempting to subscribe.

Migrations

  • 0004_notificationpreference_pushsubscription_and_more — schema changes for the new fields/models.
  • 0005_backfill_category — back-fills category from the legacy kind enum so filter pills partition existing data correctly.

Tests

  • 28 new unit tests (preferences, archive, push subscribe/unsubscribe, dispatch matrix). Total now 834.

Notes

  • Real Web Push delivery requires VAPID keys + pywebpush. The model + endpoints + service worker + opt-in UI are shipped now; production delivery is a small follow-up that adds an apps/notifications/push.py module with send_push_to_user(). The notify() dispatcher already imports it lazily, so adding push delivery is a no-rewrite change.
v0.11.0

Phase 12 — Forms 2.0. A polished widget library that subclasses Django's forms.Widget with proper sizes, validation states, and helper text plumbing. Closes the "form polish = perceived quality" gap with floating-label inputs, chip-style multi-select, free-form tag input, typeahead combobox, drag-drop file dropzone with XHR upload, date range picker, and a self-hosted Markdown rich text editor.

Phase 12 — Forms 2.0. A polished widget library that subclasses Django's

forms.Widget with proper sizes, validation states, and helper text

plumbing. Closes the "form polish = perceived quality" gap with

floating-label inputs, chip-style multi-select, free-form tag input,

typeahead combobox, drag-drop file dropzone with XHR upload, date

range picker, and a self-hosted Markdown rich text editor.

Added — Widget library (apps/core/widgets/)

  • WrappableWidget mixin + _field_wrapper.html — shared shell for label / widget / helper / error rendering with auto-detected validation state (default / success / warning / error).
  • {% apex_field %} template tag — render any BoundField wrapped in the canonical Apex form-field shell. Auto-derives state from form errors; helpers can be overridden per-field.
  • Three sizes (sm / md / lg) shared across every widget.

Added — Inputs

  • FloatingLabelInput — single-line input with a label that floats inside on focus or when filled. Pure CSS, no JS for the float.
  • FloatingLabelTextarea — multi-line variant with auto-grow (Alpine apexAutogrow) and optional character counter.
  • IconPrefixInput — Lucide icon inside the left edge.
  • IconSuffixInput — icon at the right edge; clickable=True makes it a button that dispatches apex:icon-suffix:click (for password show/hide, copy-to-clipboard, etc.).

Added — Choice

  • MultiSelect — chip-style picker over a fixed option set. Backs forms.MultipleChoiceField. Posts as repeated form values.
  • TagInput — free-form chips with paste-to-split + optional suggestion chips. Stores as a comma-joined string.
  • Combobox — single-select typeahead with an optional async_url for HTMX/JSON-driven options.
  • TypeaheadMixin — paired view mixin that converts ?_typeahead=1&q=… requests into JSON option lists.

Added — Date + upload

  • DateRangePicker — trigger-button + popover with two native date inputs and preset shortcuts (Today, Last 7 days, etc.). Stores the range as a comma-joined ISO string.
  • FileDropzone — drag-drop multi-file with previews and per-file XHR progress. Configurable upload_url, accept, max_files, max_size_mb. Endpoint must return JSON {id, name, size, url}; the widget tracks IDs in a hidden field.

Added — Rich content + helpers

  • RichText — Markdown editor backed by self-hosted EasyMDE (vendored under static/{js,css}/vendor/, refreshable via npm run vendor:easymde). Toolbar presets: minimal, basic, full.
  • apexCharCounter(maxLen) — Alpine helper for any text input with auto-coloring (amber → red) as you approach the cap.
  • apexReveal(targetId, predicate) — show/hide a field group based on another field's value. Pure Alpine, no backend.

Added — Alpine factories (in static/js/shell.js)

  • apexAutogrow(maxRows) — textarea auto-grow.
  • apexDateRange({from, to}) — popover state + ISO string handling.
  • apexCharCounter(maxLen) — character-count tracking.
  • apexReveal(targetId, predicate) — conditional reveal.
  • Extended apexDropzone with XHR upload mode, per-file progress, cancel, server ID tracking, and uploadedIds getter for the form hidden input.

Added — Template tags

  • {% apex_field bound_field %} — wraps a field in _field_wrapper.html.
  • {{ value|json_dumps }} — JSON-serialize a value for safe Alpine init.

Added — Demo upload endpoint

  • pages:forms_gallery_upload — synthetic multipart endpoint backing the gallery's FileDropzone demo.

Settings

  • FORM_RENDERER = "django.forms.renderers.TemplatesSetting" so widget templates resolve from the project templates/ directory.
  • django.forms in INSTALLED_APPS so Django's built-in widget templates remain discoverable.

Forms upgraded

  • Customer create/edit — FloatingLabelInput, IconPrefixInput (email), FloatingLabelTextarea (notes with character counter).
  • Profile edit — FloatingLabelInput, IconPrefixInput (email), FloatingLabelTextarea (bio).
  • Mail compose — Combobox (recipient), FloatingLabelInput (subject), FloatingLabelTextarea (body).
  • Project create/edit + task + milestone — FloatingLabelInput, FloatingLabelTextarea, Combobox.

Forms gallery rewrite

  • pages/forms_gallery.html rebuilt as a left-rail TOC with 12 widget sections + reference sections for validation states and sizes. Each section renders the live widget via {% apex_field %} and shows the Python snippet to declare the field.

Tests

  • 81 new unit tests across base infrastructure, inputs, choice, date, upload, rich text. Total now 806.
  • 7 new E2E tests covering the gallery + the upgraded Customer form.

Dependencies

  • easymde@^2.20.0 (npm devDependency, vendored to static/).
v0.10.0

Phase 11 — HTMX datatable. A reusable, server-driven table system that any list view can opt into. Closes the "tables are where buyers stress-test dashboards" gap with sort, filter, paginate, search, bulk actions, saved views, column visibility, and three export formats — all without a SPA.

Phase 11 — HTMX datatable. A reusable, server-driven table system that

any list view can opt into. Closes the "tables are where buyers stress-test

dashboards" gap with sort, filter, paginate, search, bulk actions, saved

views, column visibility, and three export formats — all without a SPA.

Added — Datatable system

  • apps/core/tables/ package with TableConfig, Column, Filter, BulkAction frozen dataclasses + TableView mixin. Public API:

```python

from apps.core.tables import TableView, TableConfig, Column, Filter, BulkAction

```

  • HTMX swaps for every interaction (sort, filter, search, paginate, bulk action) — server-rendered _table.html partial is the only thing that re-renders. URL is push-stated so back/forward works.
  • 6 filter widgets: text, select, multi-select, daterange, numeric range, boolean. Filter value translation is whitelist-driven — URL params for unconfigured columns are silently ignored.
  • Multi-column sort via ?sort=col1,-col2,col3. Disallowed columns drop silently.
  • Bulk-action toolbar appears when ≥1 row selected. Confirm modal uses Phase 10's modal primitive. Subclasses implement handle_bulk_action(action, ids, request).
  • Saved views (SavedView model) — per-user named filter+sort combos. Default-view auto-applies on bare visit; switcher / set-default / delete via ?_view_action=... round-trips.
  • Column visibility (UserPreference model) — per-user persisted via a generic JSON-blob preference store. Pinned columns can't be hidden.
  • 3 export formats: - CSV (stdlib csv, all rows of current filter) - XLSX (openpyxl — added as a runtime dep) - PDF (WeasyPrint, capped at 500 rows; cap check happens before importing WeasyPrint so the over-cap error works without cairo)
  • Empty states with three modes auto-detected: "no data yet", "no matches" (filter narrowed too much), or generic "no rows". Per-table copy via TableConfig.empty_headline / empty_body.
  • Mobile fallback — table collapses to stacked label/value cards under md via _row_card.html.

Added — Models

  • UserPreference(user, key, value JSON) — generic per-user JSON store.
  • SavedView(user, table_key, name, params, is_default) — per-table named filter+sort combos.

Added — Templates

  • templates/core/tables/ — full set: _table.html, _table_body.html, _row.html, _row_card.html, _toolbar.html, _pagination.html, _empty.html, _skeleton.html, _export.html, _bulk_toolbar.html.

Added — Template tags

  • {% sort_link col_key label %} — sortable header anchor with HTMX swap, asc → desc → unsorted toggle, ARIA-correct aria-sort.
  • {% table_url page=N %} — preserve params with optional overrides.
  • {% page_ids_script object_list config.key %} — feeds bulk selection.
  • {{ obj|dotted_attr:"a.b.c" }} — walks dotted attribute paths on rows.
  • {{ {…}|urlencode_dict }} — encodes a dict-of-lists for saved-view URLs.

Added — Alpine factory

  • apexBulk({pageIds}) — selection + select-all-on-page + confirm modal state. Lives in static/js/shell.js.

Wired into

  • Customers list — 6 columns, 3 bulk actions, friendly empty state.
  • Orders list — 5 columns, 3 status-transition bulk actions.
  • Invoices list — 6 columns, 3 lifecycle bulk actions.
  • Products list — 6 columns, 2 publish/archive bulk actions.
  • Users list — 5 columns, activate/deactivate bulk actions, boolean filter on is_active.
  • Activity log — replaces the timeline grouping with a sortable, filterable, exportable audit table; KPI strip preserved on the page.

Showcase

  • pages/datatable.html rewritten as a links-out grid pointing at the six TableView-powered list views, plus a "what's included" summary and a copy-paste view-creation snippet.
  • seed_demo bumped from 20 → 100 customers so the table has realistic pagination and filter scenarios out of the box.

Tests

  • 81 new unit tests (config, filters, prefs, models, view, bulk, saved views, exports). Total now 725.
  • 8 new E2E tests covering search swap, sort, pagination, CSV download, bulk select, column visibility menu, save+apply view.

Removed

  • The hand-rolled pages/datatable/ mock backend (200+ lines of static rows + custom filter/sort/export logic). Replaced by the real system.

Dependencies

  • openpyxl>=3.1 (runtime).

Notes

  • PDF export tests skip gracefully when WeasyPrint's native libs (cairo, pango, gdk-pixbuf) aren't installed, mirroring the existing apps/invoices/tests/test_pdf.py pattern.
  • The activity log's old date-bucket timeline UI is replaced by a TableView. Trade-off accepted for sort + filter + export power; the KPI strip and scope=mine filter are preserved on the page.
v0.9.0

Phase 10 — Component library. Adds a first-class /components/ surface documenting every reusable UI primitive shipped with Apex, plus a toast-notification system wired into Django's messages framework.

Phase 10 — Component library. Adds a first-class /components/ surface

documenting every reusable UI primitive shipped with Apex, plus a

toast-notification system wired into Django's messages framework.

Added — Components surface

  • apps.components new app with /components/ index + per-primitive detail pages (components:index, components:detail).
  • 26 primitives across 7 categories, each on its own page with multiple documented variants: - Overlay: Modal, Drawer, Toast, Tooltip, Popover. - Disclosure: Tabs (underline / pill / vertical / with-badges), Accordion (single / multi / FAQ), Stepper (horizontal numbered / progress-bar / vertical). - Inputs: Datepicker, Daterange, Timepicker, Color picker. - Choice: Multi-select, Tag input, Combobox, Toggle group, Segmented control, Rating (interactive + read-only), Slider (single / stepped / min-max range). - Upload: File dropzone (multi-file with previews + single avatar). - Feedback: Skeleton, Spinner, Progress ring, Empty state. - Identity: Avatar (sizes / status dot / group stack / squared), Badge (status pills / dot / solid / counts / sizes).
  • Component registry (apps/components/registry.py) — single source of truth for the index page + detail page lookup + palette keywords.
  • "Components" sidebar entry under Showcase, with command-palette keyword search across modal / drawer / toast / tabs / accordion.

Added — Toast notification system

  • **apps.core.messages.toast(request, level, body, *, action, persistent)** helper built on Django's messages framework. Existing messages.success(...) calls also light up automatically.
  • {% apex_toasts %} inclusion tag drains messages into a JSON payload consumed by Alpine.
  • Toast container partial (templates/partials/toasts.html) wired into layouts/dashboard.html and layouts/public.html. Sticky bottom right, aria-live="polite", per-level styling, action button, dismiss.
  • window.apexToast({...}) global JS helper for client-side pushes without a server round trip.
  • Reduced-motion respect: auto-dismiss extends 5s → 8s when prefers-reduced-motion is set.

Added — Alpine factories (in static/js/shell.js)

  • apexModal(id), apexDrawer(id) — focus restoration on close, dispatch via $dispatch('apex:open', id).
  • apexPopover() — local toggle with click-outside / Esc handling.
  • apexTabs(initial) — roving tabindex + arrow / Home / End keyboard nav.
  • apexAccordion({multi, initial}) — single / multi-open with isOpen / toggle API.
  • apexToasts(), apexMultiSelect, apexTagInput, apexCombobox, apexDropzone — pattern factories powering the demo pages.

Added — Lucide icons

  • 18 new lucide glyphs added to apps/core/templatetags/apex.py ICONS: blocks, panel-right, info, message-square, layout, chevrons-up-down, list-ordered, calendar-range, palette, list-checks, tag, toggle-right, sliders-horizontal, upload-cloud, inbox, user-circle, badge, loader-circle, square-stack.

Tests

  • 30 new unit tests covering registry integrity, view auth gating, the toast extra_tags round-trip, and "every primitive returns 200" for all 26 slugs.
  • 6 new E2E tests (Playwright) for index, modal, drawer, toast, tabs, accordion.

Notes

  • Field-grade Django form widgets for Multi-select, Tag input, Combobox, Date range, and File dropzone land in Phase 12 — Forms 2.0. The Phase 10 demos document the patterns; Phase 12 hardens them into forms.Widget subclasses.
v0.8.0

Major template-parity expansion. Closes the Tier-1, Tier-2, and Tier-3 gaps vs Metronic-class dashboard templates with 13 new phases of work.

Major template-parity expansion. Closes the Tier-1, Tier-2, and Tier-3

gaps vs Metronic-class dashboard templates with 13 new phases of work.

Added — Dashboards

  • 4 dashboard variants at /dashboards/{analytics,crm,ecommerce,saas}/, each with realistic mock data, theme-aware ApexCharts, and ~5 sections: - Analytics: page views chart, category revenue, top pages, top countries. - CRM: pipeline overview, deal stages donut, top sales reps, lead sources, recent deals, quarterly targets. - eCommerce: 30-day daily sales, order status, top products, sales by category, recent transactions, revenue targets. - SaaS: MRR/ARR growth, plan donut, marketing channels, user growth, recent signups, growth targets.
  • New "Dashboards" sidebar group bundling Overview + 4 variants.
  • 11 new ApexCharts factories in static/js/charts.js, all theme-aware.

Added — Apps

  • Projects + Tasks + Milestones (apps/projects/): full CRUD with 4-tab detail surface (Overview / Tasks / Team / Activity), kanban-style task board, milestone tracker, soft-delete archive. 6 seeded projects.
  • Public profile pages (apps/profiles/): rich user profile with 4 tabs (Overview / Projects / Activity / Connections), team directory, shared-project teammates view. New User fields: title, location, website.
  • Activity log (apps/activity/): workspace-wide event stream driven by signal hooks on customer/order/invoice/project/task creation + login. Date-grouped buckets, category + scope filters, KPI strip.
  • Subscription / billing portal (apps/billing/): Stripe-Customer- Portal-style surface with Subscription + PaymentMethod models, usage meters, plan comparison, payment-method management, cancel/reactivate.
  • Help center / knowledge base (apps/help/): Category + Article models, full-text search, related articles, view counters. 6 categories + 19 seeded articles.
  • Public blog (apps/blog/): Topic + Post models, featured hero, topic chips, search. 4 topics + 9 seeded posts with emoji covers.

Added — Showcase / status pages

  • Coming Soon page with live Alpine countdown timer + email signup.
  • Maintenance page (HTTP 503) with start + ETA timestamps.
  • 503 Service Unavailable with synthetic incident reference.
  • Forms gallery — every layout, input, validation state, choice control, size, and wizard-stepper pattern in one anchored page.
  • Widgets gallery — stat cards, badges, avatars, leaderboard, progress targets, timeline, button variants, empty states.
  • Datatable showcase with server-side sort/filter/search/paginate, column visibility, row density, bulk select, CSV export.
  • API docs page with sticky sidebar nav, copy-to-clipboard code blocks, method pills, webhooks, errors, rate limits, SDKs, changelog.
  • Maps page powered by Leaflet + OpenStreetMap (no API key required): customer markers + popups, MRR-proportional density circles, dark-mode tile filter. Leaflet loads only on this page via head_extra.

Added — Demo experience

  • Strong demo password (ApexShowcase!2026) replaces demo1234 — no more Chrome "weak password" warnings on signin.
  • DEMO_MODE setting auto-fills the login form's username and password fields and shows a small "Demo credentials" banner. Off in production by default; on in dev. Form-level autocomplete="off" + data-1p-ignore + data-lpignore so password managers don't offer to save the demo password.
  • seed_demo reads from settings.DEMO_USERNAME / DEMO_PASSWORD so rotating the demo password is a single setting change.
  • 11 new Lucide icons (eye, mouse-pointer, clock, trending-up, trophy, target, shopping-bag, rotate-ccw, user-minus, user-check, bar-chart-3, briefcase, check, map-pin) for the new pages.

Added — Tests

  • 188 new unit tests bring the total to 588 passing (was 400).
  • Coverage: model invariants, signal handlers, view filters, KPI math, CSV export shape, datatable sort + filter regressions, profile tab context counts, billing plan changes, payment-method scoping, help paragraph splitting, blog featured-hero behavior.

Changed

  • README rewritten with a feature table broken into 4 sections (Dashboards / Apps / Marketing & content / Showcase / Account) and a comprehensive screenshot gallery covering every new page.
  • capture_screenshots.py updated to capture all 50+ pages with dynamic resolvers for first-project / first-person / first-post slugs.
  • Updated demo credential references across 14 e2e test files, getting-started.md, README, and CHANGELOG.
  • Migrated nav from a flat list of 17 items to a structured 6-group sidebar (Dashboards / Commerce / Apps / Marketing / Showcase / Account) with 35 entries.
v0.1.0

Initial MVP release.

Initial MVP release.

Added

  • Django 5.1 project scaffolding with Tailwind CSS v4 and pytest harness.
  • Base template with Apex OKLCh design tokens, dark-mode flash prevention, and CDN-loaded Alpine + HTMX (with SRI hashes).
  • Sidebar navigation with 5 MVP items (Dashboard, Orders, Products, Users, Settings) via context processor + inline Lucide SVG icon templatetag.
  • Sticky header with theme toggle (persists via localStorage) and conditional logout form.
  • Custom User model with avatar, role (admin/manager/staff), and bio fields.
  • Auth flows: registration, login, logout, 4-step password reset.
  • Dashboard landing with stats cards (4-up), revenue chart (ApexCharts area + 7d/30d/90d range selector), side panel (traffic donut + goal progress bars), recent orders table, and activity feed.
  • Products module with Product + Category models and full CRUD (list, detail, create, edit).
  • Orders module with Order + OrderItem models, CRUD, and inline formset for line items (Alpine-driven add-row).
  • User management CRUD at /users/ gated to staff-only via StaffRequiredMixin.
  • Profile settings page at /settings/ where users edit their own profile.
  • Custom 403, 404, and 500 error pages.
  • seed_demo management command that populates a demo DB (demo / ApexShowcase!2026 login + 15 users + 25 products + 30 orders).
  • Playwright E2E smoke test suite (login, dashboard render, sidebar navigation, theme toggle, orders list).

Known limitations

  • Verify-email flow is deferred; registration auto-logs-in without verification.
  • Search input in the header is cosmetic (Phase 2).
  • Mobile sidebar has no hamburger toggle (Phase 2).