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>
86 lines
2.6 KiB
HTML
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 — 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 — 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>
|
|
— 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 %}
|