Molodetz blogrol volgens DPP-template

This commit is contained in:
2026-10-05 09:36:20 +02:00
commit f290a4c766
277 changed files with 15758 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
<!-- retoor <retoor@molodetz.nl> -->
<a class="presence-host" href="/mensen/{{ _user.username }}" aria-label="{{ _user.username }}">
<img class="avatar" src="{{ avatar_url(_user, _size or 40) }}" width="{{ _size or 40 }}" height="{{ _size or 40 }}" alt="" loading="lazy">
<span class="presence-dot{% if is_online(_user.uid) %} online{% endif %}" data-presence-uid="{{ _user.uid }}"></span>
</a>
+6
View File
@@ -0,0 +1,6 @@
<!-- retoor <retoor@molodetz.nl> -->
<nav class="breadcrumbs" aria-label="Kruimelpad">
{% for name, path in seo.breadcrumbs %}
{% if not loop.last %}<a href="{{ path }}">{{ name }}</a><span aria-hidden="true">/</span>{% else %}<span aria-current="page">{{ name }}</span>{% endif %}
{% endfor %}
</nav>
+7
View File
@@ -0,0 +1,7 @@
<!-- retoor <retoor@molodetz.nl> -->
<div class="post-meta">
<span class="label">{{ TOPICS.get(_post.topic, _post.topic) }}</span>
{{ local_dt(_post.published_at or _post.created_at, 'date') }}
{% if _post.is_placeholder %}<span class="tag-placeholder" title="Overgenomen tekst; wordt vervangen door een eigen verhaal">Placeholder</span>{% endif %}
{% if _post.status != 'published' %}<span class="tag-placeholder tag-draft">Concept</span>{% endif %}
</div>
+7
View File
@@ -0,0 +1,7 @@
<!-- retoor <retoor@molodetz.nl> -->
<figure>
<a class="frame" href="{{ _item.url }}" data-lightbox data-caption="{{ _item.caption }}">
<img src="{{ _item.thumb_url or _item.url }}" alt="{{ _item.caption }}" width="{{ _item.width }}" height="{{ _item.height }}" loading="lazy">
</a>
<figcaption><span>{{ _item.caption }}</span><span class="index">{{ '%02d' % _index }}</span></figcaption>
</figure>
+4
View File
@@ -0,0 +1,4 @@
<!-- retoor <retoor@molodetz.nl> -->
{% if _next_url %}
<div class="load-more"><a class="button ghost" href="{{ _next_url }}" rel="next">Oudere notities</a></div>
{% endif %}
+9
View File
@@ -0,0 +1,9 @@
<!-- retoor <retoor@molodetz.nl> -->
{% macro modal(id, title, wide=False) %}
<div class="modal-overlay" id="{{ id }}" role="dialog" aria-modal="true" aria-labelledby="{{ id }}-title">
<div class="modal{% if wide %} wide{% endif %}">
<header><h2 id="{{ id }}-title">{{ title }}</h2><button class="modal-close" type="button" aria-label="Sluiten">&times;</button></header>
{{ caller() }}
</div>
</div>
{% endmacro %}
+8
View File
@@ -0,0 +1,8 @@
<!-- retoor <retoor@molodetz.nl> -->
{% if _pages and _pages.pages > 1 %}
<nav class="actions" aria-label="Paginering">
{% if _pages.has_prev %}<a class="button ghost small" href="?page={{ _pages.page - 1 }}" rel="prev">Vorige</a>{% endif %}
<span class="muted">{{ _pages.page }} / {{ _pages.pages }}</span>
{% if _pages.has_next %}<a class="button ghost small" href="?page={{ _pages.page + 1 }}" rel="next">Volgende</a>{% endif %}
</nav>
{% endif %}
+7
View File
@@ -0,0 +1,7 @@
<!-- retoor <retoor@molodetz.nl> -->
<article class="post-card">
{% set _post = _card %}
{% include "_content_header.html" %}
<h3><a href="/posts/{{ _card.slug }}">{{ render_title(_card.title) }}</a></h3>
<p>{{ content_preview(_card.body, 200) }}</p>
</article>
+5
View File
@@ -0,0 +1,5 @@
<!-- retoor <retoor@molodetz.nl> -->
<form class="field" method="get" action="/docs/search" role="search">
<label for="docs-q">Zoeken</label>
<input id="docs-q" type="search" name="q" value="{{ query or '' }}" placeholder="bijv. sessie">
</form>
+9
View File
@@ -0,0 +1,9 @@
<!-- retoor <retoor@molodetz.nl> -->
<div class="field">
<label for="topic">Onderwerp</label>
<select id="topic" name="topic">
{% for key, label in TOPICS.items() %}
<option value="{{ key }}" {% if _selected == key %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
+2
View File
@@ -0,0 +1,2 @@
<!-- retoor <retoor@molodetz.nl> -->
<a class="user-link" href="/mensen/{{ _user.username }}">{{ _user.username }}</a>
+17
View File
@@ -0,0 +1,17 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>Auditlog</h1>
<div class="table-wrap">
<table class="admin-table">
<thead><tr><th>Tijd</th><th>Gebeurtenis</th><th>Categorie</th><th>Actor</th><th>Resultaat</th></tr></thead>
<tbody>
{% for row in rows %}
<tr><td>{{ local_dt(row.created_at) }}</td><td><code>{{ row.event_key }}</code></td><td><a href="?category={{ row.category }}">{{ row.category }}</a></td><td>{{ row.actor_kind }}</td><td><span class="status {{ 'failed' if row.result == 'denied' else 'done' }}">{{ row.result }}</span></td></tr>
{% else %}
<tr><td colspan="5" class="muted">Nog geen regels.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+27
View File
@@ -0,0 +1,27 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>Back-ups</h1>
<form class="actions" method="post" action="/admin/backups">
<select name="target" aria-label="Doel">{% for target in ['database', 'uploads', 'keys', 'full'] %}<option value="{{ target }}">{{ target }}</option>{% endfor %}</select>
<button class="button" type="submit">Back-up inplannen</button>
</form>
<div class="table-wrap">
<table class="admin-table">
<thead><tr><th>Doel</th><th>Status</th><th>Grootte</th><th>Aangemaakt</th><th></th></tr></thead>
<tbody>
{% for row in backups %}
<tr>
<td>{{ row.target }}</td>
<td><span class="status {{ row.status }}">{{ row.status }}</span>{% if row.error %}<br><span class="muted">{{ row.error }}</span>{% endif %}</td>
<td>{{ row.size_label if row.size_bytes else '' }}</td>
<td>{{ local_dt(row.created_at) }}</td>
<td>{% if can_download and row.status == 'done' %}<a class="button ghost small" href="/admin/backups/{{ row.uid }}/download">Download</a>{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="5" class="muted">Nog geen back-ups.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+25
View File
@@ -0,0 +1,25 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block main_class %}wide{% endblock %}
{% block extra_head %}<link rel="stylesheet" href="{{ static_url('/static/css/admin.css') }}">{% block admin_head %}{% endblock %}{% endblock %}
{% block content %}
<div class="admin-layout">
<nav class="admin-nav" aria-label="Beheer">
<a href="/admin" class="{{ nav_active(request, '/admin', exact=True) }}">Overzicht</a>
<a href="/admin/posts" class="{{ nav_active(request, '/admin/posts') }}">Berichten</a>
<a href="/admin/joins" class="{{ nav_active(request, '/admin/joins') }}">Aanmeldingen</a>
<a href="/admin/services" class="{{ nav_active(request, '/admin/services') }}">Services</a>
<a href="/admin/backups" class="{{ nav_active(request, '/admin/backups') }}">Back-ups</a>
<a href="/admin/trash" class="{{ nav_active(request, '/admin/trash') }}">Prullenbak</a>
<a href="/admin/users" class="{{ nav_active(request, '/admin/users') }}">Accounts</a>
<a href="/admin/settings" class="{{ nav_active(request, '/admin/settings') }}">Instellingen</a>
<a href="/admin/audit" class="{{ nav_active(request, '/admin/audit') }}">Auditlog</a>
<a href="/admin/stats" class="{{ nav_active(request, '/admin/stats') }}">Statistieken</a>
<a href="/profile/api-key">API-sleutel</a>
<a href="/docs/beheer">Docs</a>
</nav>
<div class="admin-main">
{% block admin_content %}{% endblock %}
</div>
</div>
{% endblock %}
+12
View File
@@ -0,0 +1,12 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>Beheer</h1>
<div class="stat-cards">
<div class="stat-card"><span class="value">{{ post_count }}</span><span class="name">Berichten</span></div>
<div class="stat-card"><span class="value">{{ open_joins }}</span><span class="name">Open aanmeldingen</span></div>
<div class="stat-card"><span class="value">{{ user_count }}</span><span class="name">Accounts</span></div>
<div class="stat-card"><span class="value">{{ deleted_count }}</span><span class="name">In prullenbak</span></div>
</div>
<div class="actions"><a class="button" href="/admin/posts/new">Nieuw bericht</a><a class="button ghost" href="/admin/joins">Aanmeldingen bekijken</a></div>
{% endblock %}
+31
View File
@@ -0,0 +1,31 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>Aanmeldingen</h1>
<div class="table-wrap">
<table class="admin-table">
<thead><tr><th>Wie</th><th>Contact</th><th>Werk</th><th>Bericht</th><th>Status</th><th>Ontvangen</th><th></th></tr></thead>
<tbody>
{% for item in requests %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.contact }}</td>
<td>{% if item.repo_url %}<a href="{{ item.repo_url }}" rel="nofollow noopener" target="_blank">{{ item.repo_url }}</a>{% if item.link_status %}<br><span class="muted">{{ item.link_status }}{% if item.link_title %}: {{ item.link_title }}{% endif %}</span>{% endif %}{% else %}<span class="muted">geen</span>{% endif %}</td>
<td>{{ item.message }}</td>
<td><span class="status {{ item.status }}">{{ item.status }}</span></td>
<td>{{ local_dt(item.created_at) }}</td>
<td class="actions">
<form method="post" action="/admin/joins/{{ item.uid }}/status">
<select name="status" aria-label="Status">{% for value in ['open', 'contacted', 'accepted', 'declined'] %}<option value="{{ value }}" {% if item.status == value %}selected{% endif %}>{{ value }}</option>{% endfor %}</select>
<button class="button ghost small" type="submit">Zet</button>
</form>
{% if item.repo_url %}<form method="post" action="/admin/joins/{{ item.uid }}/check"><button class="button ghost small" type="submit" data-loading>Link controleren</button></form>{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="7" class="muted">Nog geen aanmeldingen.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+31
View File
@@ -0,0 +1,31 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>{{ 'Bericht bewerken' if post else 'Nieuw bericht' }}</h1>
<form class="form wide" method="post" action="{{ '/admin/posts/' ~ post.uid if post else '/admin/posts' }}" data-form>
<div class="field"><label for="title">Titel</label><input id="title" type="text" name="title" required minlength="3" maxlength="200" value="{{ post.title if post else '' }}"></div>
{% set _selected = post.topic if post else 'rol' %}
{% include "_topic_selector.html" %}
<div class="editor">
<div class="field">
<label for="body">Tekst (markdown)</label>
<textarea id="body" name="body" required minlength="10" data-preview-source>{{ post.body if post else '' }}</textarea>
<span class="help">Markdown met GFM. Emoji via :shortcode:. Voorbeeld rechts is een live voorbeeld; de gepubliceerde versie wordt op de server gerenderd.</span>
</div>
<div class="field">
<label>Voorbeeld</label>
<dp-content class="preview prose" data-preview-target></dp-content>
</div>
</div>
<div class="field">
<label for="status">Status</label>
<select id="status" name="status">
<option value="draft" {% if not post or post.status == 'draft' %}selected{% endif %}>Concept</option>
<option value="published" {% if post and post.status == 'published' %}selected{% endif %}>Gepubliceerd</option>
</select>
</div>
<label class="check"><input type="checkbox" name="is_placeholder" value="true" {% if post and post.is_placeholder %}checked{% endif %}> Markeer als placeholder</label>
<div class="actions"><button class="button" type="submit" data-loading>Opslaan</button><a class="button ghost" href="/admin/posts">Annuleren</a></div>
</form>
{% endblock %}
{% block extra_js %}<script type="module" src="{{ static_url('/static/js/utils/EditorPreview.js') }}"></script>{% endblock %}
+28
View File
@@ -0,0 +1,28 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>Berichten</h1>
<div class="actions"><a class="button" href="/admin/posts/new">Nieuw bericht</a></div>
<div class="table-wrap">
<table class="admin-table">
<thead><tr><th>Titel</th><th>Onderwerp</th><th>Status</th><th>Bijgewerkt</th><th></th></tr></thead>
<tbody>
{% for post in posts %}
<tr>
<td><a href="/posts/{{ post.slug }}">{{ plain_title(post.title) }}</a>{% if post.is_placeholder %} <span class="tag-placeholder">Placeholder</span>{% endif %}</td>
<td>{{ TOPICS.get(post.topic, post.topic) }}</td>
<td><span class="status {{ post.status }}">{{ post.status }}</span></td>
<td>{{ local_dt(post.updated_at) }}</td>
<td class="actions">
<a class="button ghost small" href="/admin/posts/{{ post.uid }}/edit">Bewerken</a>
<form method="post" action="/admin/posts/{{ post.uid }}/publish"><button class="button ghost small" type="submit">{{ 'Depubliceren' if post.status == 'published' else 'Publiceren' }}</button></form>
<form method="post" action="/admin/posts/{{ post.uid }}/delete" data-confirm="Bericht naar de prullenbak verplaatsen?"><button class="button danger small" type="submit">Verwijderen</button></form>
</td>
</tr>
{% else %}
<tr><td colspan="5" class="muted">Nog geen berichten.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+53
View File
@@ -0,0 +1,53 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>{{ service.title }}</h1>
<p class="muted">{{ service.description }}</p>
<div data-service-monitor="detail" data-service="{{ service.name }}">
<div class="tabs" role="tablist" data-tabs>
<button type="button" class="active" data-tab="overview">Overzicht</button>
<button type="button" data-tab="config">Configuratie</button>
<button type="button" data-tab="logs">Logs</button>
</div>
<section data-panel="overview">
<p>Status: <span class="status {{ service.status }}" data-field="status">{{ service.status }}</span></p>
<div class="stat-cards" data-field="cards">
{% for card in service.metrics.cards or [] %}<div class="stat-card"><span class="value">{{ card.value }}</span><span class="name">{{ card.label }}</span></div>{% endfor %}
</div>
<div class="actions">
<form method="post" action="/admin/services/{{ service.name }}/command"><input type="hidden" name="verb" value="{{ 'stop' if service.enabled else 'start' }}"><button class="button ghost small" type="submit">{{ 'Stop' if service.enabled else 'Start' }}</button></form>
<form method="post" action="/admin/services/{{ service.name }}/command"><input type="hidden" name="verb" value="run"><button class="button ghost small" type="submit">Nu draaien</button></form>
<form method="post" action="/admin/services/{{ service.name }}/command"><input type="hidden" name="verb" value="clear"><button class="button ghost small" type="submit">Logs wissen</button></form>
</div>
</section>
<section data-panel="config" hidden>
<form class="form" method="post" action="/admin/services/{{ service.name }}/config">
{% for group in service.fields | map(attribute='group') | unique %}
<span class="label">{{ group }}</span>
{% for field in service.fields if field.group == group %}
<div class="field">
<label for="f-{{ field.key }}">{{ field.label }}</label>
{% if field.type in ('bool', 'select') %}
<select id="f-{{ field.key }}" name="{{ field.key }}">
{% for option in (field.options if field.type == 'select' else ['0', '1']) %}
<option value="{{ option }}" {% if field.value == option | string %}selected{% endif %}>{{ option }}</option>
{% endfor %}
</select>
{% elif field.type == 'text' %}
<textarea id="f-{{ field.key }}" name="{{ field.key }}">{{ field.value }}</textarea>
{% else %}
<input id="f-{{ field.key }}" type="{{ 'password' if field.secret else ('number' if field.type == 'int' else 'text') }}" name="{{ field.key }}" value="{{ field.value }}" {% if field.minimum is not none %}min="{{ field.minimum }}"{% endif %} {% if field.maximum is not none %}max="{{ field.maximum }}"{% endif %}>
{% endif %}
{% if field.help %}<span class="help">{{ field.help }}</span>{% endif %}
</div>
{% endfor %}
{% endfor %}
<div class="actions"><button class="button" type="submit">Opslaan</button></div>
</form>
</section>
<section data-panel="logs" hidden>
<pre class="log-tail" data-field="logs">{{ service.logs | join('\n') }}</pre>
</section>
</div>
{% endblock %}
{% block extra_js %}<script type="module" src="{{ static_url('/static/js/utils/ServiceMonitor.js') }}"></script>{% endblock %}
+25
View File
@@ -0,0 +1,25 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>Services</h1>
<div class="table-wrap">
<table class="admin-table" data-service-monitor="index">
<thead><tr><th>Service</th><th>Status</th><th>Laatste run</th><th>Volgende run</th><th></th></tr></thead>
<tbody>
{% for service in services %}
<tr data-service="{{ service.name }}">
<td><a href="/admin/services/{{ service.name }}">{{ service.title }}</a><br><span class="muted">{{ service.description }}</span></td>
<td><span class="status {{ service.status }}" data-field="status">{{ service.status }}</span></td>
<td data-field="last_run">{{ local_dt(service.last_run) }}</td>
<td data-field="next_run">{{ local_dt(service.next_run) }}</td>
<td class="actions">
<form method="post" action="/admin/services/{{ service.name }}/command"><input type="hidden" name="verb" value="{{ 'stop' if service.enabled else 'start' }}"><button class="button ghost small" type="submit">{{ 'Stop' if service.enabled else 'Start' }}</button></form>
<form method="post" action="/admin/services/{{ service.name }}/command"><input type="hidden" name="verb" value="run"><button class="button ghost small" type="submit">Nu draaien</button></form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block extra_js %}<script type="module" src="{{ static_url('/static/js/utils/ServiceMonitor.js') }}"></script>{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>Instellingen</h1>
<form class="form" method="post" action="/admin/settings">
{% for key, value in settings.items() %}
<div class="field">
<label for="s-{{ key }}">{{ key }}</label>
{% if key in ('maintenance_mode', 'friendly_404') %}
<select id="s-{{ key }}" name="{{ key }}"><option value="0" {% if value == '0' %}selected{% endif %}>0</option><option value="1" {% if value == '1' %}selected{% endif %}>1</option></select>
{% else %}
<input id="s-{{ key }}" type="text" name="{{ key }}" value="{{ value }}">
{% endif %}
</div>
{% endfor %}
<div class="actions"><button class="button" type="submit">Opslaan</button></div>
</form>
{% endblock %}
+19
View File
@@ -0,0 +1,19 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>Statistieken</h1>
<div class="tabs" role="tablist" data-tabs>
<button type="button" class="active" data-tab="paths">Paden</button>
<button type="button" data-tab="days">Per dag</button>
</div>
<section data-panel="paths">
<table class="admin-table"><thead><tr><th>Pad</th><th>Hits</th></tr></thead><tbody>
{% for row in top_paths %}<tr><td>{{ row.path }}</td><td>{{ row.hits }}</td></tr>{% else %}<tr><td colspan="2" class="muted">Nog geen data.</td></tr>{% endfor %}
</tbody></table>
</section>
<section data-panel="days" hidden>
<table class="admin-table"><thead><tr><th>Dag</th><th>Hits</th></tr></thead><tbody>
{% for row in daily %}<tr><td>{{ format_date(row.day) }}</td><td>{{ row.hits }}</td></tr>{% else %}<tr><td colspan="2" class="muted">Nog geen data.</td></tr>{% endfor %}
</tbody></table>
</section>
{% endblock %}
+26
View File
@@ -0,0 +1,26 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>Prullenbak</h1>
<div class="table-wrap">
<table class="admin-table">
<thead><tr><th>Soort</th><th>Item</th><th>Verwijderd</th><th>Door</th><th></th></tr></thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.table }}</td>
<td>{{ item.label }}</td>
<td>{{ local_dt(item.deleted_at) }}</td>
<td>{{ item.deleted_by }}</td>
<td class="actions">
<form method="post" action="/admin/trash/restore"><input type="hidden" name="stamp" value="{{ item.deleted_at }}"><button class="button ghost small" type="submit">Herstellen</button></form>
<form method="post" action="/admin/trash/purge" data-confirm="Definitief verwijderen? Dit kan niet ongedaan worden gemaakt."><input type="hidden" name="stamp" value="{{ item.deleted_at }}"><button class="button danger small" type="submit">Definitief</button></form>
</td>
</tr>
{% else %}
<tr><td colspan="5" class="muted">De prullenbak is leeg.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+25
View File
@@ -0,0 +1,25 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "admin/base_admin.html" %}
{% block admin_content %}
<h1>Accounts</h1>
<div class="table-wrap">
<table class="admin-table">
<thead><tr><th>Naam</th><th>Rol</th><th>Sinds</th><th></th></tr></thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}{% if is_primary_admin(user) %} <span class="muted">(primair)</span>{% endif %}</td>
<td>{{ user.role }}</td>
<td>{{ local_dt(user.created_at, 'date') }}</td>
<td>
<form class="actions" method="post" action="/admin/users/{{ user.uid }}/role">
<select name="role" aria-label="Rol"><option value="Admin" {% if user.role == 'Admin' %}selected{% endif %}>Admin</option><option value="Member" {% if user.role == 'Member' %}selected{% endif %}>Member</option></select>
<button class="button ghost small" type="submit">Zet</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
<header class="article-header">
<span class="label">Beheer</span>
<h1>Inloggen</h1>
</header>
{% if errors %}
<div class="notice error" role="alert"><ul>{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul></div>
{% endif %}
<form class="form" method="post" action="/auth/login">
<input type="hidden" name="next" value="{{ next or '/admin' }}">
<div class="field"><label for="username">Naam of e-mail</label><input id="username" type="text" name="username" required value="{{ (form or {}).username or '' }}" autocomplete="username"></div>
<div class="field"><label for="password">Wachtwoord</label><input id="password" type="password" name="password" required minlength="6" maxlength="128" autocomplete="current-password"></div>
<label class="check"><input type="checkbox" name="remember" value="true"> Onthoud mij 30 dagen</label>
<div class="actions"><button class="button" type="submit" data-loading>Inloggen</button></div>
</form>
{% endblock %}
+87
View File
@@ -0,0 +1,87 @@
<!-- retoor <retoor@molodetz.nl> -->
{% set seo = seo or {} %}
{% set viewer = current_user(request) %}
<!doctype html>
<html lang="nl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>{{ seo.title or app_title }}</title>
<meta name="description" content="{{ seo.description or '' }}">
<meta name="robots" content="{{ seo.robots or 'index,follow' }}">
<meta name="keywords" content="{{ seo.keywords or '' }}">
<meta name="asset-version" content="{{ static_version }}">
<meta name="theme-color" content="#0d0c0a">
{% if seo.canonical %}<link rel="canonical" href="{{ seo.canonical }}">{% endif %}
{% if seo.prev_url %}<link rel="prev" href="{{ seo.prev_url }}">{% endif %}
{% if seo.next_url %}<link rel="next" href="{{ seo.next_url }}">{% endif %}
<meta property="og:site_name" content="{{ app_title }}">
<meta property="og:title" content="{{ seo.title or app_title }}">
<meta property="og:description" content="{{ seo.description or '' }}">
<meta property="og:type" content="{{ seo.og_type or 'website' }}">
{% if seo.canonical %}<meta property="og:url" content="{{ seo.canonical }}">{% endif %}
{% if seo.og_image %}<meta property="og:image" content="{{ seo.og_image }}">{% endif %}
<meta property="og:locale" content="nl_NL">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ seo.title or app_title }}">
<meta name="twitter:description" content="{{ seo.description or '' }}">
{% for block_json in seo.json_ld or [] %}
<script type="application/ld+json">{{ block_json | safe }}</script>
{% endfor %}
<link rel="icon" href="{{ static_url('/static/media/favicon.svg') }}" type="image/svg+xml">
<link rel="stylesheet" href="{{ static_url('/static/css/variables.css') }}">
<link rel="stylesheet" href="{{ static_url('/static/css/base.css') }}">
<link rel="stylesheet" href="{{ static_url('/static/css/layout.css') }}">
<link rel="stylesheet" href="{{ static_url('/static/css/content.css') }}">
<link rel="stylesheet" href="{{ static_url('/static/css/forms.css') }}">
<link rel="stylesheet" href="{{ static_url('/static/css/components.css') }}">
<link rel="modulepreload" href="{{ static_url('/static/js/Application.js') }}">
<link rel="modulepreload" href="{{ static_url('/static/js/components/index.js') }}">
{% block extra_head %}{% endblock %}
</head>
<body>
<a class="sr-only" href="#main">Naar inhoud</a>
<header class="site-header">
<nav class="site-nav" aria-label="Hoofdmenu">
<a class="brand" href="/">MOLODETZ</a>
<button class="nav-toggle" type="button" data-nav-toggle aria-expanded="false" aria-controls="nav-links">Menu</button>
<div class="nav-links" id="nav-links">
<a href="/rol" class="{{ nav_active(request, '/rol') }}">Rol</a>
<a href="/standaard" class="{{ nav_active(request, '/standaard') }}">Standaard</a>
<a href="/flyers" class="{{ nav_active(request, '/flyers') }}">Flyers</a>
<a href="/memes" class="{{ nav_active(request, '/memes') }}">Memes</a>
<a href="/mensen" class="{{ nav_active(request, '/mensen') }}">Mensen</a>
<a href="/binnen" class="{{ nav_active(request, '/binnen') }}">Binnen</a>
{% if viewer %}
<span class="nav-account">
{% if is_admin(viewer) %}<a href="/admin" class="{{ nav_active(request, '/admin') }}">Beheer</a>{% endif %}
<a href="/notifications" class="{{ nav_active(request, '/notifications') }}">Meldingen{% set unread = unread_count(request) %}{% if unread %}<span class="badge-count">{{ unread }}</span>{% endif %}</a>
<form method="post" action="/auth/logout"><button type="submit">Uit</button></form>
</span>
{% endif %}
</div>
</nav>
</header>
<main id="main" class="page {% block main_class %}{% endblock %}">
{% if seo.breadcrumbs and seo.breadcrumbs | length > 1 %}
{% include "_breadcrumbs.html" %}
{% endif %}
{% block content %}{% endblock %}
</main>
<footer class="site-footer">
<div class="inner">
<span class="tagline">Molodetz. {{ setting('site_tagline') }}</span>
<a href="/docs">Docs</a>
<a href="/voorwaarden">Voorwaarden</a>
<a href="/privacy">Privacy</a>
{% if not viewer %}<a href="/auth/login" rel="nofollow">Inloggen</a>{% endif %}
<span>&copy; {{ current_year() }}</span>
</div>
</footer>
<dp-toast></dp-toast>
<dp-lightbox></dp-lightbox>
<span class="response-time" title="Server-renderingstijd">{{ response_time(request) }}</span>
<script type="module" src="{{ static_url('/static/js/Application.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
<header class="article-header">
<span class="label">Binnen</span>
<h1>Ik schrijf mee</h1>
<p class="muted">De rol is open voor mensen, niet voor een publiek. Zeg wie je bent en hoe we je bereiken. Een link naar je werk helpt, maar is niet verplicht.</p>
</header>
{% if submitted %}
<div class="notice success" role="status">Genoteerd. retoor leest elke aanmelding zelf en neemt contact op.</div>
{% endif %}
{% if errors %}
<div class="notice error" role="alert"><ul>{% for error in errors %}<li>{{ error }}</li>{% endfor %}</ul></div>
{% endif %}
<form class="form" method="post" action="/binnen" data-form>
<div class="field">
<label for="name">Naam</label>
<input id="name" type="text" name="name" required minlength="2" maxlength="80" value="{{ form.name or '' }}" autocomplete="name">
</div>
<div class="field">
<label for="contact">Contact</label>
<input id="contact" type="text" name="contact" required minlength="3" maxlength="255" value="{{ form.contact or '' }}" placeholder="e-mail, Matrix of Signal">
<span class="help">Alleen zichtbaar voor beheer.</span>
</div>
<div class="field">
<label for="repo_url">Repo of werk (optioneel)</label>
<input id="repo_url" type="text" name="repo_url" maxlength="500" value="{{ form.repo_url or '' }}" placeholder="https://">
</div>
<div class="field">
<label for="message">Waarover wil je schrijven? (optioneel)</label>
<textarea id="message" name="message" maxlength="2000" rows="4">{{ form.message or '' }}</textarea>
</div>
<div class="honeypot" aria-hidden="true"><label for="website">Website</label><input id="website" type="text" name="website" tabindex="-1" autocomplete="off"></div>
<p class="muted">Door te versturen ga je akkoord met de <a href="/privacy">privacyverklaring</a>. We bewaren alleen wat je hier invult.</p>
<div class="actions"><button class="button" type="submit" data-loading>Ik schrijf mee</button></div>
</form>
{% endblock %}
+52
View File
@@ -0,0 +1,52 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "docs/base_docs.html" %}
{% block docs_content %}
<h1>API: {{ group.title }}</h1>
<p>{{ group.description }}</p>
<p class="muted">Elke HTML-route geeft JSON terug met <code>Accept: application/json</code>. Authenticatie: sessiecookie, <code>X-API-KEY</code>, <code>Authorization: Bearer</code> of <code>Basic</code>.</p>
{% for item in endpoints %}
<section class="endpoint" data-endpoint data-method="{{ item.method }}" data-path="{{ item.path }}" data-auth="{{ item.auth }}" data-negotiation="{{ item.negotiation }}" data-destructive="{{ '1' if item.destructive else '0' }}">
<div class="signature"><span class="method">{{ item.method }}</span><code>{{ item.path }}</code><span class="auth">{{ item.min_role }}</span></div>
<strong>{{ item.title }}</strong>
{% if item.description %}<p>{{ item.description }}</p>{% endif %}
{% if item.fields %}
<table class="admin-table"><thead><tr><th>Veld</th><th>Type</th><th>Plek</th><th>Verplicht</th></tr></thead><tbody>
{% for f in item.fields %}<tr><td><code>{{ f.name }}</code>{% if f.enum %}<div class="allowed">Toegestaan: {{ f.enum | join(', ') }}</div>{% endif %}</td><td>{{ f.type }}</td><td>{{ f.location }}</td><td>{{ 'ja' if f.required else 'nee' }}</td></tr>{% endfor %}
</tbody></table>
{% endif %}
<div class="tabs" data-tabs>
<button type="button" class="active" data-tab="curl">cURL</button>
<button type="button" data-tab="js">JS</button>
<button type="button" data-tab="python">Python</button>
<button type="button" data-tab="expected">Verwacht</button>
{% if item.interactive %}<button type="button" data-tab="live">Live</button>{% endif %}
</div>
<pre data-panel="curl">{{ item.curl }}</pre>
<pre data-panel="js" hidden>const response = await fetch("{{ item.path }}", { method: "{{ item.method }}", headers: { "Accept": "application/json" } });
console.log(await response.json());</pre>
<pre data-panel="python" hidden>import httpx
print(httpx.request("{{ item.method }}", BASE + "{{ item.path }}", headers={"Accept": "application/json", "X-API-KEY": API_KEY}).json())</pre>
<pre data-panel="expected" hidden>{{ item.sample | tojson(indent=2) if item.sample is not none else 'Geen voorbeeld' }}</pre>
{% if item.interactive %}
<div data-panel="live" hidden class="tester">
{% for f in item.fields %}
<div class="field"><label>{{ f.name }} ({{ f.location }})</label>
{% if f.enum %}<select data-param="{{ f.name }}" data-location="{{ f.location }}">{% for option in f.enum %}<option>{{ option }}</option>{% endfor %}</select>
{% else %}<input type="text" data-param="{{ f.name }}" data-location="{{ f.location }}" value="{{ f.example or '' }}">{% endif %}
</div>
{% endfor %}
<div class="actions"><button class="button small" type="button" data-send {% if item.auth != 'public' and not viewer %}disabled title="Log in om te versturen"{% endif %}>Verstuur</button></div>
<pre data-response>Nog niet verstuurd.</pre>
</div>
{% endif %}
</section>
{% endfor %}
{% if services %}
<h2>Live services</h2>
{% for service in services %}
<section class="endpoint"><strong>{{ service.title }}</strong> <code>{{ service.name }}</code><p>{{ service.description }}</p>
<p class="muted">Velden: {% for f in service.fields %}<code>{{ f.key }}</code> ({{ f.type }}){% if not loop.last %}, {% endif %}{% endfor %}</p></section>
{% endfor %}
{% endif %}
{% endblock %}
{% block extra_js %}<script type="module" src="{{ static_url('/static/js/utils/ApiTester.js') }}"></script>{% endblock %}
+32
View File
@@ -0,0 +1,32 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block main_class %}wide{% endblock %}
{% block extra_head %}<link rel="stylesheet" href="{{ static_url('/static/css/docs.css') }}">{% endblock %}
{% block content %}
<div class="docs-layout">
<aside class="docs-sidebar">
{% include "_sidebar_search.html" %}
{% for section, pages in sidebar %}
{% if pages %}
<div>
<h4>{{ section }}</h4>
<ul>{% for page in pages %}<li><a href="/docs/{{ page.slug }}" class="{{ nav_active(request, '/docs/' ~ page.slug, exact=True) }}">{{ page.title }}</a></li>{% endfor %}</ul>
</div>
{% endif %}
{% if section == 'Bouwen met de API' %}
<div>
<h4>API-groepen</h4>
<ul>{% for group in api_groups %}<li><a href="/docs/api/{{ group.slug }}" class="{{ nav_active(request, '/docs/api/' ~ group.slug, exact=True) }}">{{ group.title }}</a></li>{% endfor %}</ul>
</div>
{% endif %}
{% endfor %}
<div>
<h4>Downloads</h4>
<ul><li><a href="/docs/download.md">download.md</a></li><li><a href="/docs/download.html">download.html</a></li><li><a href="/voorwaarden">Voorwaarden</a></li><li><a href="/privacy">Privacy</a></li><li><a href="/swagger">OpenAPI</a></li></ul>
</div>
</aside>
<article class="docs-content prose">
{% block docs_content %}{% endblock %}
</article>
</div>
{% endblock %}
+5
View File
@@ -0,0 +1,5 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "docs/base_docs.html" %}
{% block docs_content %}
<div data-docs-content>{{ body | safe }}</div>
{% endblock %}
+5
View File
@@ -0,0 +1,5 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "docs/base_docs.html" %}
{% block docs_content %}
<div data-docs-content>{{ body | safe }}</div>
{% endblock %}
+24
View File
@@ -0,0 +1,24 @@
# API en authenticatie
Elke route heeft vier gezichten: HTML, JSON, documentatie en (waar geselecteerd) een assistent-tool. Vraag JSON met `Accept: application/json`.
## Authenticatie
Volgorde van resolutie:
1. `session` cookie (64 hex tekens) na inloggen op `/auth/login`.
2. `X-API-KEY` header met je API-sleutel.
3. `Authorization: Bearer <sleutel>`.
4. `Authorization: Basic` met naam of e-mail en wachtwoord.
Er is geen JWT en geen OAuth.
## Voorbeeld
```bash
curl -H 'Accept: application/json' https://molodetz.nl/rol
```
## Fouten
Fouten hebben de vorm `{"error": {"status": 404, "message": "..."}}`. Validatiefouten geven `422` met `{"error": "validation", "fields": [...], "messages": [...]}`.
@@ -0,0 +1,18 @@
# Architectuur
FastAPI met Jinja2, server-side gerenderd. SQLite via `dataset`, synchroon en bewust zonder threadpool. Sessies met PBKDF2-SHA256 wachtwoorden.
## Lagen
- `molodetz/database/`: één bestand per onderwerp, importeer altijd vanaf de package-root.
- `molodetz/routers/`: spiegelt het URL-pad.
- `molodetz/services/`: achtergrondservices onder één supervisor met een bestandsslot.
- `molodetz/templates/`: één Jinja-omgeving in `templating.py`.
## Verwijderen
Alles wordt zacht verwijderd met `deleted_at` en `deleted_by`. Alleen opruimtaken verwijderen hard.
## Statische bestanden
Assets lopen via `static_url()`, wat ze versioneert onder `/static/v<versie>/`. Die zijn een jaar onveranderlijk cachebaar.
+14
View File
@@ -0,0 +1,14 @@
# Back-ups en onderhoud
Doelen: `database`, `uploads`, `keys`, `full`. Archieven worden gecomprimeerd met zstd of brotli en staan onder `data/backups/`. Alleen de primaire beheerder kan downloaden.
## CLI
```bash
molodetz backups list
molodetz backups run --target database
molodetz backups prune --keep 14 --dry-run
molodetz system prune --dry-run
```
Het onderhoudsscript `bin/maintenance.sh` draait alle prunes en toont schijfgebruik voor en na.
+13
View File
@@ -0,0 +1,13 @@
# Beheer en services
Onder *Beheer, Services* staan alle achtergrondservices. Elke service heeft een overzicht, configuratie en logs. Starten, stoppen, nu draaien en logs wissen zijn databaseschrijfacties waar de service binnen een seconde op reageert.
| Service | Doel |
|---------|------|
| presence | publiceert de online-set |
| housekeeping | ruimt verlopen sessies op, snoeit het auditlog |
| backup | verwerkt de back-upwachtrij en plant automatische back-ups |
## Onderhoudsmodus
Zet `maintenance_mode` op `1` onder *Instellingen*. Beheerders blijven alles zien.
+5
View File
@@ -0,0 +1,5 @@
# Binnenkomen
Het formulier op [/binnen](/binnen) slaat een aanmelding op: naam, contact, optioneel een link naar je werk en een bericht. Beheerders krijgen een melding en zien de aanmelding onder *Beheer, Aanmeldingen*.
Er is geen automatische toegang. retoor leest elke aanmelding en neemt zelf contact op.
@@ -0,0 +1,5 @@
# dp-avatar
Toont een deterministische SVG-avatar plus presence-stip. Attribuut `seed`, `size`, `uid`.
Light DOM, geen shadow DOM. Registreert zichzelf met `customElements.define`.
@@ -0,0 +1,5 @@
# dp-chat
Eenvoudige log met invoerregel. Geeft een `dp-chat-send` event.
Light DOM, geen shadow DOM. Registreert zichzelf met `customElements.define`.
@@ -0,0 +1,5 @@
# dp-code
Markeert een codeblok met highlight.js en voegt een Kopieer-knop toe.
Light DOM, geen shadow DOM. Registreert zichzelf met `customElements.define`.
@@ -0,0 +1,5 @@
# dp-content
Rendert live markdown aan de clientkant via marked, daarna DOMPurify. Alleen voor live inhoud, zoals het editorvoorbeeld.
Light DOM, geen shadow DOM. Registreert zichzelf met `customElements.define`.
@@ -0,0 +1,5 @@
# dp-context-menu
Klein contextmenu met knoppen, gepositioneerd bij de cursor.
Light DOM, geen shadow DOM. Registreert zichzelf met `customElements.define`.
@@ -0,0 +1,5 @@
# dp-dialog
Bevestigingsdialoog. Formulieren met `data-confirm` lopen hierlangs.
Light DOM, geen shadow DOM. Registreert zichzelf met `customElements.define`.
@@ -0,0 +1,5 @@
# dp-lightbox
Volledig scherm voor afbeeldingen met `data-lightbox`. Escape of klik sluit.
Light DOM, geen shadow DOM. Registreert zichzelf met `customElements.define`.
@@ -0,0 +1,5 @@
# dp-title
Inline-variant van dp-content voor titels.
Light DOM, geen shadow DOM. Registreert zichzelf met `customElements.define`.
@@ -0,0 +1,5 @@
# dp-toast
Host voor korte meldingen. Gebruik `Toast.flash(el, bericht)` of `app.toast(bericht)`.
Light DOM, geen shadow DOM. Registreert zichzelf met `customElements.define`.
@@ -0,0 +1,5 @@
# dp-upload
Sleepvlak voor bestanden. Geeft een `dp-files` event met de gekozen bestanden.
Light DOM, geen shadow DOM. Registreert zichzelf met `customElements.define`.
@@ -0,0 +1,7 @@
# Frontend en componenten
ES6-modules, één klasse per bestand, geen framework en geen bundler. `Application.js` maakt alle singletons aan en zet `window.app`.
Componenten zijn custom elements met het voorvoegsel `dp-`, zonder shadow DOM. Zie de componentpagina's in de zijbalk.
Clientrendering met marked en DOMPurify is alleen voor live inhoud: het voorbeeld in de editor. Gepubliceerde berichten worden op de server gerenderd.
@@ -0,0 +1,6 @@
# Voorwaarden en privacy
- [Voorwaarden](/voorwaarden)
- [Privacy](/privacy)
Molodetz heeft geen advertenties, geen tracking over apps heen, geen betalingen en geen sociale login.
@@ -0,0 +1,17 @@
# Schrijven op de rol
Berichten zijn markdown en staan in SQLite. Alleen beheerders schrijven. Een bericht heeft een titel, een onderwerp (`rol` of `standaard`), een status (`draft` of `published`) en optioneel de markering *placeholder*.
## Markdown
De server rendert met GFM: tabellen, doorhalen, harde regeleinden. HTML in de tekst wordt ge-escaped. Links mogen alleen `http`, `https`, `mailto` en `tel` zijn. Emoji schrijf je als `:shortcode:`, bijvoorbeeld `:fire:`.
Een losse afbeeldings-URL wordt een afbeelding, `@naam` wordt een link naar het profiel.
## Placeholders
De eerste notities zijn overgenomen uit de oude Molodetz-pagina en dragen het label *Placeholder*. retoor vervangt ze door eigen verhalen. Haal het vinkje weg bij het bewerken zodra een bericht echt is.
## Slugs
Een slug is permanent: titel plus de laatste twaalf tekens van de uid. Bewerken verandert de slug niet.
+15
View File
@@ -0,0 +1,15 @@
# Welkom bij Molodetz
Molodetz is een rustige blogrol. Notities over het werk, het afmaken en het oordeel. Deze documentatie beschrijft hoe de site werkt, hoe je de API gebruikt en hoe beheer de boel draaiende houdt.
## Wat er is
- **Rol**: korte notities, nieuwste eerst.
- **Standaard**: waar Molodetz op staat.
- **Flyers** en **Memes**: twee galerijen.
- **Mensen**: wie er schrijft.
- **Binnen**: het formulier om mee te schrijven.
## Waar te beginnen
Lees [Schrijven op de rol](/docs/schrijven) voor de inhoud, [API en authenticatie](/docs/api) voor integraties, en [Architectuur](/docs/architectuur) als je wilt weten hoe het gebouwd is.
+14
View File
@@ -0,0 +1,14 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "docs/base_docs.html" %}
{% block docs_content %}
<h1>Zoeken</h1>
{% if query %}<p class="muted">{{ results | length }} resultaten voor <strong>{{ query }}</strong></p>{% endif %}
{% for hit in results %}
<div class="search-hit">
<a href="{{ hit.url }}">{{ hit.title }}</a> <span class="muted">{{ hit.score }}</span>
<p>{{ hit.snippet | e | replace('[[', '<mark>') | replace(']]', '</mark>') | safe }}</p>
</div>
{% else %}
{% if query %}<p class="empty">Niets gevonden.</p>{% endif %}
{% endfor %}
{% endblock %}
+9
View File
@@ -0,0 +1,9 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
<section class="error-page">
<div class="code">{{ status_code }}</div>
<p class="lead">{{ message }}</p>
<div class="actions"><a class="button ghost" href="/">Naar de voorpagina</a><a class="button ghost" href="/rol">Naar de rol</a></div>
</section>
{% endblock %}
+19
View File
@@ -0,0 +1,19 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block main_class %}wide{% endblock %}
{% block extra_head %}<link rel="stylesheet" href="{{ static_url('/static/css/gallery.css') }}">{% endblock %}
{% block content %}
<header class="article-header">
<span class="label">{{ 'Gezet in serif' if kind == 'flyer' else 'Lichtelijk provocerend' }}</span>
<h1>{{ title }}</h1>
</header>
<p class="gallery-intro">{% if kind == 'flyer' %}Rustig gezet, scherp bedoeld. Klik voor het volle formaat.{% else %}Niet om te schreeuwen. Om even te blijven hangen.{% endif %}</p>
<div class="gallery {{ 'flyers' if kind == 'flyer' else 'memes' }}">
{% for item in items %}
{% set _item = item %}{% set _index = loop.index %}
{% include "_gallery_item.html" %}
{% else %}
<p class="empty">Nog geen beelden.</p>
{% endfor %}
</div>
{% endblock %}
+38
View File
@@ -0,0 +1,38 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
<section class="hero">
<span class="label">Een blogrol</span>
<h1>Molodetz</h1>
<p class="lead">Een kleine kring die schrijft over het werk. De standaard ligt boven wat nu gewoon lijkt. <em>Geen cursus. Geen geschreeuw. Wel een mening.</em></p>
</section>
<div class="section-head">
<span class="label">Laatst op de rol</span>
<a href="/rol">Alles</a>
</div>
{% for post in posts %}
{% set _card = post %}
{% include "_post_card.html" %}
{% else %}
<p class="empty">Nog niets gepubliceerd.</p>
{% endfor %}
{% if featured_flyer %}
<section class="featured">
<div class="section-head"><span class="label">Flyer</span><a href="/flyers">{{ flyer_count }} flyers</a></div>
<figure>
<a href="{{ static_url(featured_flyer.source_path) }}" data-lightbox data-caption="{{ featured_flyer.caption }}">
<img src="{{ featured_flyer.thumb_url or static_url(featured_flyer.source_path) }}" alt="{{ featured_flyer.caption }}" width="{{ featured_flyer.width }}" height="{{ featured_flyer.height }}">
</a>
<figcaption>{{ featured_flyer.caption }}</figcaption>
</figure>
</section>
{% endif %}
<section class="closing">
<span class="label">Binnen</span>
<p>De rol is open voor mensen, niet voor een publiek. Wie mee wil schrijven, zegt het.</p>
<div class="actions"><a class="button" href="/binnen">Ik schrijf mee</a><a class="button ghost" href="/memes">{{ meme_count }} memes</a></div>
</section>
{% endblock %}
+17
View File
@@ -0,0 +1,17 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
<header class="article-header">
<span class="label">{{ 'De rol' if topic == 'rol' else 'De standaard' }}</span>
<h1>{{ title }}</h1>
<p class="muted">{% if topic == 'rol' %}Korte notities. Eén gedachte per keer, scherp genoeg om op terug te komen.{% else %}Waar Molodetz op staat, en waarom de keuzes vastliggen.{% endif %}</p>
</header>
{% for post in posts %}
{% set _card = post %}
{% include "_post_card.html" %}
{% else %}
<p class="empty">Hier staat nog niets.</p>
{% endfor %}
{% set _next_url = next_url %}
{% include "_load_more.html" %}
{% endblock %}
+23
View File
@@ -0,0 +1,23 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
<header class="article-header">
<span class="label">Mensen</span>
<h1>Wie schrijft</h1>
<p class="muted">Een rol van mensen, geen ledenlijst. Wie binnenkomt, staat hier zodra er geschreven is.</p>
</header>
{% for person in people %}
<div class="person-row">
{% set _user = person %}{% set _size = 48 %}
{% include "_avatar_link.html" %}
<div class="who"><b>{% include "_user_link.html" %}</b><span>{{ person.bio or 'Schrijft op de rol.' }}</span></div>
<span class="count">{{ person.post_count }} {{ 'bericht' if person.post_count == 1 else 'berichten' }}</span>
</div>
{% else %}
<p class="empty">Nog niemand op de rol.</p>
{% endfor %}
<section class="closing">
<p>De volgende schrijft hier zodra die binnen is.</p>
<div class="actions"><a class="button" href="/binnen">Ik schrijf mee</a></div>
</section>
{% endblock %}
+16
View File
@@ -0,0 +1,16 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
<header class="article-header"><span class="label">Account</span><h1>Meldingen</h1></header>
{% if unread %}
<form method="post" action="/notifications/read"><button class="button ghost small" type="submit">Alles gelezen</button></form>
{% endif %}
{% for item in notifications %}
<div class="post-card">
<div class="post-meta">{{ dt_ago(item.created_at) }}{% if not item.read_at %}<span class="tag-placeholder tag-draft">Nieuw</span>{% endif %}</div>
<a href="{{ item.url }}">{{ item.message }}</a>
</div>
{% else %}
<p class="empty">Geen meldingen.</p>
{% endfor %}
{% endblock %}
+19
View File
@@ -0,0 +1,19 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
<header class="article-header">
<div class="byline">
{% set _user = person %}{% set _size = 64 %}
{% include "_avatar_link.html" %}
</div>
<h1>{{ person.username }}</h1>
<p class="muted">{{ person.bio or 'Schrijft op de rol.' }}</p>
</header>
<div class="section-head"><span class="label">Berichten</span><span class="muted">{{ person.post_count }}</span></div>
{% for post in posts %}
{% set _card = post %}
{% include "_post_card.html" %}
{% else %}
<p class="empty">Nog niets gepubliceerd.</p>
{% endfor %}
{% endblock %}
+29
View File
@@ -0,0 +1,29 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
{% set viewer = current_user(request) %}
<article>
<header class="article-header">
{% set _post = post %}
{% include "_content_header.html" %}
<h1>{{ render_title(post.title) }}</h1>
{% if author %}
<div class="byline">
{% set _user = author %}{% set _size = 28 %}
{% include "_avatar_link.html" %}
<span>door {% include "_user_link.html" %}</span>
</div>
{% endif %}
</header>
<div class="prose" data-enhance>
{{ render_content(post.body) | safe }}
</div>
{% if viewer and is_admin(viewer) %}
<div class="actions"><a class="button ghost small" href="/admin/posts/{{ post.uid }}/edit">Bewerken</a></div>
{% endif %}
</article>
<section class="closing">
<span class="label">Verder lezen</span>
<div class="actions"><a class="button ghost" href="/{{ post.topic }}">Terug naar {{ topic_title | lower }}</a></div>
</section>
{% endblock %}
+17
View File
@@ -0,0 +1,17 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
<header class="article-header"><span class="label">Juridisch</span><h1>Privacy</h1></header>
<div class="prose">
<p>Molodetz verzamelt zo weinig mogelijk.</p>
<h2>Aanmeldingen via Binnen</h2>
<p>Als je het formulier <em>Ik schrijf mee</em> invult, bewaren we je naam, je contactgegeven, optioneel een link naar je werk en je bericht. Daarnaast bewaren we een niet-omkeerbare hash van je IP-adres om misbruik te herkennen. Alleen beheerders zien dit. We delen het met niemand.</p>
<p>Als beheer je link controleert, haalt de server eenmalig de titel van die pagina op. Er gaat daarbij niets van jou naar die site behalve het verzoek zelf.</p>
<h2>Bezoek</h2>
<p>We tellen per dag hoe vaak een pad wordt opgevraagd, zonder IP-adres of identiteit. Er zijn geen cookies voor lezers. Er zijn geen externe scripts, lettertypen of trackers.</p>
<h2>Accounts</h2>
<p>Wie inlogt, krijgt een httponly sessiecookie. We bewaren je laatste activiteit voor de online-indicator en een auditlog van beheeracties.</p>
<h2>Verwijderen</h2>
<p>Wil je dat je aanmelding verdwijnt? Vraag het aan retoor via retoor@molodetz.nl. Verwijderen gebeurt eerst zacht en daarna definitief.</p>
</div>
{% endblock %}
+10
View File
@@ -0,0 +1,10 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
<header class="article-header"><span class="label">Account</span><h1>API-sleutel</h1></header>
<p class="muted">Gebruik deze sleutel als <code>X-API-KEY</code> header of als <code>Authorization: Bearer</code>. Vernieuwen maakt de oude direct ongeldig.</p>
<pre class="log-tail" data-copy>{{ api_key }}</pre>
<form method="post" action="/profile/api-key/regenerate" data-confirm="De huidige sleutel wordt ongeldig. Doorgaan?">
<button class="button danger" type="submit">Vernieuwen</button>
</form>
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
<!-- retoor <retoor@molodetz.nl> -->
{% extends "base.html" %}
{% block content %}
{% set viewer = current_user(request) %}
<header class="article-header"><span class="label">Juridisch</span><h1>Voorwaarden</h1><p class="muted">Versie {{ terms_version }}</p></header>
<div class="prose">
<p>Molodetz is een kleine, besloten blogrol. Lezen is vrij. Schrijven gebeurt door mensen die retoor op de rol heeft gezet.</p>
<p>Wat op de rol staat is van de auteur. Overnemen mag met bronvermelding en een link terug.</p>
<p>Er is geen advertentie, geen tracking over apps heen, geen betaalmuur en geen sociale login. Accounts loggen in met een sessie op deze site.</p>
<p>Misbruik van formulieren, automatische aanmeldingen of pogingen om beheer binnen te komen worden gelogd en geweigerd.</p>
</div>
{% if viewer %}
<form method="post" action="/voorwaarden/accept">
<input type="hidden" name="next" value="{{ next }}">
<button class="button" type="submit">Ik accepteer versie {{ terms_version }}</button>
</form>
{% endif %}
{% endblock %}