Files
chicken_babies_site/app/templates/admin/dashboard.html
Phillip Tarrant 9a8506970c feat: phase 4 admin CMS — dashboard, editor, media, CSRF
Head Hen CMS end-to-end: dashboard lists all posts (drafts + published),
Markdown editor with live preview + drag-drop image upload, Pillow media
pipeline re-encoding every upload to JPEG, post CRUD + publish toggle +
hard delete, About page edit, and double-submit CSRF cookie enforced on
every admin mutating endpoint (Phase 3's TODO markers resolved).

Slug auto-generated on create and server-locked once a post has been
published. Unpublish preserves `published_at` so re-publish keeps
original date ordering. Every admin write invalidates the read-side
Post/Page TTL caches and records an `auth_events` audit row.

CSRF middleware is narrow by design — issues/refreshes the `cb_csrf`
cookie only on `GET /admin*`, and mutating endpoints opt in via
`require_csrf_form` or `require_csrf_header` Depends. Public routes,
healthz, and pre-auth login stay untouched.

64 new tests cover slugs, CSRF, media, admin posts/pages services, and
end-to-end CMS routes. Tests never mock the DB — real temp SQLite files
per the CLAUDE.md mandate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:42:01 -05:00

86 lines
2.6 KiB
HTML

{#
Admin dashboard — post list + About edit link + new-post button.
Context:
- user : app.models.entities.User (required)
- posts : list[Post] (newest-updated first)
- about : app.models.entities.Page | None
- msg : str (PRG flash key)
- csrf_token : str (for the inline forms)
#}
{% extends "admin/base.html" %}
{% block title %}Dashboard &mdash; Admin{% endblock %}
{% block content %}
<section class="admin-dashboard">
<header class="admin-dashboard__header">
<h1 class="admin-dashboard__title">Dashboard</h1>
<p class="admin-dashboard__greeting">
Signed in as <code>{{ user.email }}</code>.
</p>
</header>
{% if msg %}
<p class="admin-flash admin-flash--ok" role="status">
{% if msg == "created" %}Post created.
{% elif msg == "saved" %}Changes saved.
{% elif msg == "deleted" %}Post deleted.
{% elif msg == "published" %}Post published.
{% elif msg == "unpublished" %}Post moved to draft.
{% else %}Done.
{% endif %}
</p>
{% endif %}
<section class="admin-dashboard__section">
<div class="admin-dashboard__section-head">
<h2>Posts</h2>
<a class="btn btn--primary" href="/admin/posts/new">New post</a>
</div>
{% if posts %}
<table class="post-table">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Slug</th>
<th scope="col">Status</th>
<th scope="col">Updated</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for post in posts %}
{% include "admin/_post_row.html" %}
{% endfor %}
</tbody>
</table>
{% else %}
<p class="post-list__empty">No posts yet &mdash; create one.</p>
{% endif %}
</section>
<section class="admin-dashboard__section">
<div class="admin-dashboard__section-head">
<h2>About page</h2>
</div>
{% if about %}
<p>
<strong>{{ about.title }}</strong>
&mdash; last updated
<time datetime="{{ about.updated_at.isoformat() }}">
{{ about.updated_at.strftime("%b %d, %Y") }}
</time>
</p>
<p>
<a class="btn btn--secondary" href="/admin/pages/about/edit">Edit About</a>
</p>
{% else %}
<p class="post-list__empty">About page is missing. Reseed to recover.</p>
{% endif %}
</section>
</section>
{% endblock %}