Compare commits

...

10 Commits

Author SHA1 Message Date
Aiirondev_dev 4991587d99 Enhance safe area support for iOS devices: update CSS for navbar and body to respect safe area insets and improve layout on mobile screens 2026-04-22 16:25:45 +02:00
Aiirondev_dev 2b8bf0b7d5 Update Docker configuration and scripts for multi-tenant support: change app image, adjust port settings, and clean up logs 2026-04-21 23:20:42 +02:00
Aiirondev_dev 9ae316cdd9 Enhance multi-tenant support: update scripts to handle multi-tenant Docker compose files and secure admin credentials 2026-04-21 16:57:43 +02:00
Aiirondev_dev 12f62e5146 Change Docker image for tenant-manager service and update entrypoint to use sh; modify shebang in manage-tenant.sh to use sh 2026-04-21 16:26:48 +02:00
Aiirondev_dev 9ddaaaae1e Add tenant-manager service to docker-compose for multi-tenant management 2026-04-21 16:19:19 +02:00
Aiirondev_dev 3a1518571b Fix script to use 'docker compose' command for tenant management 2026-04-20 22:14:25 +02:00
Aiirondev_dev ebb266e099 Add multi-tenant resource copying and ensure executable permissions for new scripts 2026-04-20 21:59:04 +02:00
Aiirondev_dev df677908d0 Add multitenant scripts and documentation to release bundle; remove outdated build script 2026-04-20 21:39:24 +02:00
Aiirondev_dev bc61e87ab0 Refactor UI styles to use CSS variables for consistent theming
- Updated background colors across various templates to use CSS variables for better theming support.
- Replaced hardcoded colors with variables like `var(--ui-surface)` and `var(--ui-surface-soft)` for backgrounds.
- Adjusted text colors to use `var(--ui-text)` for improved readability and consistency.
- Enhanced the theme toggle functionality in the base template to dynamically change theme colors based on user preference.
- Ensured that all relevant templates reflect the new theming approach for a cohesive user experience.
2026-04-20 20:20:33 +02:00
Aiirondev_dev 2d37ded5be Remove outdated German section and streamline tenant management documentation for clarity 2026-04-20 20:05:44 +02:00
41 changed files with 370 additions and 513 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=80
INVENTAR_HTTPS_PORT=443
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/inventarsystem:latest
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:latest
+9 -1
View File
@@ -229,8 +229,16 @@ jobs:
cp config.json release-bundle/config.json
cp update.sh release-bundle/update.sh
cp init-admin.sh release-bundle/init-admin.sh
# Multitenant scripts & docs
cp docker-compose-multitenant.yml release-bundle/docker-compose-multitenant.yml
cp docker/nginx/multitenant.conf release-bundle/docker/nginx/multitenant.conf
cp manage-tenant.sh release-bundle/manage-tenant.sh
cp run-tenant-cmd.sh release-bundle/run-tenant-cmd.sh
cp MULTITENANT_DEPLOYMENT.md release-bundle/MULTITENANT_DEPLOYMENT.md
cp MULTITENANT_PYTHON_API.md release-bundle/MULTITENANT_PYTHON_API.md
chmod +x release-bundle/start.sh release-bundle/stop.sh release-bundle/restart.sh release-bundle/backup.sh release-bundle/update.sh release-bundle/init-admin.sh
chmod +x release-bundle/start.sh release-bundle/stop.sh release-bundle/restart.sh release-bundle/backup.sh release-bundle/update.sh release-bundle/init-admin.sh release-bundle/manage-tenant.sh release-bundle/run-tenant-cmd.sh
tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle .
- name: Create or update GitHub Release
-177
View File
@@ -1,177 +0,0 @@
# Fehlersuche: Geplante Ausleihen werden nicht automatisch aktiviert
## 🔍 Mögliche Ursachen
### 1. **Termin wurde für falsches Datum erstellt**
Die meisten Probleme entstehen, weil der Termin für ein **falsches Datum** (z.B. morgen statt heute) erstellt wurde.
### 2. **Scheduler läuft nicht**
Der automatische Scheduler kann deaktiviert sein oder nicht starten.
### 3. **Zeitzonen-Problem**
Die Startzeit kann in einer anderen Zeitzone gespeichert sein als erwartet.
---
## ✅ SOFORT-FIX: Manuell aktivieren
Falls du eine geplante Ausleihe sofort aktivieren möchtest, nutze diesen Befehl auf dem Server:
```bash
docker exec inventarsystem-mongodb mongosh --eval "
db.ausleihungen.updateOne(
{_id: ObjectId('BOOKING_ID_HIER')},
{\$set: {Status: 'active', LastUpdated: new Date()}}
)" Inventarsystem
```
Ersetze `BOOKING_ID_HIER` mit der ID deiner Ausleihe.
---
## 🔧 DEBUG: Alle geplanten Ausleihen anschauen
Führe auf deinem Server aus:
```bash
docker exec inventarsystem-mongodb mongosh --eval "
db.ausleihungen.find({Status: 'planned'}).pretty()
" Inventarsystem
```
Das zeigt dir:
- **Start**: Das gespeicherte Startdatum/Uhrzeit
- **Periode**: Die Schulstunde (falls verwendet)
- **Erstellungs-Zeit vs. aktuelle Zeit**: Vergleich
---
## 🎯 PROBLEM 1: Falsches Datum beim Erstellen
**Symptom**: Start-Zeit ist in der Zukunft (z.B. morgen statt heute)
**Lösung**:
1. In der Kalender-Ansicht das **richtige Datum** auswählen
2. Die **aktuelle Uhrzeit** für den Beginn nutzen (nicht eine zukünftige Zeit)
3. Für Schulstunden: Stelle sicher die **heutige Schulstunde** zu wählen
---
## 🎯 PROBLEM 2: Scheduler läuft nicht
**Symptom**: Keine Logs über Termin-Aktivierungen
**Überprüfung**:
```bash
# 1. Container-Logs anschauen
docker logs inventarsystem-app | grep -i "scheduler\|appointment\|status update"
# 2. Scheduler ist aktiviert?
docker exec inventarsystem-app python3 -c "
import sys
sys.path.insert(0, '/app/Web')
import settings as cfg
print(f'Scheduler aktiviert: {cfg.SCHEDULER_ENABLED}')
print(f'Scheduler Intervall: {cfg.SCHEDULER_INTERVAL_MIN} Minuten')
"
```
**Lösung wenn deaktiviert**:
- In `config.json` ändern:
```json
"scheduler": {
"enabled": true,
"interval_minutes": 1
}
```
- Container neu starten: `docker-compose restart app`
---
## 🎯 PROBLEM 3: Startzeit als nur-Zeit (kein Datum)
**Symptom**: Start-Zeit zeigt nur "13:30" ohne Datum
**Überprüfung**: In der Datenbank nach `{Start: {$type: "string"}}` suchen statt datetime
**Lösung**: Die Ausleihe muss mit einem vollständigen Datum+Uhrzeit erstellt werden.
---
## 🚀 AUTOMATISCHE AKTIVIERUNG TESTEN
Nach dem Fix: Erstelle eine Ausleihe für:
- **Heute** (das aktuelle Datum)
- **Schulstunde 7** (13:30-14:15)
- Startzeitpunkt sollte **jetzt oder in 1 Minute** sein
Warte 2 Minuten. Der Status sollte von `planned` zu `active` wechseln.
---
## 📊 SCHEDULER STATUS ÜBERPRÜFEN
```bash
# Zeige die letzten 50 Log-Zeilen
docker logs --tail 50 inventarsystem-app | tail -20
# Suche nach "Appointment status update"
docker logs inventarsystem-app 2>/dev/null | grep -i "Appointment status" | tail -5
```
Erwartete Log-Ausgabe:
```
Appointment status update finished: X changed (Y active, Z completed)
```
---
## 🔍 DETAILLIERTE DEBUG-INFOS
Wenn obiges nicht funktioniert, führe dies aus:
```bash
docker exec inventarsystem-app python3 -c "
from pymongo import MongoClient
import datetime
client = MongoClient('mongodb', 27017)
db = client['Inventarsystem']
aus = db['ausleihungen']
print('=== GEPLANTE AUSLEIHEN ===')
for b in aus.find({'Status': 'planned'}).limit(3):
print(f\"Start: {b.get('Start')} | Typ: {type(b.get('Start')).__name__}\")
print(f\"Periode: {b.get('Period')} | User: {b.get('User')}\")
print(f\"Jetzt > Start? {datetime.datetime.now() > b.get('Start', datetime.datetime.max)}\")
print()
"
```
---
## 💡 EMPFOHLENE EINSTELLUNG FÜR SCHULEN
In `config.json` verwende diese Werte:
```json
{
"scheduler": {
"enabled": true,
"interval_minutes": 1,
"backup_interval_hours": 24
}
}
```
Dies checkt **jede Minute** ob Termine aktiviert werden sollen - perfekt für Schulstunden.
---
## Weitere Hilfe?
Falls das nicht funktioniert:
1. Schreib mir die **exakte Uhrzeit** wann du die Ausleihe erstellt hast
2. Und wann sie hätte aktiviert werden sollen
3. Zeige mir ein Beispiel aus der DB mit `docker exec inventarsystem-mongodb mongosh...`
-36
View File
@@ -126,39 +126,3 @@ The `get_tenant_db` function queries `g.tenant_context` inside Flask, calculates
This ensures that scaling is extremely cheap on resources because 1 Application Container connects to 100 separate Tenant Databases using just 1 shared `MongoClient` pool.
---
1. Das Kernkonzept: Wie funktioniert dein Multi-Tenant-System?
Du betreibst keinen separaten Docker-Container für jeden einzelnen Kunden (Tenant). Das wäre extrem speicherintensiv und schwer zu warten. Stattdessen nutzt du ein „Database-per-Tenant“ & „Shared-App“ Modell:
Der App-Pool: In Docker laufen z.B. 3 identische app-Container, die sich die Arbeit teilen (Load-Balancing).
Das Routing (Nginx): Ein Nutzer ruft https://schule1.deinedomain.de auf. Nginx leitet die Anfrage an irgendeinen der freien app-Container weiter.
Die Tenant-Weiche (Python): Deine Python-App greift die Subdomain (schule1) aus der URL ab und lenkt alle Datenbankabfragen dieses Nutzers ausschließlich auf die MongoDB-Datenbank inventar_schule1.
Das Ergebnis: 100% isolierte Daten für jeden Mandanten, aber maximale Ressourceneffizienz auf dem Server.
2. Der Praxis-Ablauf: Einen neuen Kunden anlegen
Um eine neue Schule oder einen neuen Kunden mit eigener Subdomain an den Start zu bringen, brauchst du nur zwei Schritte:
DNS einrichten (Domain-Ebene):
Erstelle bei deinem Domain-Provider einen A-Record oder CNAME für die Subdomain (z.B. schule1), der auf die IP deines Servers zeigt.
Pro-Tipp: Richte dir einen Wildcard-Record (*.deinedomain.de) ein. Dann musst du bei neuen Kunden nie wieder in die DNS-Einstellungen gehen. Jede fiktive Subdomain zeigt dann automatisch auf deinen Server!
Datenbank initialisieren (Server-Ebene):
Führe deinen Wrapper auf dem Server aus, um dem Kunden eine Datenbank und einen initialen Admin-Nutzer zu generieren:
Das System ist nun für diesen Kunden unter schule1.deinedomain.de voll einsatzbereit.
3. Performance anpassen (Skalieren statt Neustarten)
Wenn du plötzlich 20 neue Kunden hast und der Server langsamer wird, musst du keine neuen Instanzen für genau diese Kunden bauen. Du erhöhst einfach die allgemeine Arbeitskraft deines Docker-Compose-Netzwerks:
Nginx bemerkt sofort, dass es nun 5 statt vorher z.B. 2 Container gibt, und verteilt die Last aller aktiven Subdomains dynamisch auf alle 5 Container. Es gibt dabei keine Downtime.
4. Nützliche Zusatz-Infos & Pro-Tipps
Zero-Downtime Soft-Restarts (Session Invalidation):
Manchmal hängt ein Kunde fest oder du hast configs geändert. Anstatt den ganzen Server (und alle Kunden) neu zu starten, nutzt du .run-tenant-cmd.sh restart-tenant schule1. Das leert nur den Redis-Cache und die Sessions dieses einen Kunden. Seine Nutzer müssen sich neu einloggen, alle anderen Mandanten merken davon nichts.
Datensicherheit / Backups:
Da jeder Tenant eine eigene Datenbank hat (inventar_schule1, inventar_schule2), kannst du im Fall eines Datenverlusts (z.B. ein Lehrer löscht versehentlich das halbe Inventar) das Backup nur für diesen einen Kunden wiederherstellen (mongorestore --db inventar_schule1 ...), ohne die Bestandsdaten der anderen 50 Schulen zu überschreiben.
SSL-Zertifikate:
Damit HTTPS für alle Subdomains automatisch funktioniert, generiere beim Setup unbedingt ein Wildcard-Zertifikat via Let's Encrypt (z.B. certbot certonly --manual -d "*.deinedomain.de"). Sonst zeigt der Browser bei einem neu angelegten Tenant eine Sicherheitswarnung, bis du das Zertifikat manuell erneuerst.
Kostenersparnis:
Da alle Container geteilt werden, kannst du auf einem Standard 4GB RAM / 2 vCPU VPS (ca. 5-10 Euro im Monat) problemlos 10-20 verschiedene Schulen parallel hosten, was dieses Architekturmodell extrem skalierbar und günstig macht.
-1
View File
@@ -1 +0,0 @@
[2026-04-19 20:20:39,502] ERROR in app: Error during scheduler shutdown: name '__file__' is not defined
+1
View File
@@ -15,3 +15,4 @@ python-barcode
openpyxl
cryptography
pywebpush
py-vapid==1.9.0
+78 -19
View File
@@ -21,28 +21,47 @@
--ui-radius: 12px;
}
@media (prefers-color-scheme: dark) {
:root {
--ui-bg: #0f172a;
--ui-bg-accent: #1e293b;
--ui-surface: #1e293b;
--ui-surface-soft: #25314a;
--ui-border: #334155;
--ui-text: #f1f5f9;
--ui-text-muted: #94a3b8;
--ui-title: #ffffff;
--ui-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
--ui-shadow-md: 0 10px 24px rgba(0, 0, 0, 0.5);
}
/* Make inputs visible in dark mode */
input, select, textarea {
background-color: var(--ui-surface-soft) !important;
color: var(--ui-text) !important;
border-color: var(--ui-border) !important;
:root[data-theme="dark"] {
--ui-bg: #0f172a;
--ui-bg-accent: #1e293b;
--ui-surface: #1e293b;
--ui-surface-soft: #25314a;
--ui-border: #334155;
--ui-text: #f1f5f9;
--ui-text-muted: #94a3b8;
--ui-title: #ffffff;
--ui-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.4);
--ui-shadow-md: 0 10px 24px rgba(0, 0, 0, 0.5);
}
/* Safe Area Support for iPad notches, Dynamic Island, and rounded corners */
html {
/* Ensure viewport-fit is respected */
viewport-fit: cover;
}
body {
/* Respect safe areas on mobile devices */
position: relative;
}
/* Future-proof: Support for various viewport and safe-area features */
@supports (padding: max(0px)) {
body {
/* Ensure body padding never conflicts with safe areas */
padding: 0;
}
}
/* Make inputs visible in dark mode */
:root[data-theme="dark"] input,
:root[data-theme="dark"] select,
:root[data-theme="dark"] textarea {
background-color: var(--ui-surface-soft) !important;
color: var(--ui-text) !important;
border-color: var(--ui-border) !important;
}
body {
font-family: "Manrope", "Segoe UI", "Noto Sans", sans-serif;
color: var(--ui-text);
@@ -172,11 +191,16 @@ select:focus {
box-shadow: none;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
/* Safe area insets for iPad notches and Dynamic Island */
padding-left: env(safe-area-inset-left, 0);
padding-right: env(safe-area-inset-right, 0);
}
.navbar-brand {
font-weight: 800;
letter-spacing: 0.02em;
/* Prevent navbar brand from extending into unsafe areas */
word-break: break-word;
}
@media (max-width: 900px) {
@@ -190,6 +214,41 @@ select:focus {
}
}
/* Mobile and tablet safe area handling for navbar */
@media (max-width: 992px) {
.navbar {
/* Ensure navbar spans full width but respects safe areas */
width: 100vw;
margin-left: calc(-50vw + 50%);
/* Safe area padding is already handled by navbar class */
}
.navbar .container-fluid {
width: 100%;
box-sizing: border-box;
/* Ensure padding accounts for safe areas on small screens */
padding-left: max(env(safe-area-inset-left, 0), 12px);
padding-right: max(env(safe-area-inset-right, 0), 12px);
}
.navbar-collapse {
/* Ensure collapsed navbar respects safe areas */
padding-left: env(safe-area-inset-left, 0);
padding-right: env(safe-area-inset-right, 0);
}
}
/* iPad-specific landscape and portrait handling */
@media (max-height: 600px) and (orientation: landscape) {
.navbar {
padding-top: max(env(safe-area-inset-top, 0), 4px);
}
.navbar .container-fluid {
padding-bottom: 4px;
}
}
/* Edit and generate buttons with theme-aware text for better visibility */
.edit-button, .duplicate-button, .generate-qr-button {
color: var(--ui-text) !important; /* Override any existing color */
+5 -5
View File
@@ -11,7 +11,7 @@
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:12px; margin:16px 0 20px;">
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface);">
<div style="font-size:0.85rem; color:#64748b;">Chain Status</div>
{% if verify_result.ok %}
<div style="font-size:1.35rem; font-weight:700; color:#166534;">OK</div>
@@ -19,15 +19,15 @@
<div style="font-size:1.35rem; font-weight:700; color:#991b1b;">FEHLER</div>
{% endif %}
</div>
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface);">
<div style="font-size:0.85rem; color:#64748b;">Einträge</div>
<div style="font-size:1.35rem; font-weight:700;">{{ verify_result.count }}</div>
</div>
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface);">
<div style="font-size:0.85rem; color:#64748b;">Letzter Index</div>
<div style="font-size:1.35rem; font-weight:700;">{{ verify_result.last_chain_index }}</div>
</div>
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface);">
<div style="font-size:0.85rem; color:#64748b;">Mismatch Count</div>
<div style="font-size:1.35rem; font-weight:700;">{{ verify_result.mismatches|length }}</div>
</div>
@@ -61,7 +61,7 @@
</div>
{% endif %}
<div style="border:1px solid #e2e8f0; border-radius:10px; background:#fff; padding:12px;">
<div style="border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface); padding:12px;">
<h3 style="margin-top:0;">Letzte Audit-Ereignisse</h3>
<div style="max-height:560px; overflow:auto;">
<table class="table" style="width:100%; border-collapse:collapse;">
+4 -4
View File
@@ -161,7 +161,7 @@
</div>
<div id="invoice-modal" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;">
<div style="max-width:760px; margin:40px auto; background:#fff; border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
<div style="max-width:760px; margin:40px auto; background: var(--ui-surface); border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:18px;">
<div>
<h2 style="margin:0;">Rechnung erstellen</h2>
@@ -174,15 +174,15 @@
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:16px; margin-bottom:16px;">
<div>
<label for="invoice-item" style="display:block; font-weight:700; margin-bottom:6px;">Element</label>
<input id="invoice-item" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
<input id="invoice-item" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
</div>
<div>
<label for="invoice-borrower" style="display:block; font-weight:700; margin-bottom:6px;">Schüler</label>
<input id="invoice-borrower" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
<input id="invoice-borrower" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
</div>
<div>
<label for="invoice-code" style="display:block; font-weight:700; margin-bottom:6px;">Element-ID / Code</label>
<input id="invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
<input id="invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
</div>
<div>
<label for="invoice-amount" style="display:block; font-weight:700; margin-bottom:6px;">Preis</label>
+3 -3
View File
@@ -17,14 +17,14 @@
</div>
</div>
<div style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:14px; margin-bottom:12px;">
<div style="background: var(--ui-surface); border:1px solid #e2e8f0; border-radius:14px; padding:14px; margin-bottom:12px;">
<input id="damage-search" type="text" placeholder="Suche nach Name, Code, Typ, Benutzer oder Schaden..." style="width:100%; padding:10px 12px; border:1px solid #d1d5db; border-radius:10px;">
</div>
<div style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:0; overflow:hidden;">
<div style="background: var(--ui-surface); border:1px solid #e2e8f0; border-radius:14px; padding:0; overflow:hidden;">
<table id="damage-table" style="width:100%; border-collapse:collapse;">
<thead>
<tr style="background:#f8fafc;">
<tr style="background: var(--ui-surface-soft);">
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Item</th>
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Status</th>
<th style="text-align:left; padding:11px; border-bottom:1px solid #e5e7eb;">Letzte Meldung</th>
+89 -22
View File
@@ -14,8 +14,20 @@
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#2c3e50" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1a252f" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#2c3e50" id="meta-theme-color">
<script>
// Init theme early to prevent FOUC (Flash of Unstyled Content)
(function() {
var theme = localStorage.getItem('inventarsystem-theme');
if (theme === 'dark' || (!theme && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.setAttribute('data-theme', 'dark');
document.getElementById('meta-theme-color').setAttribute('content', '#1a252f');
} else {
document.documentElement.setAttribute('data-theme', 'light');
document.getElementById('meta-theme-color').setAttribute('content', '#2c3e50');
}
})();
</script>
<meta name="csrf-token" content="{{ csrf_token }}">
<title>{% block title %}Inventarsystem{% endblock %}</title>
{% block head %}
@@ -117,21 +129,20 @@
--module-brand-icon: "📚";
}
@media (prefers-color-scheme: dark) {
:root {
--module-primary-color: #1a252f;
--module-primary-light: #2c3e50;
--module-accent-color: #2980b9;
--module-brand-icon: "📦";
--page-bg-gradient: linear-gradient(180deg, #0f172a, #1e293b);
--page-bg-overlay1: rgba(56, 189, 248, 0.05);
--page-bg-overlay2: rgba(59, 130, 246, 0.06);
}
html[data-module="library"] {
--module-primary-color: #4a354d;
--module-primary-light: #614666;
--module-accent-color: #8e44ad;
}
:root[data-theme="dark"] {
--module-primary-color: #1a252f;
--module-primary-light: #2c3e50;
--module-accent-color: #2980b9;
--module-brand-icon: "📦";
--page-bg-gradient: linear-gradient(180deg, #0f172a, #1e293b);
--page-bg-overlay1: rgba(56, 189, 248, 0.05);
--page-bg-overlay2: rgba(59, 130, 246, 0.06);
}
html[data-theme="dark"][data-module="library"] {
--module-primary-color: #4a354d;
--module-primary-light: #614666;
--module-accent-color: #8e44ad;
}
/* Global styles */
@@ -158,13 +169,19 @@
background-color: var(--module-primary-color) !important;
position: sticky;
top: 0;
padding-top: env(safe-area-inset-top, 0); /* iOS Safe Area */
/* iOS Safe Area insets for notches and rounded corners */
padding-top: env(safe-area-inset-top, 0);
padding-left: env(safe-area-inset-left, 0);
padding-right: env(safe-area-inset-right, 0);
z-index: 1900;
}
.navbar .container-fluid {
gap: 10px;
padding-bottom: env(safe-area-inset-bottom, 0); /* Fallback */
/* Ensure container respects safe areas on all sides */
padding-left: max(env(safe-area-inset-left, 0), 16px);
padding-right: max(env(safe-area-inset-right, 0), 16px);
padding-bottom: env(safe-area-inset-bottom, 0);
}
.navbar-brand {
@@ -495,7 +512,7 @@
/* Account dropdown stays light theme on mobile */
.dropdown-menu-end {
background-color: #ffffff;
background-color: var(--ui-surface);
border: 1px solid #dee2e6;
}
@@ -504,7 +521,7 @@
}
.dropdown-menu-end .dropdown-item:hover {
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
color: #007bff;
}
}
@@ -861,6 +878,15 @@
<a class="nav-link nav-priority-link {% if current_path == url_for('upload_admin') %}nav-active{% endif %}" href="{{ url_for('upload_admin') }}"> Hochladen</a>
</li>
{% endif %}
<li class="nav-item">
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li class="nav-item dropdown ms-lg-auto">
<a class="nav-link dropdown-toggle" href="#" id="invMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
Mehr Optionen
@@ -982,6 +1008,15 @@
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}">📖 Hochladen</a>
</li>
{% endif %}
<li class="nav-item">
<button id="themeToggleBtn" class="btn btn-link nav-link px-3" aria-label="Dark Mode umschalten" title="Theme umschalten">
<span class="theme-icon-light" style="display: none;">☀️</span>
<span class="theme-icon-dark" style="display: none;">🌙</span>
</button>
</li>
<li class="nav-item dropdown ms-lg-auto">
<a class="nav-link dropdown-toggle" href="#" id="libMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">
Mehr Optionen
@@ -1098,7 +1133,7 @@
#onboarding-modal {
width: min(680px, 100%);
background: #ffffff;
background: var(--ui-surface);
border-radius: 14px;
border: 1px solid #dbe3ee;
box-shadow: 0 18px 35px rgba(15, 23, 42, 0.25);
@@ -1825,5 +1860,37 @@
<!-- Push Notifications Script -->
<script src="{{ url_for('static', filename='js/push-notifications.js') }}"></script>
<!-- Theme Toggle Script -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const toggleBtns = document.querySelectorAll('#themeToggleBtn');
if (toggleBtns.length === 0) return;
function updateIcons(theme) {
const isDark = theme === 'dark';
document.querySelectorAll('.theme-icon-light').forEach(icon => icon.style.display = isDark ? 'inline' : 'none');
document.querySelectorAll('.theme-icon-dark').forEach(icon => icon.style.display = isDark ? 'none' : 'inline');
}
// Get current setup from initial script in head
let currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
updateIcons(currentTheme);
toggleBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', currentTheme);
localStorage.setItem('inventarsystem-theme', currentTheme);
document.getElementById('meta-theme-color').setAttribute('content', currentTheme === 'dark' ? '#1a252f' : '#2c3e50');
updateIcons(currentTheme);
});
});
});
</script>
</body>
</html>
+1 -1
View File
@@ -53,7 +53,7 @@
max-width: 500px;
margin: 50px auto;
padding: 30px;
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
+8 -8
View File
@@ -52,7 +52,7 @@
}
.summary-card {
background: #fff;
background: var(--ui-surface);
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 14px 16px;
@@ -85,11 +85,11 @@
padding: 11px 14px;
border: 1px solid #d6dde6;
border-radius: 12px;
background: #fff;
background: var(--ui-surface);
}
.panel {
background: #fff;
background: var(--ui-surface);
border: 1px solid #e2e8f0;
border-radius: 18px;
padding: 18px;
@@ -120,7 +120,7 @@
text-transform: uppercase;
letter-spacing: 0.04em;
color: #64748b;
background: #f8fafc;
background: var(--ui-surface-soft);
}
.library-table tr:hover td {
@@ -408,7 +408,7 @@
</div>
<div id="damage-invoice-modal" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;">
<div style="max-width:760px; margin:40px auto; background:#fff; border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
<div style="max-width:760px; margin:40px auto; background: var(--ui-surface); border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:18px;">
<div>
<h2 style="margin:0;">Rechnung erstellen</h2>
@@ -421,15 +421,15 @@
<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(240px, 1fr)); gap:16px; margin-bottom:16px;">
<div>
<label for="damage-invoice-item" style="display:block; font-weight:700; margin-bottom:6px;">Element</label>
<input id="damage-invoice-item" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
<input id="damage-invoice-item" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
</div>
<div>
<label for="damage-invoice-borrower" style="display:block; font-weight:700; margin-bottom:6px;">Schüler</label>
<input id="damage-invoice-borrower" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
<input id="damage-invoice-borrower" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
</div>
<div>
<label for="damage-invoice-code" style="display:block; font-weight:700; margin-bottom:6px;">Element-ID / Code</label>
<input id="damage-invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background:#f8fafc;">
<input id="damage-invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
</div>
<div>
<label for="damage-invoice-amount" style="display:block; font-weight:700; margin-bottom:6px;">Preis</label>
+2 -2
View File
@@ -40,7 +40,7 @@
}
.invoice-card {
background: #fff;
background: var(--ui-surface);
border: 1px solid #e2e8f0;
border-radius: 16px;
padding: 18px;
@@ -65,7 +65,7 @@
text-transform: uppercase;
letter-spacing: 0.04em;
color: #64748b;
background: #f8fafc;
background: var(--ui-surface-soft);
}
.invoice-table tr:hover td {
+13 -13
View File
@@ -18,7 +18,7 @@
.library-header h1 {
margin: 0 0 5px 0;
font-size: 2em;
color: #333;
color: var(--ui-text);
}
.library-header p {
@@ -48,7 +48,7 @@
/* Scanner panel */
.library-scan-panel {
background: #ffffff;
background: var(--ui-surface);
border: 1px solid #d9dde4;
border-radius: 8px;
padding: 14px;
@@ -68,7 +68,7 @@
border: 1px solid #d0d7e2;
border-radius: 6px;
font-size: 14px;
background: #fff;
background: var(--ui-surface);
}
.library-scan-status {
@@ -95,7 +95,7 @@
border: 1px solid #d9dde4;
border-radius: 8px;
overflow: hidden;
background: #fff;
background: var(--ui-surface);
max-width: 460px;
}
@@ -111,7 +111,7 @@
border-radius: 6px;
cursor: pointer;
font-weight: 500;
color: #333;
color: var(--ui-text);
transition: all 0.2s ease;
}
@@ -160,7 +160,7 @@
.filter-item label {
font-weight: 600;
font-size: 0.9em;
color: #333;
color: var(--ui-text);
margin-bottom: 6px;
}
@@ -206,7 +206,7 @@
}
.library-items-table thead {
background: #f8f9fa;
background: var(--ui-surface-soft);
border-bottom: 2px solid #ddd;
}
@@ -214,7 +214,7 @@
padding: 14px 16px;
text-align: left;
font-weight: 600;
color: #333;
color: var(--ui-text);
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -228,7 +228,7 @@
}
.library-items-table tbody tr:hover {
background: #f8f9fa;
background: var(--ui-surface-soft);
}
.library-items-table tbody tr:last-child td {
@@ -238,7 +238,7 @@
/* Table cells */
.table-title {
font-weight: 600;
color: #333;
color: var(--ui-text);
}
.table-status {
@@ -320,7 +320,7 @@
}
.modal-content {
background-color: #fff;
background-color: var(--ui-surface);
border-radius: 8px;
padding: 20px;
max-width: 900px;
@@ -373,7 +373,7 @@
}
.close:hover {
color: #333;
color: var(--ui-text);
}
/* Mobile responsive */
@@ -494,7 +494,7 @@
</div>
<div class="filter-buttons">
<button id="applyFilterBtn" class="button" style="background: #4f46e5; color: white;">Filter anwenden</button>
<button id="clearFilterBtn" class="button" style="background: #f0f2f5; color: #333;">Zurücksetzen</button>
<button id="clearFilterBtn" class="button" style="background: #f0f2f5; color: var(--ui-text);">Zurücksetzen</button>
</div>
</div>
+1 -1
View File
@@ -337,7 +337,7 @@
color: #0d6efd;
}
.license-content {
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
padding: 20px 30px;
border-radius: 0 0 5px 5px;
border: 1px solid #dee2e6;
+1 -1
View File
@@ -217,7 +217,7 @@
h1 {
text-align: center;
margin-bottom: 30px;
color: #333;
color: var(--ui-text);
}
.filter-controls {
+9 -9
View File
@@ -20,7 +20,7 @@
gap: 12px;
padding: 12px;
border-bottom: 1px solid var(--ui-border);
background: #f8fafc;
background: var(--ui-surface-soft);
}
.calendar-header button {
@@ -36,7 +36,7 @@
}
.calendar-header button:hover {
background: #f1f5f9;
background: var(--ui-bg);
}
.calendar-header span {
@@ -80,7 +80,7 @@
}
.cal-cell:not(.header):not(.empty):hover {
background: #f1f5f9;
background: var(--ui-bg);
}
.cal-cell.empty {
@@ -115,7 +115,7 @@
.calendar-day-details {
border-top: 1px solid #e7edf5;
padding: 12px;
background: #f8fafc;
background: var(--ui-surface-soft);
display: none;
}
@@ -1646,7 +1646,7 @@
<span>🛠️</span>
<span id="toggle-damage-history-text">Historie anzeigen</span>
</button>
<div id="damage-history-panel" style="display:none; margin-top:8px; border:1px solid #e7edf5; border-radius:10px; padding:12px; background:#f8fafc;">
<div id="damage-history-panel" style="display:none; margin-top:8px; border:1px solid #e7edf5; border-radius:10px; padding:12px; background: var(--ui-surface-soft);">
<div style="display:grid; gap:10px;">${damageHistoryHtml}</div>
</div>
</div>
@@ -2847,7 +2847,7 @@
}
.filter-toggle, .clear-filter {
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
border: 1px solid #dee2e6;
border-radius: 3px;
padding: 3px 8px;
@@ -3028,7 +3028,7 @@
margin-right: 8px;
border: 1px solid #dbe3ee;
border-radius: 10px;
background: #f8fafc;
background: var(--ui-surface-soft);
color: var(--ui-text);
justify-content: center;
align-items: center;
@@ -3859,7 +3859,7 @@
max-height: 400px;
overflow: hidden;
border-radius: 8px;
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
}
/* Borrower and appointment info panels in modal */
@@ -3891,7 +3891,7 @@
.detail-value { color: #212529; flex: 1; word-wrap: break-word; }
.detail-group.full-width .detail-label { margin-bottom: 5px; }
.detail-group.full-width .detail-value {
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
padding: 10px;
border-radius: 4px;
border: 1px solid #dee2e6;
+11 -11
View File
@@ -104,7 +104,7 @@
border-radius: 8px;
padding: 9px 12px;
cursor: pointer;
background: #f8fafc;
background: var(--ui-surface-soft);
color: var(--ui-title);
font-weight: 600;
text-align: center;
@@ -152,7 +152,7 @@
}
.filter-toggle, .clear-filter {
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
border: 1px solid #dee2e6;
border-radius: 3px;
padding: 3px 8px;
@@ -333,7 +333,7 @@
margin-right: 8px;
border: 1px solid #dbe3ee;
border-radius: 10px;
background: #f8fafc;
background: var(--ui-surface-soft);
color: var(--ui-text);
justify-content: center;
align-items: center;
@@ -706,7 +706,7 @@
padding: 15px;
border: 1px solid var(--ui-border);
border-radius: 5px;
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
}
.filter-inputs h3 {
@@ -1049,7 +1049,7 @@
}
.detail-group.full-width .detail-value {
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
padding: 10px;
border-radius: 4px;
border: 1px solid #dee2e6;
@@ -1069,7 +1069,7 @@
max-height: 400px;
overflow: hidden;
border-radius: 8px;
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
}
.modal-image {
@@ -2022,7 +2022,7 @@
gap: 12px;
padding: 12px;
border-bottom: 1px solid var(--ui-border);
background: #f8fafc;
background: var(--ui-surface-soft);
}
.calendar-header button {
@@ -2038,7 +2038,7 @@
}
.calendar-header button:hover {
background: #f1f5f9;
background: var(--ui-bg);
}
.calendar-header span {
@@ -2082,7 +2082,7 @@
}
.cal-cell:not(.header):not(.empty):hover {
background: #f1f5f9;
background: var(--ui-bg);
}
.cal-cell.empty {
@@ -2117,7 +2117,7 @@
.calendar-day-details {
border-top: 1px solid #e7edf5;
padding: 12px;
background: #f8fafc;
background: var(--ui-surface-soft);
display: none;
}
@@ -4456,7 +4456,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<span>🛠️</span>
<span id="toggle-damage-history-text">Historie anzeigen</span>
</button>
<div id="damage-history-panel" style="display:none; margin-top:8px; border:1px solid #e7edf5; border-radius:10px; padding:12px; background:#f8fafc;">
<div id="damage-history-panel" style="display:none; margin-top:8px; border:1px solid #e7edf5; border-radius:10px; padding:12px; background: var(--ui-surface-soft);">
<div style="display:grid; gap:10px;">${damageHistoryHtml}</div>
</div>
</div>
+3 -3
View File
@@ -78,12 +78,12 @@
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
margin-bottom: 20px;
background-color: #fff;
background-color: var(--ui-surface);
border: 1px solid #e9ecef;
}
.card-header {
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
padding: 15px 20px;
border-bottom: 1px solid #e9ecef;
}
@@ -170,7 +170,7 @@
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
border-radius: 4px;
border-left: 3px solid #6c757d;
}
+4 -4
View File
@@ -61,7 +61,7 @@
bottom: 125%;
left: 50%;
transform: translateX(-50%);
background-color: #333;
background-color: var(--ui-text);
color: white;
padding: 5px 10px;
border-radius: 4px;
@@ -405,7 +405,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
.borrowed-item {
background-color: #fff;
background-color: var(--ui-surface);
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
@@ -457,7 +457,7 @@ document.addEventListener('DOMContentLoaded', function() {
.exemplar-details {
margin-top: 10px;
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
padding: 10px;
border-radius: 5px;
border-left: 3px solid #6c757d;
@@ -515,7 +515,7 @@ document.addEventListener('DOMContentLoaded', function() {
.no-items-message {
text-align: center;
padding: 30px;
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
border-radius: 5px;
color: #6c757d;
margin-top: 20px;
+2 -2
View File
@@ -17,7 +17,7 @@
<div id="push-notification-settings" style="margin-bottom: 24px;"></div>
<div style="display:grid; grid-template-columns:1fr; gap:14px;">
<section style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
<section style="background: var(--ui-surface); border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
<h2 style="margin:0 0 12px; font-size:1.15rem;">Meine Benachrichtigungen</h2>
{% if user_notifications %}
<div style="display:grid; gap:10px;">
@@ -53,7 +53,7 @@
</section>
{% if is_admin_user %}
<section style="background:#fff; border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
<section style="background: var(--ui-surface); border:1px solid #e2e8f0; border-radius:14px; padding:16px;">
<h2 style="margin:0 0 12px; font-size:1.15rem;">Admin-Benachrichtigungen</h2>
{% if admin_notifications %}
<div style="display:grid; gap:10px;">
+4 -4
View File
@@ -121,7 +121,7 @@
--primary-dark: #2980b9;
--success-color: #2ecc71;
--error-color: #e74c3c;
--text-color: #333;
--text-color: var(--ui-text);
--light-bg: #f9f9f9;
--border-radius: 8px;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
@@ -163,7 +163,7 @@ body {
}
.content {
background-color: #fff;
background-color: var(--ui-surface);
padding: 2rem;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
@@ -345,7 +345,7 @@ input::placeholder {
padding: 10px 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f8fafc;
background: var(--ui-surface-soft);
}
.password-rules-title {
@@ -406,7 +406,7 @@ input::placeholder {
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 10px;
background: #f8fafc;
background: var(--ui-surface-soft);
}
.permission-panel h4 {
+1 -1
View File
@@ -129,7 +129,7 @@ function createLoadingIndicator(message) {
}
.reset-loading-message {
color: #333;
color: var(--ui-text);
font-size: 16px;
font-weight: 500;
}
+8 -8
View File
@@ -207,7 +207,7 @@
}
.reset-item-info {
background: #f8f9fa;
background: var(--ui-surface-soft);
border-radius: 8px;
padding: 20px;
margin-bottom: 25px;
@@ -216,7 +216,7 @@
.reset-item-info h3 {
margin: 0 0 15px 0;
color: #333;
color: var(--ui-text);
font-size: 1.3rem;
}
@@ -232,7 +232,7 @@
}
.reset-item-details strong {
color: #333;
color: var(--ui-text);
}
.reset-warning-section {
@@ -285,7 +285,7 @@
flex-direction: column;
cursor: pointer;
font-weight: 500;
color: #333;
color: var(--ui-text);
}
.reset-action-desc {
@@ -298,7 +298,7 @@
.active-borrowings {
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
background: var(--ui-surface-soft);
border-radius: 5px;
font-size: 0.9rem;
}
@@ -321,7 +321,7 @@
.reset-options-section h4 {
margin: 0 0 15px 0;
color: #333;
color: var(--ui-text);
}
.reset-option {
@@ -365,7 +365,7 @@
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
color: var(--ui-text);
}
.reset-reason-section textarea {
@@ -391,7 +391,7 @@
display: flex;
justify-content: flex-end;
gap: 15px;
background: #f8f9fa;
background: var(--ui-surface-soft);
border-radius: 0 0 12px 12px;
}
@@ -45,7 +45,7 @@
}
h1 {
color: #333;
color: var(--ui-text);
font-size: 28px;
margin-bottom: 10px;
}
@@ -67,7 +67,7 @@
}
.info-box strong {
color: #333;
color: var(--ui-text);
}
.info-box p {
@@ -106,7 +106,7 @@
.btn-secondary {
background: #e0e0e0;
color: #333;
color: var(--ui-text);
}
.btn-secondary:hover {
+2 -2
View File
@@ -22,7 +22,7 @@
}
.student-card-form {
background: #f5f5f5;
background: var(--ui-bg);
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
@@ -43,7 +43,7 @@
.form-group label {
font-weight: 600;
margin-bottom: 5px;
color: #333;
color: var(--ui-text);
}
.form-group input,
+1 -1
View File
@@ -1601,7 +1601,7 @@ document.addEventListener('DOMContentLoaded', function() {
.booking-summary {
margin: 15px 0;
padding: 10px;
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9em;
+6 -6
View File
@@ -72,7 +72,7 @@
.tutorial-side {
grid-column: span 12;
background: #ffffff;
background: var(--ui-surface);
border: 1px solid #cfdbe8;
border-radius: 16px;
padding: 16px;
@@ -102,7 +102,7 @@
.workflow-nav button {
border: 1px solid #dbe3ee;
background: #f8fafc;
background: var(--ui-surface-soft);
color: #0f172a;
border-radius: 12px;
padding: 11px 12px;
@@ -133,7 +133,7 @@
.workflow-step {
grid-column: span 12;
background: #ffffff;
background: var(--ui-surface);
border: 1px solid #cfdbe8;
border-radius: 16px;
padding: 18px;
@@ -331,17 +331,17 @@
<h3>Tägliche Aufgaben</h3>
<p>Das ist der gewöhnliche Arbeitsablauf während eines Schultages.</p>
<div style="background: #f8fafc; border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
<div style="background: var(--ui-surface-soft); border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
<p style="margin-bottom: 10px; font-weight: 700; color: #0f172a;">Morgens:</p>
<p style="margin: 0; color: #334155;">Öffnen Sie die Startseite und prüfen Sie offene Reservierungen oder Rückgabefristen.</p>
</div>
<div style="background: #f8fafc; border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
<div style="background: var(--ui-surface-soft); border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
<p style="margin-bottom: 10px; font-weight: 700; color: #0f172a;">Mittags:</p>
<p style="margin: 0; color: #334155;">Neue Artikel oder Bücher hinzufügen oder Ausleihungen begründen.</p>
</div>
<div style="background: #f8fafc; border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
<div style="background: var(--ui-surface-soft); border: 1px solid #dbe3ee; border-radius: 12px; padding: 14px; margin: 14px 0;">
<p style="margin-bottom: 10px; font-weight: 700; color: #0f172a;">Nachmittags:</p>
<p style="margin: 0; color: #334155;">Rückgaben buchen und kurz prüfen, ob alles in Ordnung ist.</p>
</div>
+12 -12
View File
@@ -78,14 +78,14 @@
.book-info {
padding: 15px;
background-color: #fff;
background-color: var(--ui-surface);
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 10px;
}
.book-info h4 {
color: #333;
color: var(--ui-text);
margin: 0 0 10px 0;
font-size: 1.2em;
font-weight: bold;
@@ -115,7 +115,7 @@
}
.book-description h5 {
color: #333;
color: var(--ui-text);
margin: 0 0 8px 0;
font-size: 1em;
font-weight: bold;
@@ -125,7 +125,7 @@
max-height: 150px;
overflow-y: auto;
padding: 5px;
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
border: 1px solid #e9ecef;
border-radius: 3px;
font-size: 0.9em;
@@ -187,7 +187,7 @@
font-style: italic;
text-align: center;
padding: 20px;
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
border: 1px solid #dee2e6;
border-radius: 4px;
}
@@ -334,7 +334,7 @@
}
.popup-content p {
color: #333;
color: var(--ui-text);
margin: 15px 0;
line-height: 1.5;
font-size: 1em;
@@ -380,7 +380,7 @@
}
.popup-close-x:hover {
color: #333;
color: var(--ui-text);
}
/* Upload form styles */
@@ -388,13 +388,13 @@
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
background-color: var(--ui-surface);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.upload-form h1 {
color: #333;
color: var(--ui-text);
margin-bottom: 30px;
text-align: center;
font-size: 2em;
@@ -408,7 +408,7 @@
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
color: var(--ui-text);
}
.form-group input,
@@ -428,14 +428,14 @@
}
.filter-inputs {
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
padding: 20px;
border-radius: 5px;
margin: 20px 0;
}
.filter-inputs h3 {
color: #333;
color: var(--ui-text);
margin-bottom: 15px;
font-size: 1.2em;
}
+3 -3
View File
@@ -407,14 +407,14 @@
}
.filter-bar {
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
padding: 15px;
border-radius: 6px;
border: 1px solid #dee2e6;
}
.password-requirements {
background-color: #f8f9fa;
background-color: var(--ui-surface-soft);
padding: 10px;
border-radius: 4px;
margin-top: 10px;
@@ -439,7 +439,7 @@
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 12px;
background: #f8fafc;
background: var(--ui-surface-soft);
}
.permissions-group h6 {
-107
View File
@@ -1,107 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
SUDO=""
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
VENV_PYTHON="$SCRIPT_DIR/.venv/bin/python"
if [ ! -x "$VENV_PYTHON" ]; then
echo "Error: .venv python not found at $VENV_PYTHON"
echo "Create it first, e.g.: python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt"
exit 1
fi
if [ "$(id -u)" -ne 0 ] && [ ! -w "$SCRIPT_DIR/.venv" ]; then
echo "Fixing ownership of .venv for current user..."
if [ -n "$SUDO" ]; then
$SUDO chown -R "$(id -un):$(id -gn)" "$SCRIPT_DIR/.venv"
else
echo "Error: .venv is not writable and sudo is unavailable."
exit 1
fi
fi
for path in "$SCRIPT_DIR/dist" "$SCRIPT_DIR/build"; do
if [ -e "$path" ] && [ "$(id -u)" -ne 0 ]; then
echo "Ensuring ownership of $(basename "$path") for current user..."
if [ -n "$SUDO" ]; then
$SUDO chown -R "$(id -un):$(id -gn)" "$path"
else
if [ ! -w "$path" ]; then
echo "Error: $path is not writable and sudo is unavailable."
exit 1
fi
fi
fi
done
echo "Installing Nuitka build dependencies in .venv..."
"$VENV_PYTHON" -m pip install --upgrade pip setuptools wheel
"$VENV_PYTHON" -m pip install --upgrade "nuitka==2.8.10" ordered-set zstandard
DIST_DIR="$SCRIPT_DIR/dist"
BUILD_DIR="$SCRIPT_DIR/build"
OUTPUT_NAME="inventarsystem"
mkdir -p "$DIST_DIR" "$BUILD_DIR"
NUITKA_DATA_ARGS=()
if [ -d "$SCRIPT_DIR/Web/templates" ]; then
NUITKA_DATA_ARGS+=("--include-data-dir=$SCRIPT_DIR/Web/templates=templates")
fi
if [ -d "$SCRIPT_DIR/Web/static" ]; then
NUITKA_DATA_ARGS+=("--include-data-dir=$SCRIPT_DIR/Web/static=static")
fi
if [ -d "$SCRIPT_DIR/uploads" ]; then
NUITKA_DATA_ARGS+=("--include-data-dir=$SCRIPT_DIR/uploads=uploads")
fi
ORIGINAL_PERF_PARANOID=""
if [ -r /proc/sys/kernel/perf_event_paranoid ]; then
ORIGINAL_PERF_PARANOID="$(cat /proc/sys/kernel/perf_event_paranoid)"
if [ "$ORIGINAL_PERF_PARANOID" -gt 1 ]; then
echo "Temporarily setting kernel.perf_event_paranoid=1 for Nuitka build compatibility..."
if ! $SUDO sh -c 'echo 1 > /proc/sys/kernel/perf_event_paranoid'; then
echo "Warning: Could not adjust kernel.perf_event_paranoid automatically."
echo "Run: sudo sh -c 'echo 1 > /proc/sys/kernel/perf_event_paranoid' and retry."
exit 1
fi
fi
fi
restore_perf_setting() {
if [ -n "$ORIGINAL_PERF_PARANOID" ] && [ "$ORIGINAL_PERF_PARANOID" -gt 1 ]; then
$SUDO sh -c "echo $ORIGINAL_PERF_PARANOID > /proc/sys/kernel/perf_event_paranoid" >/dev/null 2>&1 || true
fi
}
trap restore_perf_setting EXIT
echo "Building standalone binary with Nuitka..."
"$VENV_PYTHON" -m nuitka \
--standalone \
--assume-yes-for-downloads \
--follow-imports \
--output-dir="$DIST_DIR" \
--output-filename="$OUTPUT_NAME" \
"${NUITKA_DATA_ARGS[@]}" \
--remove-output \
"$SCRIPT_DIR/Web/app.py"
APP_DIST_DIR="$DIST_DIR/app.dist"
if [ -d "$APP_DIST_DIR" ]; then
echo "Nuitka build complete."
echo "Run with: $APP_DIST_DIR/$OUTPUT_NAME"
else
echo "Build finished, but expected output directory not found: $APP_DIST_DIR"
echo "Check Nuitka output above."
exit 1
fi
+15 -19
View File
@@ -8,9 +8,19 @@
# Scale example: To support 10 tenants with 20 users each:
# docker-compose -f docker-compose-multitenant.yml up -d --scale app=10
version: '3.9'
services:
# Management Container for multi-tenant scripts
tenant-manager:
image: docker:cli
container_name: tenant-manager
restart: "no"
profiles:
- tools
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- .:/workspace
entrypoint: ["sh", "-c", "cd /workspace && ./manage-tenant.sh \"$$@\"", "--"]
nginx:
image: nginx:1.27-alpine
container_name: inventarsystem-nginx
@@ -59,9 +69,9 @@ services:
image: mongo:7.0
container_name: inventarsystem-mongodb
restart: unless-stopped
command: mongod --wiredTigerCacheSizeGB 2 --journal --dbPath /data/db
ports:
- "27017:27017"
command: mongod --wiredTigerCacheSizeGB 2
expose:
- "27017"
volumes:
- mongodb_data:/data/db
- ./docker/mongo:/docker-entrypoint-initdb.d:ro
@@ -181,9 +191,6 @@ volumes:
networks:
inventar-net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
# Scale guidance:
# 1 tenant (20 users): 1 app instance + redis + mongodb = ~500MB
@@ -197,14 +204,3 @@ networks:
# - Large (10-20 tenants): 8GB RAM, 4-8 CPU cores
# - Jumbo (20+ tenants): 16GB+ RAM, 8+ CPU cores with clustering
# Management Container for multi-tenant scripts
tenant-manager:
image: bash
container_name: tenant-manager
restart: "no"
profiles:
- tools
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- .:/workspace
entrypoint: ["bash", "-c", "cd /workspace && ./manage-tenant.sh \"$$@\"", "--"]
+5 -4
View File
@@ -37,7 +37,8 @@ upstream inventar_backend {
# Main HTTPS server block
server {
listen 443 ssl http2;
listen 443 ssl;
http2 on;
server_name ~^(?<tenant>[a-z0-9-]+)?\.?example\.com$;
# SSL Configuration (wildcard certificate)
@@ -75,9 +76,9 @@ server {
application/x-javascript application/xml+rss application/json;
gzip_comp_level 5;
# Access/Error logs with tenant in filename
access_log /var/log/nginx/inventar_access_${tenant}.log combined buffer=32k;
error_log /var/log/nginx/inventar_error_${tenant}.log warn;
# Access/Error logs
access_log /var/log/nginx/inventar_access.log combined buffer=32k;
error_log /var/log/nginx/inventar_error.log warn;
# Root location - proxy to app with tenant context
location / {
+5 -4
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/bin/sh
# Script to manage multitenant deployment
# Allows adding, removing, and restarting tenants without downtime for others
@@ -48,11 +48,12 @@ case "$COMMAND" in
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
if [ -n "$APP_CONTAINER" ]; then
docker exec $APP_CONTAINER python3 -c "
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient
import sys; sys.path.insert(0, '/app/Web'); import settings; from pymongo import MongoClient; import hashlib
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
db = client[f'{settings.MONGODB_DB}_{sys.argv[1]}']
db.users.insert_one({'username': 'admin', 'password': 'hashed_password_here', 'role': 'admin'})
print(f'Tenant {sys.argv[1]} database initialized.')
hashed_pw = hashlib.sha512('admin123'.encode()).hexdigest()
db.users.insert_one({'Username': 'admin', 'Password': hashed_pw, 'Role': 'admin', 'Name': 'Admin', 'Nachname': 'User'})
print(f'Tenant {sys.argv[1]} database initialized. Default admin: admin / admin123')
" "$TENANT_ID"
echo "Tenant '$TENANT_ID' successfully added. Ready to use."
else
+1
View File
@@ -14,3 +14,4 @@ python-barcode
openpyxl
cryptography
pywebpush
py-vapid==1.9.0
+2 -1
View File
@@ -3,5 +3,6 @@ set -euo pipefail
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
"$SCRIPT_DIR/stop.sh"
"$SCRIPT_DIR/stop.sh" "$@"
"$SCRIPT_DIR/start.sh" "$@"
"$SCRIPT_DIR/start.sh"
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
# Wrapper to run tenant management fully containerized via docker-compose
docker-compose -f docker-compose-multitenant.yml --profile tools run --rm tenant-manager "$@"
docker compose -f docker-compose-multitenant.yml --profile tools run --rm tenant-manager "$@"
+17 -11
View File
@@ -14,10 +14,11 @@ if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
fi
NUITKA_BUILD_VALUE="0"
HTTP_PORT_VALUE="80"
HTTPS_PORT_VALUE="443"
HTTP_PORT_VALUE="8001"
HTTPS_PORT_VALUE="8443"
CRON_SETUP_VALUE="${INVENTAR_SETUP_CRON:-1}"
APP_IMAGE_VALUE="${INVENTAR_APP_IMAGE:-$APP_IMAGE_REPO:latest}"
COMPOSE_FILE="docker-compose.yml"
usage() {
cat <<EOF
@@ -26,6 +27,7 @@ Usage: $0 [options]
Options:
--no-cron Do not create or update cron jobs
--with-cron Create/update cron jobs (default)
--multitenant Use the multi-tenant architecture deployment
-h, --help Show this help message
EOF
}
@@ -41,6 +43,10 @@ parse_args() {
CRON_SETUP_VALUE="1"
shift
;;
--multitenant)
COMPOSE_FILE="docker-compose-multitenant.yml"
shift
;;
-h|--help)
usage
exit 0
@@ -450,7 +456,7 @@ stack_owns_host_port() {
local container_port="$2"
local mapped_port
mapped_port="$(docker compose --env-file "$ENV_FILE" port nginx "$container_port" 2>/dev/null | tail -n1 || true)"
mapped_port="$(docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" port nginx "$container_port" 2>/dev/null | tail -n1 || true)"
if [ -z "$mapped_port" ]; then
return 1
fi
@@ -493,20 +499,20 @@ configure_host_ports() {
requested_http="$(awk -F= '/^INVENTAR_HTTP_PORT=/{print $2}' "$ENV_FILE" | tr -d ' ' || true)"
fi
if [ -z "$requested_http" ]; then
requested_http="80"
requested_http="8001"
fi
if stack_owns_host_port "$requested_http" "80"; then
HTTP_PORT_VALUE="$requested_http"
elif port_in_use "$requested_http"; then
stop_host_nginx_services || true
if ! port_in_use "$requested_http"; then
HTTP_PORT_VALUE="$requested_http"
echo "Freed HTTP port $requested_http by stopping host nginx service"
else
HTTP_PORT_VALUE="$(find_free_port 8080)"
echo "HTTP port 80 is in use. Using fallback HTTP port: $HTTP_PORT_VALUE"
echo "HTTP port is in use. Using fallback HTTP port: $HTTP_PORT_VALUE"
fi
else
HTTP_PORT_VALUE="$requested_http"
@@ -518,13 +524,13 @@ configure_host_ports() {
fi
if [ -z "$requested_https" ]; then
requested_https="443"
requested_https="8443"
fi
if stack_owns_host_port "$requested_https" "443"; then
HTTPS_PORT_VALUE="$requested_https"
elif port_in_use "$requested_https"; then
stop_host_nginx_services || true
if ! port_in_use "$requested_https"; then
HTTPS_PORT_VALUE="$requested_https"
@@ -533,7 +539,7 @@ configure_host_ports() {
fi
HTTPS_PORT_VALUE="$(find_free_port 8443)"
echo "HTTPS port 443 is in use. Using fallback HTTPS port: $HTTPS_PORT_VALUE"
echo "HTTPS port is in use. Using fallback HTTPS port: $HTTPS_PORT_VALUE"
else
HTTPS_PORT_VALUE="$requested_https"
fi
@@ -550,7 +556,7 @@ EOF
verify_stack_health() {
local compose_args running_services retry_count=0
compose_args=(--env-file "$ENV_FILE")
compose_args=(-f "$COMPOSE_FILE" --env-file "$ENV_FILE")
# Try health check with optional restart on first failure
while [[ $retry_count -lt 2 ]]; do
@@ -605,7 +611,7 @@ ensure_app_image_loaded
write_env_file
echo "Starting Inventarsystem Docker stack (app + mongodb)..."
docker compose --env-file "$ENV_FILE" up -d --remove-orphans
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --remove-orphans
verify_stack_health
+15 -1
View File
@@ -4,12 +4,26 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
COMPOSE_FILE="docker-compose.yml"
while [[ $# -gt 0 ]]; do
case "$1" in
--multitenant)
COMPOSE_FILE="docker-compose-multitenant.yml"
shift
;;
*)
shift
;;
esac
done
if ! command -v docker >/dev/null 2>&1; then
echo "Error: docker command not found. Install Docker first."
exit 1
fi
echo "Stopping Inventarsystem Docker stack..."
docker compose down
docker compose -f "$COMPOSE_FILE" down
echo "Stack stopped."
+24 -1
View File
@@ -325,12 +325,35 @@ download_and_extract_bundle() {
cp -f "$tmp_dir/update.sh" "$PROJECT_DIR/update.sh"
fi
# Neu: Multi-Tenant Ressourcen kopieren, falls vorhanden
if [ -f "$tmp_dir/docker-compose-multitenant.yml" ]; then
cp -f "$tmp_dir/docker-compose-multitenant.yml" "$PROJECT_DIR/docker-compose-multitenant.yml"
fi
if [ -f "$tmp_dir/docker/nginx/multitenant.conf" ]; then
cp -f "$tmp_dir/docker/nginx/multitenant.conf" "$PROJECT_DIR/docker/nginx/multitenant.conf"
fi
if [ -f "$tmp_dir/manage-tenant.sh" ]; then
cp -f "$tmp_dir/manage-tenant.sh" "$PROJECT_DIR/manage-tenant.sh"
fi
if [ -f "$tmp_dir/run-tenant-cmd.sh" ]; then
cp -f "$tmp_dir/run-tenant-cmd.sh" "$PROJECT_DIR/run-tenant-cmd.sh"
fi
for file in "MULTITENANT_DEPLOYMENT.md" "MULTITENANT_PYTHON_API.md"; do
if [ -f "$tmp_dir/$file" ]; then
cp -f "$tmp_dir/$file" "$PROJECT_DIR/$file"
fi
done
# Ensure executable permissions on all copied scripts
chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh" "$PROJECT_DIR/init-admin.sh" "$PROJECT_DIR/manage-tenant.sh" "$PROJECT_DIR/run-tenant-cmd.sh" 2>/dev/null || true 2>/dev/null || true
chmod +x "$PROJECT_DIR"/manage-tenant.sh "$PROJECT_DIR"/run-tenant-cmd.sh 2>/dev/null || true
if [ ! -f "$PROJECT_DIR/config.json" ] && [ -f "$tmp_dir/config.json" ]; then
cp -f "$tmp_dir/config.json" "$PROJECT_DIR/config.json"
log_message "Installed default config.json from release bundle"
fi
chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh"
chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh" "$PROJECT_DIR/init-admin.sh" "$PROJECT_DIR/manage-tenant.sh" "$PROJECT_DIR/run-tenant-cmd.sh" 2>/dev/null || true
}
deploy() {