feat: implement Phase 2 (Pulse, Admin, RC2)

This commit is contained in:
2025-12-08 21:00:23 +01:00
parent 78620d8f8a
commit 96754a1d1a
23 changed files with 1415 additions and 27 deletions

View File

@@ -0,0 +1,82 @@
{% extends "base.html" %}
{% block title %}Admin Config Editor{% endblock %}
{% block content %}
<main class="container">
<nav aria-label="breadcrumb">
<ul>
<li><a href="/admin">Admin Dashboard</a></li>
<li>Config Editor</li>
</ul>
</nav>
<hgroup>
<h1>🛠️ Config Editor</h1>
<p>System Configuration (JSON). Be careful!</p>
</hgroup>
{% if error %}
<article class="pico-background-pumpkin-500" style="color: white; border-color: red;">
<header>⚠️ Error</header>
<pre>{{ error }}</pre>
</article>
{% endif %}
<form method="POST" action="/admin/config/save" id="configForm">
<label for="config_json">
crumbforest_config.json
<textarea id="config_json" name="config_json" rows="25"
style="font-family: monospace; font-size: 0.9em; white-space: pre;"
spellcheck="false">{{ config_json }}</textarea>
</label>
<script>
// Simple auto-indent on load if not already indented?
// Actually config_json from backend is likely raw string provided by ConfigService.get_raw_config()
// We can try to format it nicely in JS on load
document.addEventListener("DOMContentLoaded", () => {
const ta = document.getElementById('config_json');
try {
const obj = JSON.parse(ta.value);
ta.value = JSON.stringify(obj, null, 4);
} catch (e) {
console.warn("Could not auto-format JSON on load", e);
}
});
</script>
<div class="grid">
<button type="button" class="secondary outline" onclick="window.history.back()">Cancel</button>
<button type="submit" id="saveBtn">💾 Save Configuration</button>
</div>
</form>
<script>
const form = document.getElementById('configForm');
const ta = document.getElementById('config_json');
form.addEventListener('submit', (e) => {
try {
JSON.parse(ta.value);
// Valid JSON
} catch (err) {
e.preventDefault();
alert("❌ INVALID JSON!\n\nPlease fix the syntax error:\n" + err.message);
}
});
// Tab support
ta.addEventListener('keydown', function (e) {
if (e.key == 'Tab') {
e.preventDefault();
var start = this.selectionStart;
var end = this.selectionEnd;
this.value = this.value.substring(0, start) +
" " + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 4;
}
});
</script>
</main>
{% endblock %}

View File

@@ -0,0 +1,129 @@
{% extends group_config.template_base or "base_demo.html" %}
{% block title %}Crumbforest Pulse{% endblock %}
{% block content %}
<main class="container">
<hgroup>
<h1>📰 Crumbforest Pulse</h1>
<p>Neuigkeiten, Wissen und Geschichten aus dem Wald.</p>
</hgroup>
<!-- Tag Cloud -->
<section class="tag-cloud-container">
<h6>Themenwolke</h6>
<div class="tag-cloud">
<a href="/crumbforest/pulse" class="tag-pill {% if not current_tag %}active{% endif %}">
Alle
</a>
{% for tag in tag_cloud %}
<a href="/crumbforest/pulse?tag={{ tag.name }}" class="tag-pill {% if tag.active %}active{% endif %}"
style="font-size: {{ tag.size }}; opacity: {{ '1' if tag.active or not current_tag else '0.6' }}">
{{ tag.name }} <small>({{ tag.count }})</small>
</a>
{% endfor %}
</div>
</section>
<hr>
<!-- Articles Grid -->
<div class="articles-grid">
{% for post in posts %}
<article class="post-card">
<header>
<small>{{ post.created_at.strftime('%d.%m.%Y') }} • ✍️ {{ post.author }}</small>
<h3>
<a href="/crumbforest/pulse/{{ post.slug }}" class="contrast">{{ post.title }}</a>
</h3>
</header>
<p>{{ post.excerpt }}</p>
<footer>
<div class="tags">
{% for t in post.tags %}
<span class="badge">#{{ t }}</span>
{% endfor %}
</div>
<a href="/crumbforest/pulse/{{ post.slug }}" role="button" class="outline">
Lesen →
</a>
</footer>
</article>
{% else %}
<article>
<p>Keine Artikel gefunden. 🍂</p>
</article>
{% endfor %}
</div>
</main>
<style>
.tag-cloud-container {
background: var(--card-background-color);
padding: 1rem;
border-radius: var(--border-radius);
margin-bottom: 2rem;
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: baseline;
}
.tag-pill {
text-decoration: none;
padding: 0.2rem 0.6rem;
border-radius: 99px;
background-color: var(--secondary);
color: white;
transition: all 0.2s;
}
.tag-pill:hover,
.tag-pill.active {
background-color: var(--primary);
transform: scale(1.05);
}
.tag-pill.active {
box-shadow: 0 0 10px var(--primary-focus);
}
.articles-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
@media (min-width: 768px) {
.articles-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.tags {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.badge {
font-size: 0.7em;
padding: 2px 6px;
background-color: var(--card-sectionning-background-color);
border-radius: 4px;
color: var(--contrast);
}
.post-card {
display: flex;
flex-direction: column;
justify-content: space-between;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,67 @@
{% extends group_config.template_base or "base_demo.html" %}
{% block title %}{{ post.title }} - Pulse{% endblock %}
{% block content %}
<main class="container">
<div style="max-width: 800px; margin: 0 auto;">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb">
<ul>
<li><a href="/crumbforest/pulse">📰 Pulse</a></li>
<li>{{ post.title }}</li>
</ul>
</nav>
<article>
<header>
<h1>{{ post.title }}</h1>
<small>
Veröffentlicht am {{ post.created_at.strftime('%d.%m.%Y') }}
von <strong>{{ post.author }}</strong>
</small>
<div class="tags" style="margin-top: 0.5rem;">
{% for t in post.tags %}
<span class="badge">#{{ t }}</span>
{% endfor %}
</div>
</header>
<!-- Content -->
<div class="post-content">
{{ post.html_content | safe }}
</div>
<footer>
<hr>
<a href="/crumbforest/pulse" role="button" class="secondary outline">
← Zurück zur Übersicht
</a>
</footer>
</article>
</div>
</main>
<style>
.tags {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.badge {
font-size: 0.8em;
padding: 2px 8px;
background-color: var(--card-sectionning-background-color);
border-radius: 99px;
}
.post-content {
line-height: 1.6;
}
.post-content h2 {
margin-top: 2rem;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block title %}{{ doc_title }} - Docs{% endblock %}
{% block content %}
<main class="container">
<nav aria-label="breadcrumb">
<ul>
<li><a href="/docs">📚 Docs</a></li>
<li>{{ filename }}</li>
</ul>
</nav>
<article class="doc-content">
{{ doc_content | safe }}
<footer>
<hr>
<a href="/docs" role="button" class="secondary outline">← Zurück zur Übersicht</a>
</footer>
</article>
</main>
<style>
/* Styling for rendered Markdown */
.doc-content h1,
.doc-content h2,
.doc-content h3 {
color: var(--h1-color);
margin-top: 1.5em;
}
.doc-content pre {
background: var(--code-background-color);
padding: 1em;
border-radius: var(--border-radius);
}
.doc-content table {
width: 100%;
border-collapse: collapse;
margin: 1em 0;
}
.doc-content th,
.doc-content td {
border-bottom: 1px solid var(--muted-border-color);
padding: 0.5em;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
<main class="container">
<hgroup>
<h1>📚 Dokumentation</h1>
<p>Startfiles und Handbücher direkt im Browser lesen.</p>
</hgroup>
<div class="grid">
{% for doc in docs %}
<article>
<header>📄 {{ doc.name }}</header>
<p><code>{{ doc.file }}</code></p>
<footer>
<a href="/docs/{{ doc.file }}" role="button" class="outline">Lesen →</a>
</footer>
</article>
{% else %}
<p>Keine Dokumentation gefunden. 🤷‍♂️</p>
{% endfor %}
</div>
<hr>
<p><small>Hinweis: Diese Dateien liegen im Root-Verzeichnis des Servers.</small></p>
</main>
{% endblock %}