Initial commit: Notes Manager App (notes.braetter-int.de)

- Python/Flask Backend
- SQLAlchemy Models (notes, tasks, templates, users)
- Gunicorn + Nginx Deploy-Konfiguration
- Static Assets (CSS/JS)
- Jinja2 Templates
This commit is contained in:
2026-04-15 09:28:33 +02:00
commit 5c7ce5d0ca
24 changed files with 1666 additions and 0 deletions

54
app/templates/base.html Executable file
View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}NotesManager{% endblock %}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4" aria-label="Hauptnavigation">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('main.dashboard') }}">NotesManager</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Navigation umschalten">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
{% if current_user.is_authenticated %}
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link" href="{{ url_for('main.dashboard') }}">Dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('main.entries') }}">Dokumentation</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('main.templates') }}">Vorlagen</a></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('main.tasks') }}">Aufgaben</a></li>
{% if current_user.role == 'admin' %}
<li class="nav-item"><a class="nav-link" href="{{ url_for('main.users') }}">Benutzer</a></li>
{% endif %}
</ul>
<div class="d-flex align-items-center gap-2">
<span class="nm-user-info">{{ current_user.display_name }}</span>
<a class="btn btn-outline-light btn-sm" href="{{ url_for('main.logout') }}">Abmelden</a>
</div>
{% endif %}
</div>
</div>
</nav>
<main class="container pb-5">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Schließen"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
</body>
</html>

82
app/templates/dashboard.html Executable file
View File

@@ -0,0 +1,82 @@
{% extends 'base.html' %}
{% block title %}Dashboard - NotesManager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<div>
<h1 class="h2 mb-1">Dashboard</h1>
<div class="text-muted">Stand: {{ today.strftime('%d.%m.%Y') }}</div>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('main.entry_new') }}" class="btn btn-primary">Neuer Eintrag</a>
<a href="{{ url_for('main.export_markdown') }}" class="btn btn-outline-secondary">Markdown-Export</a>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-3"><div class="card stat-card"><div class="card-body"><div class="stat-number">{{ stats.entries_total }}</div><div class="text-muted">Einträge gesamt</div></div></div></div>
<div class="col-md-3"><div class="card stat-card"><div class="card-body"><div class="stat-number">{{ stats.entries_today }}</div><div class="text-muted">Einträge heute</div></div></div></div>
<div class="col-md-3"><div class="card stat-card"><div class="card-body"><div class="stat-number">{{ stats.templates_total }}</div><div class="text-muted">Vorlagen</div></div></div></div>
<div class="col-md-3"><div class="card stat-card"><div class="card-body"><div class="stat-number">{{ stats.open_tasks }}</div><div class="text-muted">Offene Aufgaben</div></div></div></div>
</div>
<div class="row g-4">
<div class="col-lg-7">
<div class="card shadow-sm">
<div class="card-body">
<h2 class="h5 mb-3">Neueste Dokumentation</h2>
{% if latest_entries %}
<div class="list-group list-group-flush">
{% for item in latest_entries %}
<a href="{{ url_for('main.entry_view', entry_id=item.id) }}" class="list-group-item list-group-item-action">
<div class="d-flex justify-content-between flex-wrap gap-2">
<strong>{{ item.title }}</strong>
<span class="badge text-bg-secondary">{{ item.work_date.strftime('%d.%m.%Y') }}</span>
</div>
<div class="small text-muted">{{ item.category }} · {{ item.system_name or 'kein System' }} · {{ item.status }}</div>
</a>
{% endfor %}
</div>
{% else %}
<p class="text-muted">Noch keine Einträge vorhanden.</p>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card shadow-sm mb-4">
<div class="card-body">
<h2 class="h5 mb-3">Heutige Einträge</h2>
{% if entries_today %}
<ul class="list-group list-group-flush">
{% for item in entries_today %}
<li class="list-group-item px-0">
<strong>{{ item.title }}</strong><br>
<span class="small text-muted">{{ item.category }} · {{ item.priority }} · {{ item.created_by }}</span>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted mb-0">Heute gibt es noch keine Einträge.</p>
{% endif %}
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<h2 class="h5 mb-3">Offene Aufgaben</h2>
{% if open_tasks %}
<ul class="list-group list-group-flush">
{% for task in open_tasks[:6] %}
<li class="list-group-item px-0">
<strong>{{ task.title }}</strong><br>
<span class="small text-muted">{{ task.priority }}{% if task.due_date %} · Fällig {{ task.due_date.strftime('%d.%m.%Y') }}{% endif %}</span>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted mb-0">Keine offenen Aufgaben.</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

75
app/templates/entries.html Executable file
View File

@@ -0,0 +1,75 @@
{% extends 'base.html' %}
{% block title %}Dokumentation - NotesManager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<h1 class="h2 mb-0">Dokumentation</h1>
<a href="{{ url_for('main.entry_new') }}" class="btn btn-primary">Neuer Eintrag</a>
</div>
<form class="card card-body shadow-sm mb-4" method="get">
<div class="row g-3">
<div class="col-md-5">
<label class="form-label">Suche</label>
<input name="q" value="{{ q }}" class="form-control" placeholder="Titel, Inhalt, Tags, System ...">
</div>
<div class="col-md-3">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="">Alle</option>
{% for s in ['offen', 'in_bearbeitung', 'erledigt'] %}
<option value="{{ s }}" {% if status == s %}selected{% endif %}>{{ s }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label class="form-label">Kategorie</label>
<select name="category" class="form-select">
<option value="">Alle</option>
{% for c in categories %}
<option value="{{ c }}" {% if category == c %}selected{% endif %}>{{ c }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-1 d-flex align-items-end">
<button class="btn btn-outline-secondary w-100">Filter</button>
</div>
</div>
</form>
<div class="card shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th>Datum</th>
<th>Titel</th>
<th>Kategorie</th>
<th>System</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.work_date.strftime('%d.%m.%Y') }}</td>
<td><strong>{{ item.title }}</strong><div class="small text-muted">{{ item.tags or '' }}</div></td>
<td>{{ item.category }}</td>
<td>{{ item.system_name or '-' }}</td>
<td><span class="badge text-bg-light border">{{ item.status }}</span></td>
<td class="text-nowrap">
<a href="{{ url_for('main.entry_view', entry_id=item.id) }}" class="btn btn-sm btn-outline-primary">Ansehen</a>
<a href="{{ url_for('main.entry_edit', entry_id=item.id) }}" class="btn btn-sm btn-outline-secondary">Bearbeiten</a>
<form method="post" action="{{ url_for('main.entry_delete', entry_id=item.id) }}" class="d-inline" onsubmit="return confirm('Eintrag wirklich löschen?');">
<button class="btn btn-sm btn-outline-danger">Löschen</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="6" class="text-center text-muted py-4">Keine Einträge gefunden.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

77
app/templates/entry_form.html Executable file
View File

@@ -0,0 +1,77 @@
{% extends 'base.html' %}
{% block title %}Eintrag {% if entry %}bearbeiten{% else %}anlegen{% endif %} - NotesManager{% endblock %}
{% block content %}
<h1 class="h2 mb-4">{% if entry %}Eintrag bearbeiten{% else %}Neuen Eintrag anlegen{% endif %}</h1>
<div class="row g-4">
<div class="col-lg-8">
<form method="post" class="card shadow-sm">
<div class="card-body">
<div class="row g-3">
<div class="col-md-8">
<label class="form-label">Titel</label>
<input name="title" class="form-control" required value="{{ entry.title if entry else '' }}">
</div>
<div class="col-md-4">
<label class="form-label">Datum</label>
<input type="date" name="work_date" class="form-control" value="{{ entry.work_date.isoformat() if entry else '' }}">
</div>
<div class="col-md-4">
<label class="form-label">Kategorie</label>
<input name="category" class="form-control" value="{{ entry.category if entry else '' }}" placeholder="z. B. Betrieb">
</div>
<div class="col-md-4">
<label class="form-label">System / Bereich</label>
<input name="system_name" class="form-control" value="{{ entry.system_name if entry else '' }}" placeholder="Server, Kunde, Projekt ...">
</div>
<div class="col-md-2">
<label class="form-label">Priorität</label>
<select name="priority" class="form-select">
{% for p in ['niedrig', 'mittel', 'hoch'] %}
<option value="{{ p }}" {% if (entry and entry.priority == p) or (not entry and p == 'mittel') %}selected{% endif %}>{{ p }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label class="form-label">Status</label>
<select name="status" class="form-select">
{% for s in ['offen', 'in_bearbeitung', 'erledigt'] %}
<option value="{{ s }}" {% if (entry and entry.status == s) or (not entry and s == 'offen') %}selected{% endif %}>{{ s }}</option>
{% endfor %}
</select>
</div>
<div class="col-12">
<label class="form-label">Tags</label>
<input name="tags" class="form-control" value="{{ entry.tags if entry else '' }}" placeholder="z. B. backup, kunde-a, patchday">
</div>
<div class="col-12">
<label class="form-label">Inhalt</label>
<textarea id="content" name="content" rows="16" class="form-control" required>{{ entry.content if entry else '' }}</textarea>
</div>
</div>
</div>
<div class="card-footer d-flex gap-2 justify-content-end">
<a href="{{ url_for('main.entries') }}" class="btn btn-outline-secondary">Abbrechen</a>
<button class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
<div class="col-lg-4">
<div class="card shadow-sm">
<div class="card-body">
<h2 class="h5">Vorlagen</h2>
<p class="text-muted small">Klicke auf eine Vorlage, um den Inhalt in das Formular zu übernehmen.</p>
<div class="list-group">
{% for t in templates %}
<button type="button" class="list-group-item list-group-item-action template-btn" data-template="{{ t.content | e }}">
<strong>{{ t.name }}</strong>
<div class="small text-muted">{{ t.description }}</div>
</button>
{% else %}
<div class="text-muted">Keine Vorlagen vorhanden.</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

25
app/templates/entry_view.html Executable file
View File

@@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% block title %}{{ entry.title }} - NotesManager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<div>
<h1 class="h2 mb-1">{{ entry.title }}</h1>
<div class="text-muted">{{ entry.work_date.strftime('%d.%m.%Y') }} · {{ entry.category }} · {{ entry.system_name or 'kein System' }}</div>
</div>
<div class="d-flex gap-2">
<a href="{{ url_for('main.entry_edit', entry_id=entry.id) }}" class="btn btn-outline-secondary">Bearbeiten</a>
<a href="{{ url_for('main.entries') }}" class="btn btn-primary">Zur Übersicht</a>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<div class="mb-3 small text-muted">
<strong>Status:</strong> {{ entry.status }} ·
<strong>Priorität:</strong> {{ entry.priority }} ·
<strong>Tags:</strong> {{ entry.tags or '-' }} ·
<strong>Von:</strong> {{ entry.created_by }}
</div>
<pre class="content-pre">{{ entry.content }}</pre>
</div>
</div>
{% endblock %}

26
app/templates/login.html Executable file
View File

@@ -0,0 +1,26 @@
{% extends 'base.html' %}
{% block title %}Anmelden NotesManager{% endblock %}
{% block content %}
<div class="login-wrapper">
<div class="login-card">
<div class="card">
<div class="card-body p-4 p-sm-5">
<div class="login-logo" aria-hidden="true">N</div>
<h1 class="h4 text-center mb-1">NotesManager</h1>
<p class="text-muted text-center small mb-4">Bitte melde dich an, um fortzufahren.</p>
<form method="post" novalidate>
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<input id="username" name="username" class="form-control" autocomplete="username" required autofocus>
</div>
<div class="mb-4">
<label for="password" class="form-label">Passwort</label>
<input id="password" name="password" type="password" class="form-control" autocomplete="current-password" required>
</div>
<button class="btn btn-primary w-100" type="submit">Anmelden</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

76
app/templates/tasks.html Executable file
View File

@@ -0,0 +1,76 @@
{% extends 'base.html' %}
{% block title %}Aufgaben - NotesManager{% endblock %}
{% block content %}
<div class="row g-4">
<div class="col-lg-5">
<div class="card shadow-sm">
<div class="card-body">
<h1 class="h4 mb-3">Neue Aufgabe</h1>
<form method="post">
<div class="mb-3">
<label class="form-label">Titel</label>
<input name="title" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Beschreibung</label>
<textarea name="description" class="form-control" rows="4"></textarea>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Fällig am</label>
<input type="date" name="due_date" class="form-control">
</div>
<div class="col-md-3">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="offen">offen</option>
<option value="in_bearbeitung">in_bearbeitung</option>
<option value="erledigt">erledigt</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Priorität</label>
<select name="priority" class="form-select">
<option value="niedrig">niedrig</option>
<option value="mittel" selected>mittel</option>
<option value="hoch">hoch</option>
</select>
</div>
</div>
<button class="btn btn-primary">Speichern</button>
</form>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="card shadow-sm">
<div class="card-body">
<h1 class="h4 mb-3">Aufgabenliste</h1>
<div class="list-group">
{% for item in items %}
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<strong>{{ item.title }}</strong>
<div class="small text-muted">{{ item.description or '' }}</div>
<div class="small text-muted mt-1">{{ item.status }} · {{ item.priority }}{% if item.due_date %} · {{ item.due_date.strftime('%d.%m.%Y') }}{% endif %}</div>
</div>
<div class="d-flex gap-2">
<form method="post" action="{{ url_for('main.task_toggle', task_id=item.id) }}">
<button class="btn btn-sm btn-outline-success">Umschalten</button>
</form>
<form method="post" action="{{ url_for('main.task_delete', task_id=item.id) }}" onsubmit="return confirm('Aufgabe wirklich löschen?');">
<button class="btn btn-sm btn-outline-danger">Löschen</button>
</form>
</div>
</div>
</div>
{% else %}
<p class="text-muted">Noch keine Aufgaben vorhanden.</p>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

57
app/templates/templates.html Executable file
View File

@@ -0,0 +1,57 @@
{% extends 'base.html' %}
{% block title %}Vorlagen - NotesManager{% endblock %}
{% block content %}
<div class="row g-4">
<div class="col-lg-5">
<div class="card shadow-sm">
<div class="card-body">
<h1 class="h4 mb-3">Neue Vorlage</h1>
<form method="post">
<div class="mb-3">
<label class="form-label">Name</label>
<input name="name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Beschreibung</label>
<input name="description" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Inhalt</label>
<textarea name="content" rows="12" class="form-control" required></textarea>
</div>
<button class="btn btn-primary">Speichern</button>
</form>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="card shadow-sm">
<div class="card-body">
<h1 class="h4 mb-3">Vorhandene Vorlagen</h1>
<div class="accordion" id="templateList">
{% for item in items %}
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#tpl{{ item.id }}">
{{ item.name }}
</button>
</h2>
<div id="tpl{{ item.id }}" class="accordion-collapse collapse" data-bs-parent="#templateList">
<div class="accordion-body">
<div class="small text-muted mb-2">{{ item.description }}</div>
<pre class="content-pre">{{ item.content }}</pre>
<form method="post" action="{{ url_for('main.template_delete', template_id=item.id) }}" onsubmit="return confirm('Vorlage wirklich löschen?');">
<button class="btn btn-sm btn-outline-danger">Löschen</button>
</form>
</div>
</div>
</div>
{% else %}
<p class="text-muted">Noch keine Vorlagen vorhanden.</p>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

59
app/templates/users.html Executable file
View File

@@ -0,0 +1,59 @@
{% extends 'base.html' %}
{% block title %}Benutzer - NotesManager{% endblock %}
{% block content %}
<div class="row g-4">
<div class="col-lg-5">
<div class="card shadow-sm">
<div class="card-body">
<h1 class="h4 mb-3">Benutzer anlegen</h1>
<form method="post">
<div class="mb-3">
<label class="form-label">Benutzername</label>
<input name="username" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Anzeigename</label>
<input name="display_name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Passwort</label>
<input type="password" name="password" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Rolle</label>
<select name="role" class="form-select">
<option value="user">user</option>
<option value="admin">admin</option>
</select>
</div>
<button class="btn btn-primary">Benutzer speichern</button>
</form>
</div>
</div>
</div>
<div class="col-lg-7">
<div class="card shadow-sm">
<div class="card-body">
<h1 class="h4 mb-3">Vorhandene Benutzer</h1>
<div class="table-responsive">
<table class="table align-middle">
<thead><tr><th>Benutzername</th><th>Anzeigename</th><th>Rolle</th><th>Erstellt</th></tr></thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.username }}</td>
<td>{{ item.display_name }}</td>
<td>{{ item.role }}</td>
<td>{{ item.created_at.strftime('%d.%m.%Y %H:%M') }}</td>
</tr>
{% else %}
<tr><td colspan="4" class="text-muted">Keine Benutzer vorhanden.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}