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.18.2

Admin prominence + slimmer distribution. Two same-day refinements on top of yesterday's 0.18.1.

Admin prominence + slimmer distribution. Two same-day refinements

on top of yesterday's 0.18.1.

Added

  • Header chip linking to /admin/ for staff users — a 36×36 shield-icon button sits between the theme toggle and the user menu, visible on every page. One click from anywhere in the app to the themed Django admin. The "Administration" sidebar entry under Account stays in place so the command palette still finds it.

Changed

  • Pruned 42 unused screenshots (~3.6 MB) — 19 stale root PNGs superseded by other variants (billing-cancel, notifications, kanban-dark, mail-thread, etc.) plus the entire screenshots/responsive/ subfolder (24 mobile/tablet artifacts generated by scripts/capture_responsive.py for spot-checking during development but not consumed by README, CHANGELOG, or any template). Working screenshots/ is now 36 files / 5.1 MB, all actually referenced. The capture script stays so anyone can regenerate later. Distribution zip drops from ~9.6 MB to ~6 MB.
v0.18.1

First-customer polish. Three small but visible fixes reported within hours of shipping 0.18.0 — bundled here as a same-day patch.

First-customer polish. Three small but visible fixes reported within

hours of shipping 0.18.0 — bundled here as a same-day patch.

Added

  • "Administration" sidebar entry under the Account group, linking directly to /admin/. Uses the shield icon (consistent with the admin breadcrumb), requires_staff=True, and keyword aliases (admin, django admin, permissions, groups, …) so the command palette finds it. Makes the themed /admin/ discoverable instead of hidden behind URL knowledge.

Fixed

  • screenshots/ was excluded from the distribution zip, so the bundled README.md's inline image links 404'd when the package was extracted locally. Re-included so README.md renders with all the marketing screenshots. Zip grew from ~1.4MB to ~9.5MB — worth it for the polished first impression.
  • Demo user was is_staff=True only, so visitors logging into the themed /admin/ saw zero models (Django requires per-model permissions, which a bare staff user lacks). Promoted to is_superuser=True in seed_demo so the demo showcases the full themed admin. Safe because the hourly reset_demo cron flushes + reseeds, reverting any visitor changes — including password or permission edits on the demo user itself. Already fixed on the live preview server in 0.18.0; this entry covers the distribution zip finally including the change.
  • Clock icon missing on SplitDateTime time-picker triggers. CSS mask selector targeted .time-icon, but Django actually emits .clock-icon (the .date-icon for the calendar trigger was already correct). Renamed so the Lucide clock glyph paints.
  • Changelist primary column rendered centered + bold. Django outputs the first column as <th scope="row">, and browsers default <th> to center-aligned bold. Forced left-align + normal weight so it matches the data columns.
  • Sortable column headers showed empty/broken controls. Django's multi-column sort UI (.sortoptions.sortremove, .sortpriority, .toggle) paints its icons from a sprite in admin CSS we don't load, so they rendered as blank anchors. Styled them as intentional controls: Lucide × for remove-from-sort, a pill priority badge, and chevron-up/down direction toggles, all via mask-image + currentColor.
v0.18.0

Apex-themed Django Admin. Django's /admin/ is now fully re-skinned to match the rest of Apex — same sidebar, header, dark mode, command palette, toast notifications, OKLCh color tokens. Drop-in apex.admin package gives existing admins the theme automatically and adds a ModelAdmin base class for opt-in extras.

Apex-themed Django Admin. Django's /admin/ is now fully re-skinned

to match the rest of Apex — same sidebar, header, dark mode, command

palette, toast notifications, OKLCh color tokens. Drop-in apex.admin

package gives existing admins the theme automatically and adds a

ModelAdmin base class for opt-in extras.

Added

  • apex.admin package — drop-in ModelAdmin, TabularInline, StackedInline base classes. Use them in place of admin.ModelAdmin to get Apex-specific knobs: - show_in_dashboard = True — surfaces a stat card on /admin/ with a Lucide icon, live count, and "View all" / "Add" actions. - apex_icon = "package" — Lucide icon name (see apps/core/templatetags/apex.py::ICONS) shown on the dashboard card and app-index list. - hide_readonly_on_add = True — auto-hides empty readonly metadata fields on add forms (e.g. created_at: - placeholders).
  • Themed admin templates (~20 files in templates/admin/): - Chromebase.html / base_site.html wrap every admin page in the Apex shell (sidebar + header + breadcrumbs + command palette + toast container). Dark-mode bootstrap matches the project base layout. - Auth — themed login (/admin/login/ reuses layouts/auth.html), password change (per-user + self-service), password change done, logged-out. - Index — opt-in stat cards via show_in_dashboard, themed apps & models grid with Lucide icons, recent actions feed. - CRUD — change list with empty states, change form with proper label/input grid alignment, delete confirmation, history page. - Popups — related-object add/edit (?_popup=1) skip the shell, use a sticky save bar, and auto-hide empty metadata rows. - Errors — themed 404 / 500 pages.
  • Themed widgets (templates/admin/widgets/ + scoped CSS): - Foreign key wrapper — Lucide edit / + / trash / eye icons replace the stock PNG buttons. - Select2 autocomplete — Apex-tinted dropdown, themed search input, multi-select chips, dropdown shadow + border-radius. - M2M filter_horizontal — bordered cards on both sides, themed filter inputs, Lucide arrow chips, "Choose all" / "Remove all" footer buttons. - SplitDateTime — colon-free Date / Time labels. - Calendar popup — Lucide chevron nav, themed day cells, today highlight, themed action row. - Date/time shortcuts — "Today" / "Now" chips next to date fields with Lucide calendar / clock icons. - File input — themed "Choose File" button with hover state. - Image previewImageField with existing value renders a bordered thumbnail card above the change controls (works project-wide, not just admin).
  • Scoped admin CSS (static_src/css/admin.css, ~1000 lines) — every rule under .apex-admin so the theme never leaks outside /admin/. Reuses Apex's OKLCh tokens (--background, --foreground, --primary, etc.) so dark mode works for free.
  • apex_admin templatetag library — walks admin.site._registry to build the dashboard cards and respects per-model view permissions.
  • field.label_tag|cut:":" everywhere via overridden admin/includes/fieldset.html — strips the trailing-colon convention from form labels (Name:Name).
  • Required-field asterisk — small destructive-coloured * suffix on <label class="required"> (rendered by Django for required form fields).

Changed

  • apex.admin registered in INSTALLED_APPS ABOVE django.contrib.admin so the project template loader picks up templates/admin/ overrides.
  • Sample admins (customers, orders, invoices, products) migrated to apex.admin.ModelAdmin and opt into show_in_dashboard with sensible Lucide icons.
  • Order admin gains autocomplete_fields = ("customer",) to exercise the themed Select2 dropdown.
  • Six new Lucide icons added to apps/core/templatetags/apex.py::ICONS: shield, shield-off, arrow-right, chevron-left, edit-3, box — used by the admin chrome and widget overrides.

Compatibility

  • Stock admin.ModelAdmin registrations still work — they pick up the theme via template overrides automatically. apex.admin.ModelAdmin is the hook for the new conveniences (dashboard cards, icons, hide_readonly_on_add); switching is purely opt-in.
  • All 1034 unit tests pass unchanged.
  • Third-party admin extensions (django-import-export, django-reversion, django-modeltranslation, django-mptt, etc.) ship their own admin templates and need targeted styling on top of the Apex theme when used.
v0.17.3

Python 3.14 support + AI-assistant onboarding docs.

Python 3.14 support + AI-assistant onboarding docs.

Changed

  • requires-python opened to <3.15 so Python 3.14 (released 2025-10) installs cleanly. Django 6.0 supports 3.12–3.14; Python 3.13 remains the recommended dev version.
  • Bumped whitenoise floor to >=6.12 and pytest-django to >=4.12 to pick up versions that classify Django 6.0 explicitly. No code changes required — full test suite (1034 unit tests) green.

Added

  • AGENTS.md, .cursor/rules/project.mdc, .github/copilot-instructions.md — onboarding docs for AI coding assistants. AGENTS.md follows the agents.md convention (Aider, Cline, Codex, Continue); the others target Cursor and Copilot respectively. Cover architecture, conventions, the TableView datatable system, Channels dispatch, Ninja API, organization scoping, mixin catalog, recipes, anti-patterns, and the seed_demo data manifest. Claude Code reads CLAUDE.md, which stays gitignored (local-only) by existing convention.
v0.17.1

Microsoft SQL Server support. Apex now officially supports SQL Server 2017+ as a production database alongside Postgres. Postgres remains the default — SQL Server is opt-in.

Microsoft SQL Server support. Apex now officially supports SQL Server

2017+ as a production database alongside Postgres. Postgres remains the

default — SQL Server is opt-in.

Added

  • mssql optional dependency group in pyproject.toml. Install with uv sync --extra mssql to pull Microsoft's official [mssql-django](https://github.com/microsoft/mssql-django) backend plus pyodbc. Customers who don't need SQL Server pay no install cost.
  • mssql:// / sqlserver:// schemes in apex/settings/prod.py's DATABASE_URL parser. Auto-detects the scheme and wires the right engine + ODBC driver options. Query params ?driver= and ?trust_server_certificate=yes let you target an older ODBC driver or skip cert validation against a self-signed local instance.
  • apex/settings/mssql.py — local-dev settings module pre-wired for SQL Server, configurable via MSSQL_HOST, MSSQL_PORT, MSSQL_DB, MSSQL_USER, MSSQL_PASSWORD, MSSQL_DRIVER env vars. Mirrors the pattern of apex.settings.dev (SQLite) but talks to SQL Server.
  • README "SQL Server" section with full setup recipe: install the ODBC driver, install the extra, set DATABASE_URL, migrate. Includes a docker-based local SQL Server recipe for testing before deploy.

Changed

  • .env.example now lists mssql:// as one of the supported DATABASE_URL schemes.
  • Production environment variables table in the README marks DATABASE_URL as accepting either postgres:// or mssql://.

Compatibility

  • Requires SQL Server 2017 or newer (matches mssql-django's baseline; JSONField requires 2016+ which is covered).
  • Requires Microsoft ODBC Driver 18 at the OS level. Driver 17 still works via ?driver=ODBC+Driver+17+for+SQL+Server.
  • Tested against SQL Server 2022 running in the official mcr.microsoft.com/mssql/server:2022-latest Docker image.
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).