Initial commit: Crumbforest Architecture Refinement v1 (Clean)

This commit is contained in:
2025-12-07 01:26:46 +01:00
commit 6c38ed680b
633 changed files with 61797 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
{% extends "base.html" %}
{% block content %}
<!-- Admin Header -->
<section class="mb-2">
<h1>Admin Dashboard</h1>
<p class="text-muted">
Welcome back, <strong>{{ user.email }}</strong>
<span class="badge badge-admin">{{ user.role }}</span>
</p>
</section>
<!-- Admin Actions Grid -->
<section class="grid-2" style="margin: 3rem 0;">
<!-- Content Management -->
<article class="card">
<header>
<h3>📝 Content Management</h3>
</header>
<p>
Manage blog posts, pages, and other content.
Create, edit, and publish multilingual content.
</p>
<footer>
<a href="/admin/posts" role="button">
Manage Posts →
</a>
</footer>
</article>
<!-- RAG System -->
<article class="card">
<header>
<h3>🤖 RAG System</h3>
</header>
<p>
Index content, search semantically, and manage AI providers.
OpenAI, Claude, and OpenRouter support.
</p>
<footer>
<a href="/admin/rag" role="button" class="secondary">
RAG Dashboard →
</a>
</footer>
</article>
<!-- API Management -->
<article class="card">
<header>
<h3>⚙️ API & Settings</h3>
</header>
<p>
View API documentation, check system health, and configure settings.
</p>
<footer>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<a href="/docs" role="button" class="outline" target="_blank">API Docs</a>
<a href="/__routes" role="button" class="outline" target="_blank">Routes</a>
</div>
</footer>
</article>
<!-- System Status -->
<article class="card">
<header>
<h3>📊 System Status</h3>
</header>
<p>
Monitor system health, view logs, and check database status.
</p>
<footer>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<a href="/health" role="button" class="outline" target="_blank">Health</a>
<a href="http://localhost:6333/dashboard" role="button" class="outline" target="_blank">Qdrant</a>
</div>
</footer>
</article>
</section>
<!-- Quick Links -->
<section style="margin: 3rem 0;">
<details>
<summary><strong>Developer Tools</strong></summary>
<div style="padding: 1rem;">
<h4>Debug Endpoints</h4>
<ul>
<li><a href="/__routes" target="_blank">All Routes</a> - View all available API routes</li>
<li><a href="/__whoami" target="_blank">Whoami</a> - Current user and session info</li>
<li><a href="/health" target="_blank">Health Check</a> - System health status</li>
<li><a href="/admin/rag/providers" target="_blank">Provider Status</a> - AI provider availability</li>
<li><a href="/admin/rag/status" target="_blank">RAG Status</a> - Indexing status</li>
</ul>
<h4>External Services</h4>
<ul>
<li><a href="http://localhost:6333/dashboard" target="_blank">Qdrant Dashboard</a> - Vector database UI</li>
<li><a href="/docs" target="_blank">Swagger UI</a> - Interactive API documentation</li>
</ul>
<h4>Shell Commands</h4>
<pre><code>./logs.sh app # View FastAPI logs
./test.sh # Run tests
./stop.sh # Stop system</code></pre>
</div>
</details>
</section>
<!-- System Info Table -->
<section style="margin: 3rem 0;">
<article>
<header>
<h3>Session Information</h3>
</header>
<table>
<tbody>
<tr>
<th scope="row">User ID</th>
<td>{{ user.id }}</td>
</tr>
<tr>
<th scope="row">Email</th>
<td>{{ user.email }}</td>
</tr>
<tr>
<th scope="row">Role</th>
<td><span class="badge badge-admin">{{ user.role }}</span></td>
</tr>
<tr>
<th scope="row">Locale</th>
<td>{{ user.locale }}</td>
</tr>
<tr>
<th scope="row">Current Language</th>
<td>{{ lang }} ({{ 'Deutsch' if lang == 'de' else 'English' }})</td>
</tr>
</tbody>
</table>
</article>
</section>
{% endblock %}

View File

@@ -0,0 +1,290 @@
{% extends "base.html" %}
{% block content %}
<article>
<header>
<h1>🌲 Crumbforest Chat</h1>
<p>Wähle deinen Gesprächspartner und stelle deine Fragen!</p>
</header>
<!-- Character Selection -->
<section id="character-selection" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
<article class="character-card" data-character="eule" style="cursor: pointer; border: 2px solid transparent; transition: all 0.3s;">
<header style="text-align: center;">
<h2 style="font-size: 3rem; margin: 0;">🦉</h2>
<h3 style="margin: 0.5rem 0;">Krümeleule</h3>
</header>
<p style="text-align: center; color: var(--pico-muted-color);">
Weise und geduldig. Erklärt die Welt mit Ruhe und Respekt.
</p>
</article>
<article class="character-card" data-character="fox" style="cursor: pointer; border: 2px solid transparent; transition: all 0.3s;">
<header style="text-align: center;">
<h2 style="font-size: 3rem; margin: 0;">🦊</h2>
<h3 style="margin: 0.5rem 0;">FunkFox</h3>
</header>
<p style="text-align: center; color: var(--pico-muted-color);">
Der rappende Bit! Erklärt Tech mit Beats und Reimen.
</p>
</article>
<article class="character-card" data-character="bugsy" style="cursor: pointer; border: 2px solid transparent; transition: all 0.3s;">
<header style="text-align: center;">
<h2 style="font-size: 3rem; margin: 0;">🐞</h2>
<h3 style="margin: 0.5rem 0;">Bugsy</h3>
</header>
<p style="text-align: center; color: var(--pico-muted-color);">
Fehler ohne Scham. Macht Debugging zu einer Lernchance.
</p>
</article>
</section>
<!-- Chat Interface -->
<section id="chat-interface" style="display: none;">
<header style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<div>
<h2 id="current-character-name" style="margin: 0;">Character</h2>
<p id="current-character-emoji" style="font-size: 2rem; margin: 0;">🦉</p>
</div>
<button id="change-character-btn" class="outline secondary">Character wechseln</button>
</header>
<!-- Chat Messages -->
<div id="chat-messages" style="min-height: 300px; max-height: 500px; overflow-y: auto; padding: 1rem; background: var(--pico-card-background-color); border-radius: var(--pico-border-radius); margin-bottom: 1rem;">
<p class="text-muted text-center">Stelle deine erste Frage! 🌱</p>
</div>
<!-- Input Form -->
<form id="chat-form">
<fieldset role="group">
<input
type="text"
id="question-input"
placeholder="Deine Frage..."
required
autocomplete="off"
/>
<button type="submit" id="send-btn">Senden</button>
</fieldset>
</form>
<!-- Loading Indicator -->
<div id="loading" style="display: none; text-align: center; margin-top: 1rem;">
<progress></progress>
<p class="text-muted">Dein Character denkt nach...</p>
</div>
</section>
</article>
<style>
.character-card:hover {
border-color: var(--brand-color) !important;
transform: scale(1.02);
}
.character-card.selected {
border-color: var(--brand-color) !important;
background: rgba(16, 185, 129, 0.05);
}
.message {
margin-bottom: 1.5rem;
padding: 1rem;
border-radius: var(--pico-border-radius);
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message-user {
background: var(--pico-primary-background);
border-left: 4px solid var(--pico-primary);
}
.message-character {
background: var(--pico-card-background-color);
border-left: 4px solid var(--brand-color);
}
.message-header {
font-weight: 600;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.message-content {
white-space: pre-wrap;
line-height: 1.6;
}
.sources {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--pico-muted-border-color);
font-size: 0.875rem;
color: var(--pico-muted-color);
}
.sources ul {
margin: 0.5rem 0 0 0;
padding-left: 1.5rem;
}
.sources li {
margin-bottom: 0.25rem;
}
</style>
<script>
const API_URL = '/api/chat';
let currentCharacter = null;
// Character Selection
document.querySelectorAll('.character-card').forEach(card => {
card.addEventListener('click', () => {
currentCharacter = card.dataset.character;
startChat();
});
});
// Change Character Button
document.getElementById('change-character-btn').addEventListener('click', () => {
document.getElementById('chat-interface').style.display = 'none';
document.getElementById('character-selection').style.display = 'grid';
document.getElementById('chat-messages').innerHTML = '<p class="text-muted text-center">Stelle deine erste Frage! 🌱</p>';
currentCharacter = null;
});
function startChat() {
// Update UI
document.getElementById('character-selection').style.display = 'none';
document.getElementById('chat-interface').style.display = 'block';
// Update character info
const characterInfo = {
eule: { name: 'Krümeleule', emoji: '🦉' },
fox: { name: 'FunkFox', emoji: '🦊' },
bugsy: { name: 'Bugsy', emoji: '🐞' }
};
const info = characterInfo[currentCharacter];
document.getElementById('current-character-name').textContent = info.name;
document.getElementById('current-character-emoji').textContent = info.emoji;
// Focus input
document.getElementById('question-input').focus();
}
// Chat Form Submission
document.getElementById('chat-form').addEventListener('submit', async (e) => {
e.preventDefault();
const input = document.getElementById('question-input');
const question = input.value.trim();
if (!question) return;
// Add user message to chat
addMessage('user', 'Du', question);
// Clear input
input.value = '';
// Show loading
document.getElementById('loading').style.display = 'block';
document.getElementById('send-btn').disabled = true;
try {
// Send request to API
const response = await fetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
character_id: currentCharacter,
question: question,
lang: '{{ lang }}'
})
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
// Add character response
const characterInfo = {
eule: { name: 'Krümeleule', emoji: '🦉' },
fox: { name: 'FunkFox', emoji: '🦊' },
bugsy: { name: 'Bugsy', emoji: '🐞' }
};
const info = characterInfo[currentCharacter];
addMessage('character', `${info.emoji} ${info.name}`, data.answer, data.sources);
} catch (error) {
console.error('Chat error:', error);
addMessage('error', '❌ Fehler', 'Entschuldigung, es gab einen Fehler. Bitte versuche es erneut.');
} finally {
// Hide loading
document.getElementById('loading').style.display = 'none';
document.getElementById('send-btn').disabled = false;
input.focus();
}
});
function addMessage(type, sender, content, sources = null) {
const messagesDiv = document.getElementById('chat-messages');
// Remove placeholder if exists
if (messagesDiv.querySelector('.text-muted')) {
messagesDiv.innerHTML = '';
}
const messageDiv = document.createElement('div');
messageDiv.className = `message message-${type}`;
let html = `
<div class="message-header">${sender}</div>
<div class="message-content">${escapeHtml(content)}</div>
`;
if (sources && sources.length > 0) {
html += `
<div class="sources">
📚 Quellen (${sources.length}):
<ul>
${sources.map(s => `<li>${escapeHtml(s.file_path)} (${s.score.toFixed(3)})</li>`).join('')}
</ul>
</div>
`;
}
messageDiv.innerHTML = html;
messagesDiv.appendChild(messageDiv);
// Scroll to bottom
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
{% endblock %}

View File

@@ -0,0 +1,169 @@
{% extends 'base.html' %}
{% block content %}
<!-- Hero Section -->
<section class="text-center mb-2">
<h1>Wuuuuhuuu! 🦉</h1>
<p style="font-size: 1.25rem; color: var(--pico-muted-color);">
Knowledge Management & Diary System with AI-powered RAG
</p>
</section>
<!-- Feature Cards -->
<section class="grid-2" style="margin: 3rem 0;">
<article class="card">
<header>
<h3>🤖 RAG System</h3>
</header>
<p>
Semantic search and Q&A powered by OpenAI, Claude, or OpenRouter.
Index your content and ask questions in natural language.
</p>
<footer>
{% if user and user.role == 'admin' %}
<a href="/admin/rag" role="button">Manage RAG →</a>
{% else %}
<small class="text-muted">Admin access required</small>
{% endif %}
</footer>
</article>
<article class="card">
<header>
<h3>📔 Diary System</h3>
</header>
<p>
Personal diary entries with full-text search and AI insights.
Each child has their own secure collection.
</p>
<footer>
{% if user %}
<a href="/api/hello?lang={{ lang }}" role="button" class="outline">Try API →</a>
{% else %}
<a href="/{{ lang }}/login" role="button" class="outline">Login to Access →</a>
{% endif %}
</footer>
</article>
<article class="card">
<header>
<h3>🌍 Multilingual</h3>
</header>
<p>
Full internationalization support with German and English.
Server-rendered templates with language switching.
</p>
<footer>
<small class="text-muted">Current: <strong>{{ 'Deutsch' if lang == 'de' else 'English' }}</strong></small>
</footer>
</article>
<article class="card">
<header>
<h3>🔐 Secure & GDPR</h3>
</header>
<p>
Role-based access control, audit logging, and GDPR-compliant
data handling for all operations.
</p>
<footer>
<small class="text-muted">Audit log & immutable records</small>
</footer>
</article>
</section>
<!-- Quick Start / Status -->
<section style="margin: 3rem 0;">
{% if user %}
<article>
<header>
<h3>Welcome back, {{ user.email }}! 👋</h3>
</header>
<p>You're logged in as <strong>{{ user.role }}</strong></p>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
{% if user.role == 'admin' %}
<a href="/admin" role="button">Admin Dashboard</a>
<a href="/admin/rag" role="button" class="secondary">RAG Management</a>
{% endif %}
<a href="/docs" role="button" class="outline" target="_blank">API Docs</a>
</div>
</article>
{% else %}
<article>
<header>
<h3>Get Started</h3>
</header>
<p>
Login to access the admin dashboard, manage content, and use AI-powered features.
</p>
<div style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: center;">
<a href="/{{ lang }}/login" role="button">Login</a>
<small class="text-muted">
Demo: <code>demo@crumb.local</code> / <code>demo123</code>
</small>
</div>
</article>
{% endif %}
</section>
<!-- Tech Stack -->
<section style="margin: 3rem 0;">
<details>
<summary><strong>Tech Stack & APIs</strong></summary>
<div style="padding: 1rem;">
<h4>Backend</h4>
<ul>
<li><strong>FastAPI</strong> - Modern Python web framework</li>
<li><strong>MariaDB</strong> - Relational database</li>
<li><strong>Qdrant</strong> - Vector database for RAG</li>
<li><strong>Jinja2</strong> - Template engine</li>
</ul>
<h4>API Endpoints</h4>
<pre><code>GET /api/hello - Hello World API
POST /api/diary/index - Index diary entry
POST /api/diary/search - Semantic search
POST /api/diary/ask - RAG Q&A
GET /api/diary/{child_id}/status - Status check</code></pre>
<h4>Quick Test</h4>
<pre><code>curl http://localhost:8000/api/hello?lang={{ lang }}
curl http://localhost:8000/health
curl http://localhost:8000/__routes</code></pre>
</div>
</details>
</section>
<!-- System Info (Debug) -->
{% if user and user.role == 'admin' %}
<section style="margin: 3rem 0;">
<details>
<summary><strong>System Information (Admin Only)</strong></summary>
<div style="padding: 1rem;">
<table>
<tbody>
<tr>
<th scope="row">User</th>
<td>{{ user.email }}</td>
</tr>
<tr>
<th scope="row">Role</th>
<td><span class="badge badge-admin">{{ user.role }}</span></td>
</tr>
<tr>
<th scope="row">Locale</th>
<td>{{ user.locale or lang }}</td>
</tr>
<tr>
<th scope="row">Language</th>
<td>{{ lang }}</td>
</tr>
</tbody>
</table>
</div>
</details>
</section>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,73 @@
{% extends "base.html" %}
{% block content %}
<div style="max-width: 480px; margin: 4rem auto;">
<article>
<header class="text-center">
<h1 style="margin-bottom: 0.5rem;">🦉 Login</h1>
<p class="text-muted">Welcome back to Crumbforest</p>
</header>
<form method="post" action="/{{ lang }}/login">
<label for="email">
Email Address
<input
type="email"
id="email"
name="email"
placeholder="admin@crumb.local"
value="{{ form.email if form else '' }}"
required
autofocus
>
</label>
<label for="password">
Password
<input
type="password"
id="password"
name="password"
placeholder="Enter your password"
required
>
</label>
<button type="submit" style="width: 100%;">
Login
</button>
</form>
<footer>
<details>
<summary>Demo Accounts</summary>
<div style="padding: 1rem 0;">
<table role="grid">
<tbody>
<tr>
<th scope="row">Admin</th>
<td><code>admin@crumb.local</code></td>
<td><code>admin123</code></td>
</tr>
<tr>
<th scope="row">Demo User</th>
<td><code>demo@crumb.local</code></td>
<td><code>demo123</code></td>
</tr>
</tbody>
</table>
</div>
</details>
</footer>
</article>
<!-- Quick Links -->
<div class="text-center" style="margin-top: 2rem;">
<small class="text-muted">
<a href="/{{ lang }}/">← Back to Home</a>
·
<a href="/docs" target="_blank">API Docs</a>
</small>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,87 @@
{% extends group_config.template_base or "base_demo.html" %}
{% block title %}Settings{% endblock %}
{% block content %}
<main class="container">
<hgroup>
<h1>⚙️ Settings</h1>
<p>Customize your Crumbforest experience</p>
</hgroup>
<div class="grid">
<!-- Theme Selection -->
<article>
<header>
<h3>🎨 Appearance</h3>
</header>
<form action="/settings/theme" method="post">
<label for="theme">Theme</label>
<select name="theme" id="theme" onchange="this.form.submit()">
{% for variant_id, variant in theme_variants.items() %}
<option value="{{ variant_id }}" {% if user.theme==variant_id %}selected{% endif %}>
{{ variant.name }}
</option>
{% endfor %}
</select>
<small>
Current: <strong>{{ theme_variants[user.theme].name if user.theme in theme_variants else user.theme
}}</strong>
</small>
</form>
</article>
<!-- Accessibility Settings -->
<article>
<header>
<h3>♿ Accessibility</h3>
</header>
<form action="/settings/accessibility" method="post">
<label>
<input type="checkbox" name="high_contrast" {% if user.accessibility and
user.accessibility.high_contrast %}checked{% endif %}>
High Contrast Mode
</label>
<label>
<input type="checkbox" name="animation_reduced" {% if user.accessibility and
user.accessibility.animation_reduced %}checked{% endif %}>
Reduce Animations
</label>
<label for="font_size">Font Size</label>
<select name="font_size" id="font_size">
<option value="normal" {% if not user.accessibility or user.accessibility.font_size !='large'
%}selected{% endif %}>Normal</option>
<option value="large" {% if user.accessibility and user.accessibility.font_size=='large'
%}selected{% endif %}>Large</option>
</select>
<button type="submit" class="contrast">Save Accessibility Settings</button>
</form>
</article>
</div>
<!-- Account Info (Read Only) -->
<article>
<header>
<h3>👤 Account Info</h3>
</header>
<dl>
<dt>Email</dt>
<dd>{{ user.email }}</dd>
<dt>Group</dt>
<dd>
<span class="badge">{{ group_config.name }}</span>
</dd>
<dt>Role</dt>
<dd>{{ user.role }}</dd>
</dl>
</article>
</main>
{% endblock %}