Compare commits

...

60 Commits

Author SHA1 Message Date
Aiirondev_dev f7c113b760 changes to the right page permission check 2026-07-25 14:10:43 +02:00
Aiirondev_dev f35f0d6908 changes to make the release process fluent 2026-07-24 20:46:05 +02:00
Aiirondev_dev a577c7bda7 start of the permision completion for test purposes 2026-07-24 20:25:52 +02:00
Aiirondev_dev b37e630cde prune release clearing of unused files 2026-07-24 20:06:57 +02:00
Aiirondev_dev aa0f7c68bd fix of a actions error 2026-07-24 19:34:23 +02:00
Aiirondev_dev 68596b6939 changes to fix the user authentification issues 2026-07-24 18:59:29 +02:00
Aiirondev_dev 8dbdcaff56 Addition of the decryption for the logs beeing shown 2026-07-23 18:57:32 +02:00
Aiirondev_dev 622e257145 change from invario.eu to invario-software.de 2026-07-19 21:31:36 +02:00
Aiirondev_dev 7e66dad7e7 cahnges to the decryption 2026-07-19 21:24:55 +02:00
Aiirondev_dev 69fb566e8a The release will change the valuable decryption 2026-07-19 21:17:02 +02:00
Aiirondev_dev 3e993f65e6 Fix of the endpoint name 2026-07-19 21:07:30 +02:00
Aiirondev_dev 3cffa4f601 slight changes 2026-07-19 21:00:56 +02:00
Aiirondev_dev adc484cc26 slight changes in hopes of debugging 2026-07-19 20:47:06 +02:00
Aiirondev_dev c99e61ac45 debugging fot the damaged view 2026-07-19 20:27:58 +02:00
Aiirondev_dev e1e488f723 changes to incorperate the Encryption frokm the borrower name correctly 2026-07-19 20:20:41 +02:00
Aiirondev_dev 0562aee04b changes to the decryption of the payloads from the audit event report download 2026-07-19 20:02:17 +02:00
Aiirondev_dev b1c104f36c changes to the processing of the decryption process 2026-07-19 19:53:44 +02:00
Aiirondev_dev 5afe05b2d2 changes to the Library Items Types wich caused some Issues with the right displayment 2026-07-18 12:20:20 +02:00
Aiirondev_dev 7207fef0d4 Chnages to the Items being shown to the Library User 2026-07-18 12:18:12 +02:00
Aiirondev_dev 3f9fae10af changes to the bakcup 2026-07-17 15:46:42 +02:00
Aiirondev_dev f45988d0e7 changes to the backlupo 2026-07-17 15:06:45 +02:00
Aiirondev_dev 32f39223f4 changes to the backup system 2026-07-17 14:36:59 +02:00
Aiirondev_dev 20528b60c5 changes 2026-07-16 14:53:27 +02:00
Aiirondev_dev c610c06a0e changes for the backup process 2026-07-16 14:40:06 +02:00
Aiirondev_dev 2cef1672d2 changes to the backup 2026-07-16 14:12:54 +02:00
Aiirondev_dev eb8542c410 changes to the backup.sh 2026-07-16 13:49:19 +02:00
Aiirondev_dev 1ebd83ec2c changes to the backup.sh 2026-07-16 13:41:39 +02:00
Aiirondev_dev e80bf946c0 changes to the ussage of backup.sh 2026-07-16 13:39:21 +02:00
Aiirondev_dev bfbbff597e final changes to account for the switch from github to gitea 2026-07-16 12:08:09 +02:00
Aiirondev_dev d80f3f56b7 changes to the release action 2026-07-16 11:16:54 +02:00
Aiirondev_dev 726fca0866 fix of wrong Registry keys
Release Inventarsystem / release-docker (push) Failing after 2m40s
2026-07-16 10:26:28 +02:00
Aiirondev_dev 614f73250a changes to the release 2026-07-16 10:10:56 +02:00
Aiirondev_dev 0d2488eb0b release changes 2026-07-16 08:23:10 +02:00
Aiirondev_dev 7e6a1d6b03 fiy of a typo 2026-07-16 08:15:15 +02:00
Aiirondev_dev ebf144f1a9 changes to the remote execution of the release 2026-07-16 08:09:15 +02:00
Aiirondev_dev 78fda8d569 Test deployment 2026-07-14 01:02:56 +02:00
Aiirondev_dev 248594ce0d adjustment for the runner 2026-07-14 00:59:38 +02:00
Aiirondev_dev 783c68a243 Try difrent adjustments 2026-07-14 00:53:19 +02:00
Aiirondev_dev 031d41643e adjustment for a problem with the ubuntu-latest 2026-07-14 00:51:31 +02:00
Aiirondev_dev cae3199e53 Adjustments to the release-docker.yml 2026-07-14 00:48:03 +02:00
Aiirondev_dev d0f2afae57 slight changes to the dispaying of the Detailed Library Modals 2026-07-07 22:25:55 +02:00
Aiirondev_dev 1a99448a2e slight css fix for the accesability of the script 2026-07-07 21:43:44 +02:00
Aiirondev_dev aec25a13b9 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-07-05 13:40:59 +02:00
Aiirondev_dev b31abe2177 Adjustments to the intput field for the ÖLibrary funktion 2026-07-05 13:40:48 +02:00
Aiirondev_dev 7fd27e8c02 additional changes to account for legacy Item Types 2026-07-05 12:02:57 +02:00
Aiirondev_dev 5f5784e7c3 addition of a debuging step 2026-07-05 11:52:57 +02:00
Aiirondev_dev 613c1e11e4 slight changes of important kexy filters for the retrival of Library Items 2026-07-05 11:46:30 +02:00
Aiirondev_dev 9e3b54c1b1 slight changes to the updating process to restart after such an event 2026-07-05 11:40:06 +02:00
Aiirondev_dev a8bce26eab Adjust for a normal scanner to be able t scann as well 2026-06-30 12:04:35 +02:00
Aiirondev_dev 36c391e99c fix in the export of excel lists 2026-06-30 11:32:17 +02:00
Aiirondev_dev 40c3970719 better excel export 2026-06-30 11:22:18 +02:00
Aiirondev_dev e7f4b1ce9d Slkight fix for the Items types 2026-06-30 10:43:09 +02:00
Aiirondev_dev d84466a3cc changes to the Library Item Type to reflekt the correct types 2026-06-30 10:04:59 +02:00
Aiirondev_dev 0efc01a835 slight adjustments 2026-06-28 23:47:58 +02:00
Aiirondev_dev 73cf63a56d changes 2026-06-28 23:34:42 +02:00
Aiirondev_dev fdcd26326b slight changes to correctly reflect all relevant things 2026-06-28 23:28:42 +02:00
Aiirondev_dev 1fd9e230ac changes of the upddating of library Items 2026-06-28 23:13:36 +02:00
Aiirondev_dev cc0c4e28b7 Development 2026-06-28 18:28:29 +02:00
Aiirondev_dev ea73b98549 Adjustments to the edeting funktion fpo 2026-06-28 17:03:10 +02:00
Aiirondev_dev d6dda0f25a Slight adjustments 2026-06-28 16:55:57 +02:00
17 changed files with 837 additions and 452 deletions
-11
View File
@@ -1,11 +0,0 @@
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=10000
INVENTAR_HTTP_PORTS=10000
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:v0.7.42
INVENTAR_APP_CPUS=1.0
INVENTAR_APP_MEM_LIMIT=384m
INVENTAR_APP_MEM_SWAP_LIMIT=768m
INVENTAR_WORKERS=4
INVENTAR_THREADS=2
INVENTAR_WORKER_TIMEOUT=30
INVENTAR_WORKER_CONNECTIONS=100
+75 -34
View File
@@ -1,4 +1,4 @@
name: https://github.com/AIIrondev/legendary-octo-garbanzo name: Release Inventarsystem
on: on:
push: push:
@@ -25,6 +25,7 @@ env:
jobs: jobs:
release-docker: release-docker:
# Hinweis: Falls dein lokaler Gitea-Runner ein anderes Label hat (z.B. 'linux' oder 'self-hosted'), passe dies hier an.
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -34,18 +35,20 @@ jobs:
- name: Set metadata - name: Set metadata
id: meta id: meta
env: env:
# Gitea stellt das GITHUB_TOKEN für Kompatibilität mit GitHub Actions automatisch zur Verfügung
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }} REPO: ${{ gitea.repository }}
EVENT_NAME: ${{ github.event_name }} EVENT_NAME: ${{ gitea.event_name }}
REF_NAME: ${{ github.ref_name }} REF_NAME: ${{ gitea.ref_name }}
BUMP_TYPE: ${{ github.event.inputs.bump || 'patch' }} BUMP_TYPE: ${{ gitea.event.inputs.bump || 'patch' }}
DOCKER_API_VERSION: '1.44'
run: | run: |
if [ "$EVENT_NAME" = "push" ] && [ -n "$REF_NAME" ]; then if [ "$EVENT_NAME" = "push" ] && [ -n "$REF_NAME" ]; then
TAG="$REF_NAME" TAG="$REF_NAME"
else else
# Fetch latest release tag via GitHub API (fall back to v0.8.31) # Gitea API Endpunkt nutzen, um das aktuellste Release abzufragen
latest_tag="v0.8.31" latest_tag="v0.8.31"
if meta_json=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null); then if meta_json=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/json" "https://git.invario-software.eu/api/v1/repos/$REPO/releases/latest" 2>/dev/null); then
tag_name=$(printf "%s" "$meta_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) tag_name=$(printf "%s" "$meta_json" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
if [ -n "$tag_name" ]; then if [ -n "$tag_name" ]; then
latest_tag="$tag_name" latest_tag="$tag_name"
@@ -108,19 +111,70 @@ jobs:
TAG="${base}.${i}" TAG="${base}.${i}"
fi fi
IMAGE="ghcr.io/aiirondev/legendary-octo-garbanzo:${TAG}" # Docker Images verlangen Kleinbuchstaben. Repository-Namen daher umwandeln.
LOWER_REPO=$(echo "$REPO" | tr '[:upper:]' '[:lower:]')
IMAGE="git.invario-software.eu/${LOWER_REPO}:${TAG}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT" echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
echo "lower_repo=$LOWER_REPO" >> "$GITHUB_OUTPUT"
- name: Ensure Docker CLI is available and up to date
run: |
install_docker=true
# Prüfen, ob Docker existiert und ob die Version ausreicht
if command -v docker >/dev/null 2>&1; then
# Extrahiere die Major-Version
DOCKER_MAJOR=$(docker --version | grep -oE '[0-9]+' | head -n1)
# API 1.44 erfordert mindestens Docker v25
if [ -n "$DOCKER_MAJOR" ] && [ "$DOCKER_MAJOR" -ge 25 ]; then
install_docker=false
fi
fi
if [ "$install_docker" = true ]; then
echo "Veraltete oder fehlende Docker-Installation erkannt. Lade statische Docker CLI herunter..."
DOCKER_VERSION="26.1.4"
# Download der statischen Binaries via curl oder wget (umgeht apt-get komplett)
if command -v curl >/dev/null 2>&1; then
curl -fsSLO "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz"
else
wget -q "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz"
fi
tar -xzf docker-${DOCKER_VERSION}.tgz
# Installation in lokalen Benutzer-Pfad, um sudo/root-Rechte-Probleme zu vermeiden
mkdir -p "$HOME/.local/bin"
cp docker/docker "$HOME/.local/bin/"
# Pfad für nachfolgende GitHub Actions Schritte verfügbar machen
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# Pfad für diesen spezifischen Shell-Run exportieren
export PATH="$HOME/.local/bin:$PATH"
rm -rf docker docker-${DOCKER_VERSION}.tgz
echo "Docker CLI wurde erfolgreich aktualisiert."
else
echo "Docker CLI ist bereits auf einem aktuellen Stand."
fi
docker --version
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to GHCR - name: Login to Gitea Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: git.invario-software.eu
username: ${{ github.actor }} username: ${{ gitea.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push release image - name: Build and push release image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -128,10 +182,9 @@ jobs:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
load: true # Lädt das Image in den lokalen Docker-Daemon für den 'docker save' Schritt
tags: | tags: |
${{ steps.meta.outputs.image }} ${{ steps.meta.outputs.image }}
ghcr.io/aiirondev/legendary-octo-garbanzo:latest git.invario-software.eu/${{ steps.meta.outputs.lower_repo }}:latest
- name: Build local image tar for offline deploy - name: Build local image tar for offline deploy
run: | run: |
@@ -139,20 +192,10 @@ jobs:
IMG="${{ steps.meta.outputs.image }}" IMG="${{ steps.meta.outputs.image }}"
TAG="${{ steps.meta.outputs.tag }}" TAG="${{ steps.meta.outputs.tag }}"
if docker image inspect "$IMG" >/dev/null 2>&1; then echo "Pulling freshly pushed image from registry: $IMG"
echo "Using local image $IMG" docker pull "$IMG"
docker save "$IMG" | gzip > "inventarsystem-image-${TAG}.tar.gz"
exit 0
fi
echo "Local image $IMG not found, trying to pull" echo "Saving image to offline tar archive..."
if docker pull "$IMG" >/dev/null 2>&1; then
docker save "$IMG" | gzip > "inventarsystem-image-${TAG}.tar.gz"
exit 0
fi
echo "Pull failed, attempting local docker build as fallback"
docker build -t "$IMG" .
docker save "$IMG" | gzip > "inventarsystem-image-${TAG}.tar.gz" docker save "$IMG" | gzip > "inventarsystem-image-${TAG}.tar.gz"
- name: Create release-only docker bundle - name: Create release-only docker bundle
@@ -161,7 +204,7 @@ jobs:
cat > release-bundle/docker-compose.yml <<EOF cat > release-bundle/docker-compose.yml <<EOF
services: services:
app: app:
image: \${INVENTAR_APP_IMAGE:-ghcr.io/aiirondev/legendary-octo-garbanzo:${{ steps.meta.outputs.tag }}} image: \${INVENTAR_APP_IMAGE:-${{ steps.meta.outputs.image }}}
container_name: inventarsystem-app container_name: inventarsystem-app
restart: unless-stopped restart: unless-stopped
ports: ports:
@@ -225,7 +268,7 @@ jobs:
EOF EOF
# Copy runtime scripts and config if present # Copy runtime scripts and config if present
for f in start.sh stop.sh restart.sh backup.sh config.json update.sh; do for f in start.sh stop.sh restart.sh backup.sh restore.sh config.json update.sh; do
if [ -f "$f" ]; then if [ -f "$f" ]; then
cp "$f" "release-bundle/$(basename "$f")" cp "$f" "release-bundle/$(basename "$f")"
fi fi
@@ -242,12 +285,10 @@ jobs:
find release-bundle -maxdepth 1 -type f -name '*.sh' -exec chmod +x {} \; || true find release-bundle -maxdepth 1 -type f -name '*.sh' -exec chmod +x {} \; || true
tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle . tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle .
- name: Create or update GitHub Release - name: Create or update Gitea Release
uses: softprops/action-gh-release@v2 uses: https://gitea.com/actions/gitea-release-action@v1
with: with:
tag_name: ${{ steps.meta.outputs.tag }} tag_name: ${{ steps.meta.outputs.tag }}
files: | files: |
inventarsystem-docker-bundle.tar.gz inventarsystem-docker-bundle.tar.gz
inventarsystem-image-${{ steps.meta.outputs.tag }}.tar.gz inventarsystem-image-${{ steps.meta.outputs.tag }}.tar.gz
fail_on_unmatched_files: false
generate_release_notes: true
+3 -3
View File
@@ -120,19 +120,19 @@ Das Inventarsystem stellt folgende Wartungsskripte bereit:
**Option 1** **Option 1**
```bash ```bash
wget -O - https://raw.githubusercontent.com/AIIrondev/legendary-octo-garbanzo/main/install.sh | sudo bash wget -O - https://git.invario-software.eu/Invario/Inventarsystem/raw/branch/main/install.sh | sudo bash
``` ```
**Option 2** **Option 2**
```bash ```bash
curl -s https://raw.githubusercontent.com/AIIrondev/legendary-octo-garbanzo/main/install.sh | sudo bash curl -s https://git.invario-software.eu/Invario/Inventarsystem/raw/branch/main/install.sh | sudo bash
``` ```
Legacy-MongoDB uebernehmen und altes Host-System aufraeumen: Legacy-MongoDB uebernehmen und altes Host-System aufraeumen:
```bash ```bash
curl -s https://raw.githubusercontent.com/AIIrondev/legendary-octo-garbanzo/main/install.sh | \ curl -s https://git.invario-software.eu/Invario/Inventarsystem/raw/branch/main/install.sh | \
sudo bash -s -- --migrate-legacy-db --remove-legacy-system sudo bash -s -- --migrate-legacy-db --remove-legacy-system
``` ```
+192 -98
View File
@@ -42,6 +42,7 @@ import Web.modules.database.termine as termin
import Web.modules.log.audit_log as al import Web.modules.log.audit_log as al
import push_notifications as pn import push_notifications as pn
import Web.modules.inventarsystem.pdf_export as pdf_export import Web.modules.inventarsystem.pdf_export as pdf_export
import Web.modules.inventarsystem.excel_export as excel_export
import datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from bson.objectid import ObjectId from bson.objectid import ObjectId
@@ -288,7 +289,7 @@ SCHEDULER_INTERVAL = cfg.SCHEDULER_INTERVAL_MIN
SSL_CERT = cfg.SSL_CERT SSL_CERT = cfg.SSL_CERT
SSL_KEY = cfg.SSL_KEY SSL_KEY = cfg.SSL_KEY
LIBRARY_ITEM_TYPES = ['book', 'cd', 'dvd', 'media', 'schulbuch'] LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'schoolbook', 'Buch', 'Schulbuch', 'schulbuch')
INVOICE_CURRENCY = 'EUR' INVOICE_CURRENCY = 'EUR'
NOTIFICATION_STATUS_CACHE_TTL = max(3, int(os.getenv('INVENTAR_NOTIFICATION_STATUS_CACHE_TTL', '8'))) NOTIFICATION_STATUS_CACHE_TTL = max(3, int(os.getenv('INVENTAR_NOTIFICATION_STATUS_CACHE_TTL', '8')))
@@ -1257,7 +1258,9 @@ def inject_version():
try: try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB] db = client[MONGODB_DB]
unread_notification_count = _get_unread_notification_count(db, session['username'], is_admin=is_admin) # Pass the computed permission instead of the strict is_admin boolean
can_manage = current_permissions.get('actions', {}).get('can_manage_settings', False)
unread_notification_count = _get_unread_notification_count(db, session['username'], is_admin=(is_admin or can_manage))
except Exception: except Exception:
unread_notification_count = 0 unread_notification_count = 0
finally: finally:
@@ -2476,6 +2479,7 @@ def _upload_excel_items(scope='inventory'):
parent_item_id=parent_item_id, parent_item_id=parent_item_id,
isbn=row['isbn'], isbn=row['isbn'],
item_type=row['item_type'], item_type=row['item_type'],
is_library=False
) )
if not item_id: if not item_id:
import_errors.append((row['row_number'], 'Fehler beim Anlegen des Artikels')) import_errors.append((row['row_number'], 'Fehler beim Anlegen des Artikels'))
@@ -2979,33 +2983,50 @@ def library_export_excel(scope='all'):
username = session['username'] username = session['username']
is_admin_user = us.check_admin(username) is_admin_user = us.check_admin(username)
import Web.modules.inventarsystem.excel_export as excel_export # Base query
client = MongoClient(MONGODB_HOST, MONGODB_PORT) query = {
db = client[cfg.MONGODB_DB] 'ItemType': {'$in': LIBRARY_ITEM_TYPES},
items_collection = db['items'] 'Deleted': {'$ne': True}
}
query = {'ItemType': {'$in': LIBRARY_ITEM_TYPES}, 'Deleted': {'$ne': True}}
# Scope modifications
if scope == 'borrowed_by_me': if scope == 'borrowed_by_me':
query['User'] = username query['User'] = username
query['Verfuegbar'] = False query['Verfuegbar'] = False
filename = f"Bibliothek_Ausgeliehen_{username}.xlsx" filename = f"Bibliothek_Ausgeliehen_{username}.xlsx"
elif scope == 'all_borrowed': elif scope == 'all_borrowed':
if not is_admin_user: if not is_admin_user:
# Consider adding a flash message here before redirecting
return redirect('/library') return redirect('/library')
query['Verfuegbar'] = False query['Verfuegbar'] = False
filename = "Bibliothek_Alle_Ausleihen.xlsx" filename = "Bibliothek_Alle_Ausleihen.xlsx"
elif scope == 'schulbuecher': elif scope == 'schulbuecher':
# Overwrites the base $in query, which is fine
query['ItemType'] = 'schulbuch' query['ItemType'] = 'schulbuch'
filename = "Schulbuecher_Bestand.xlsx" filename = "Schulbuecher_Bestand.xlsx"
elif scope == 'all_books':
# FIX: Keep the library item types restriction, but exclude 'general'
query['ItemType'] = {'$in': [t for t in LIBRARY_ITEM_TYPES if t != 'general']}
filename = "Bibliothek_Buecher.xlsx"
else: else:
# 'all' # 'all' scope
filename = "Bibliothek_Gesamtbestand.xlsx" filename = "Bibliothek_Gesamtbestand.xlsx"
# Database interaction
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[cfg.MONGODB_DB]
items_collection = db['items']
items = list(items_collection.find(query)) items = list(items_collection.find(query))
client.close() client.close()
# Generate and send file
excel_file = excel_export.generate_library_excel(items) excel_file = excel_export.generate_library_excel(items)
return send_file( return send_file(
excel_file, excel_file,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
@@ -3047,15 +3068,22 @@ def library_loans_admin():
if 'username' not in session: if 'username' not in session:
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error') flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
if not us.check_admin(session['username']):
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['pages'].get('library_loans_admin', False):
flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error') flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error')
return redirect(url_for('login')) return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('library'): if not cfg.MODULES.is_enabled('library'):
flash('Bibliotheks-Modul ist deaktiviert.', 'error') flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home'))
_ensure_audit_indexes_once() _ensure_audit_indexes_once()
# IMPORT HINZUGEFÜGT: Entschlüsselungs-Tool importieren
from modules.inventarsystem.data_protection import decrypt_text
def fmt_dt(dt): def fmt_dt(dt):
try: try:
return dt.strftime('%d.%m.%Y %H:%M') if dt else '' return dt.strftime('%d.%m.%Y %H:%M') if dt else ''
@@ -3102,6 +3130,9 @@ def library_loans_admin():
item_has_damage = bool(item_doc.get('HasDamage')) or condition_value == 'destroyed' or bool(item_doc.get('DamageReports')) item_has_damage = bool(item_doc.get('HasDamage')) or condition_value == 'destroyed' or bool(item_doc.get('DamageReports'))
damage_reports = item_doc.get('DamageReports', []) or [] damage_reports = item_doc.get('DamageReports', []) or []
raw_user = record.get('User', '')
decrypted_user = decrypt_text(raw_user) if raw_user else ''
loan_entries.append({ loan_entries.append({
'id': str(record.get('_id')), 'id': str(record.get('_id')),
'item_id': item_id, 'item_id': item_id,
@@ -3109,7 +3140,7 @@ def library_loans_admin():
'item_code': item_doc.get('Code_4', ''), 'item_code': item_doc.get('Code_4', ''),
'item_author': item_doc.get('Author', ''), 'item_author': item_doc.get('Author', ''),
'item_isbn': item_doc.get('ISBN', ''), 'item_isbn': item_doc.get('ISBN', ''),
'user': record.get('User', ''), 'user': decrypted_user,
'status': record.get('Status', ''), 'status': record.get('Status', ''),
'start': fmt_dt(record.get('Start')), 'start': fmt_dt(record.get('Start')),
'end': fmt_dt(record.get('End')), 'end': fmt_dt(record.get('End')),
@@ -3162,7 +3193,6 @@ def library_loans_admin():
if client: if client:
client.close() client.close()
@app.route('/api/library_items') @app.route('/api/library_items')
def api_library_items(): def api_library_items():
""" """
@@ -3193,7 +3223,7 @@ def api_library_items():
ausleihungen_db = db['ausleihungen'] ausleihungen_db = db['ausleihungen']
query = { query = {
'ItemType': {'$in': ['book', 'cd', 'dvd', 'media']}, 'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']},
'IsGroupedSubItem': {'$ne': True}, 'IsGroupedSubItem': {'$ne': True},
'Deleted': {'$ne': True} 'Deleted': {'$ne': True}
} }
@@ -3319,7 +3349,7 @@ def api_library_items():
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB] db = client[cfg.MONGODB_DB]
student_cards = db['student_cards'] student_cards = db['student_cards']
card = student_cards.find_one({'_id': ObjectId(borrow_id)}) card = student_cards.find_one({'_id': ObjectId(borrower)}) if borrower else None
card = _decrypt_student_card_doc(card) card = _decrypt_student_card_doc(card)
client.close() client.close()
borrower = card['SchülerName'] borrower = card['SchülerName']
@@ -5115,6 +5145,8 @@ def upload_item():
if 'username' not in session: if 'username' not in session:
return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401 return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401
item_is_library = False
# Check if user may insert items # Check if user may insert items
username = session['username'] username = session['username']
permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user') permissions = _get_current_user_permissions() or us.build_default_permission_payload('standard_user')
@@ -5159,6 +5191,9 @@ def upload_item():
item_type_input = sanitize_form_value(request.form.get('item_type_input', '')) item_type_input = sanitize_form_value(request.form.get('item_type_input', ''))
library_category = sanitize_form_value(request.form.get('library_category', '')) library_category = sanitize_form_value(request.form.get('library_category', ''))
if library_category != '':
item_is_library = True
try: try:
item_count = int(item_count_raw) if item_count_raw else 1 item_count = int(item_count_raw) if item_count_raw else 1
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -5227,7 +5262,7 @@ def upload_item():
return redirect(url_for(success_redirect_endpoint)) return redirect(url_for(success_redirect_endpoint))
item_isbn = '' item_isbn = ''
item_type = 'general' item_type = 'other'
if cfg.MODULES.is_enabled('library') and isbn_raw: if cfg.MODULES.is_enabled('library') and isbn_raw:
normalized_isbn = normalize_and_validate_isbn(isbn_raw) normalized_isbn = normalize_and_validate_isbn(isbn_raw)
if normalized_isbn: if normalized_isbn:
@@ -5235,8 +5270,7 @@ def upload_item():
item_type = 'book' item_type = 'book'
else: else:
item_isbn = isbn_raw item_isbn = isbn_raw
if upload_mode == 'library':
item_type = 'book'
if item_type_input != "": if item_type_input != "":
item_type = item_type_input item_type = item_type_input
@@ -5253,7 +5287,6 @@ def upload_item():
return jsonify({'success': False, 'message': error_msg}), 400 return jsonify({'success': False, 'message': error_msg}), 400
flash(error_msg, 'error') flash(error_msg, 'error')
return redirect(url_for('library_admin')) return redirect(url_for('library_admin'))
item_type = 'book'
# Only check for images if not duplicating and no duplicate images provided and no book cover # Only check for images if not duplicating and no duplicate images provided and no book cover
# For library mode, skip this check as images come only from ISBN fetch # For library mode, skip this check as images come only from ISBN fetch
@@ -6047,7 +6080,8 @@ def upload_item():
parent_item_id=parent_item_id, parent_item_id=parent_item_id,
isbn=item_isbn, isbn=item_isbn,
item_type=item_type, item_type=item_type,
library_category=library_category library_category=library_category,
is_library=item_is_library
) )
if not item_id: if not item_id:
break break
@@ -6627,6 +6661,63 @@ def edit_item(id):
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@app.route('/update_group', methods=['POST'])
def update_group():
data = request.get_json()
series_group_id = data.get('series_group_id')
if not series_group_id:
return jsonify({'success': False, 'message': 'Keine Gruppen-ID'}), 400
# 1. Shared Fields (Group Logic)
# These apply to every item in the group
shared_update = {
'Name': data.get('name'),
'Ort': data.get('ort'),
'Beschreibung': data.get('beschreibung'),
'Anschaffungsjahr': data.get('ansch_jahr'),
'Anschaffungskosten': data.get('ansch_kost'),
'Reservierbar': data.get('reservierbar'),
'ISBN': data.get('isbn'),
'ItemType': data.get('item_type'),
'LastUpdated': datetime.datetime.now()
}
# 2. Individual Updates (Specific Code Logic)
# Expected format: [{'id': '...', 'code_4': '...'}, ...]
individual_items = data.get('items', [])
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
items_col = db['items']
# A. Apply Shared Attributes to the whole group
items_col.update_many(
{'SeriesGroupId': series_group_id},
{'$set': shared_update}
)
# B. Apply Unique Codes to specific items
# We iterate through the provided list to update the specific code for each ID
for item in individual_items:
item_id = item.get('id')
new_code = item.get('code_4')
if item_id:
items_col.update_one(
{'_id': ObjectId(item_id), 'SeriesGroupId': series_group_id},
{'$set': {'Code_4': new_code}}
)
client.close()
return jsonify({'success': True, 'message': 'Gruppe und individuelle Codes aktualisiert'})
except Exception as e:
app.logger.error(f"Error updating group {series_group_id}: {e}")
return jsonify({'success': False, 'message': 'Systemfehler beim Update'}), 500
@app.route('/report_damage/<id>', methods=['POST']) @app.route('/report_damage/<id>', methods=['POST'])
def report_damage(id): def report_damage(id):
"""Register a damage report entry for an item (admin only).""" """Register a damage report entry for an item (admin only)."""
@@ -7803,7 +7894,7 @@ def register():
page_permissions = {} page_permissions = {}
for endpoint_name, _ in PERMISSION_PAGE_OPTIONS: for endpoint_name, _ in PERMISSION_PAGE_OPTIONS:
page_permissions[endpoint_name] = request.form.get(f'page_{endpoint_name}') == 'on' page_permissions[endpoint_name] = request.form.get(f'page_{endpoint_name}') == 'on'
us.add_user( us.add_user(
username, username,
password, password,
@@ -7968,6 +8059,8 @@ def admin_borrowings():
_ensure_audit_indexes_once() _ensure_audit_indexes_once()
from modules.inventarsystem.data_protection import decrypt_text
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB] db = client[MONGODB_DB]
ausleihungen = db['ausleihungen'] ausleihungen = db['ausleihungen']
@@ -8001,6 +8094,10 @@ def admin_borrowings():
item_name = None item_name = None
item_cost = None item_cost = None
has_damage = False has_damage = False
raw_user = r.get('User', '')
decrypted_user = decrypt_text(raw_user) if raw_user else ''
entries.append({ entries.append({
'id': str(r.get('_id')), 'id': str(r.get('_id')),
'item_id': str(item_doc.get('_id')) if item_doc and item_doc.get('_id') else str(it_id or ''), 'item_id': str(item_doc.get('_id')) if item_doc and item_doc.get('_id') else str(it_id or ''),
@@ -8008,7 +8105,7 @@ def admin_borrowings():
'item_name': str(item_name or ''), 'item_name': str(item_name or ''),
'item_cost': fmt_money(item_cost), 'item_cost': fmt_money(item_cost),
'item_cost_raw': item_cost if item_cost is not None else '', 'item_cost_raw': item_cost if item_cost is not None else '',
'user': r.get('User', ''), 'user': decrypted_user,
'status': r.get('Status', ''), 'status': r.get('Status', ''),
'start': fmt_dt(r.get('Start')), 'start': fmt_dt(r.get('Start')),
'end': fmt_dt(r.get('End')), 'end': fmt_dt(r.get('End')),
@@ -8074,10 +8171,26 @@ def admin_audit_dashboard():
# DEC_START: Decrypt the sensitive fields for display # DEC_START: Decrypt the sensitive fields for display
for row in audit_rows: for row in audit_rows:
if "payload" in row: payload_raw = row.get("payload")
# decrypt_document_fields acts in-place
decrypt_document_fields(row["payload"], SENSITIVE_AUDIT_FIELDS) if payload_raw and isinstance(payload_raw, str):
# DEC_END try:
# Safely evaluate the python-like dict string, providing context for ObjectId
safe_context = {"ObjectId": ObjectId, "None": None, "True": True, "False": False}
payload_dict = eval(payload_raw, {"__builtins__": {}}, safe_context)
if isinstance(payload_dict, dict):
# Decrypt the sensitive keys inside the extracted dictionary
decrypt_document_fields(payload_dict, SENSITIVE_AUDIT_FIELDS)
# Replace the raw string with the clean dictionary for the Jinja template
row["payload"] = payload_dict
except Exception as parse_err:
app.logger.error(f"Failed to parse audit payload string for index {row.get('chain_index')}: {parse_err}")
elif payload_raw and isinstance(payload_raw, dict):
# Fallback in case some newer log rows are already stored as proper dictionaries
decrypt_document_fields(payload_raw, SENSITIVE_AUDIT_FIELDS)
return render_template( return render_template(
'admin_audit.html', 'admin_audit.html',
@@ -8124,11 +8237,26 @@ def admin_audit_export_pdf_official():
audit_rows = list(db['audit_log'].find({}).sort('chain_index', -1).limit(limit)) audit_rows = list(db['audit_log'].find({}).sort('chain_index', -1).limit(limit))
# DEC_START: Decrypt sensitive fields for the PDF report
for row in audit_rows: for row in audit_rows:
if "payload" in row: payload_raw = row.get("payload")
decrypt_document_fields(row["payload"], SENSITIVE_AUDIT_FIELDS)
# DEC_END if payload_raw and isinstance(payload_raw, str):
try:
# Safely evaluate the python-like dict string with custom token support
safe_context = {"ObjectId": ObjectId, "None": None, "True": True, "False": False}
payload_dict = eval(payload_raw, {"__builtins__": {}}, safe_context)
if isinstance(payload_dict, dict):
# Decrypt the dictionary fields in-place
decrypt_document_fields(payload_dict, SENSITIVE_AUDIT_FIELDS)
# Replace string with the clean dictionary so the PDF generator can read it
row["payload"] = payload_dict
except Exception as parse_err:
app.logger.error(f"Failed to parse audit payload string for PDF export at index {row.get('chain_index')}: {parse_err}")
elif payload_raw and isinstance(payload_raw, dict):
# Fallback for standard dict payloads
decrypt_document_fields(payload_raw, SENSITIVE_AUDIT_FIELDS)
# Get school information from settings or use defaults # Get school information from settings or use defaults
school_info = _get_school_info_for_export() school_info = _get_school_info_for_export()
@@ -9159,6 +9287,10 @@ def logs():
logs_collection = db['system_logs'] logs_collection = db['system_logs']
extra_logs = list(logs_collection.find({'type': {'$in': ['damage_report', 'damage_repair']}})) extra_logs = list(logs_collection.find({'type': {'$in': ['damage_report', 'damage_repair']}}))
from modules.inventarsystem.data_protection import decrypt_text
from bson.objectid import ObjectId
for log_item in extra_logs: for log_item in extra_logs:
log_type = log_item.get('type', '') log_type = log_item.get('type', '')
item_id = log_item.get('item_id') item_id = log_item.get('item_id')
@@ -9178,7 +9310,7 @@ def logs():
note = log_item.get('note', '') note = log_item.get('note', '')
formatted_items.append({ formatted_items.append({
'Item': item_name, 'Item': item_name,
'User': log_item.get('user', 'Unknown User'), 'User': decrypt_text(log_item.get('user', 'Unknown User')),
'Start': ts_display, 'Start': ts_display,
'End': '-', 'End': '-',
'Duration': '-', 'Duration': '-',
@@ -9192,7 +9324,7 @@ def logs():
resolved_count = log_item.get('resolved_count', 0) resolved_count = log_item.get('resolved_count', 0)
formatted_items.append({ formatted_items.append({
'Item': item_name, 'Item': item_name,
'User': log_item.get('user', 'Unknown User'), 'User': decrypt_text(log_item.get('user', 'Unknown User')),
'Start': ts_display, 'Start': ts_display,
'End': '-', 'End': '-',
'Duration': '-', 'Duration': '-',
@@ -10286,91 +10418,53 @@ def notifications_unread_status():
client.close() client.close()
@app.route('/admin/damaged_items') @app.route('/admin/damaged_items')
def admin_damaged_items(): def admin_damaged_items():
"""Dedicated admin management window for damaged items.""" """Admin-Übersicht aller aktiven und vergangenen Ausleihen."""
if 'username' not in session: if 'username' not in session:
flash('Administratorrechte erforderlich.', 'error') flash('Administratorrechte erforderlich.', 'error')
return redirect(url_for('login')) return redirect(url_for('login'))
''' from modules.inventarsystem.data_protection import decrypt_text
permissions = _get_current_user_permissions() from bson.objectid import ObjectId
if not _action_access_allowed(permissions, 'can_manage_settings'):
flash('Sie haben keine Berechtigung, die Schulstammdaten zu ändern.', 'error')
if cfg.MODULES.is_enabled('library'):
return redirect(url_for('library_admin'))
'''
client = None client = None
try: try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT) client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB] db = client[MONGODB_DB]
items_col = db['items']
ausleihungen_col = db['ausleihungen'] ausleihungen_col = db['ausleihungen']
items_col = db['items']
items = list(items_col.find( ausleihungen = list(ausleihungen_col.find().sort('Start', -1))
{
'Deleted': {'$ne': True},
'$or': [
{'HasDamage': True},
{'Condition': 'destroyed'},
{'DamageReports.0': {'$exists': True}},
]
},
{
'Name': 1,
'Code_4': 1,
'ItemType': 1,
'Author': 1,
'ISBN': 1,
'Condition': 1,
'DamageReports': 1,
'DamageRepairs': 1,
'Verfuegbar': 1,
'User': 1,
'LastUpdated': 1,
}
).sort('LastUpdated', -1))
damaged_rows = [] for record in ausleihungen:
for item_doc in items: raw_user = record.get('User', '')
item_id = str(item_doc.get('_id')) if raw_user:
active_borrow = ausleihungen_col.find_one( record['User'] = decrypt_text(raw_user)
{'Item': item_id, 'Status': {'$in': ['active', 'planned']}},
{'_id': 1, 'User': 1, 'Status': 1, 'End': 1}
)
reports = item_doc.get('DamageReports', []) or []
latest_report = reports[0] if reports else {}
damaged_rows.append({ item_id = record.get('Item')
'id': item_id, if item_id:
'name': item_doc.get('Name', ''), try:
'code': item_doc.get('Code_4', ''), item_doc = items_col.find_one({'_id': ObjectId(item_id)})
'item_type': item_doc.get('ItemType', ''), if item_doc:
'author': item_doc.get('Author', ''), if item_doc.get('User'):
'isbn': item_doc.get('ISBN', ''), item_doc['User'] = decrypt_text(item_doc['User'])
'condition': item_doc.get('Condition', ''),
'available': bool(item_doc.get('Verfuegbar', False)), record['ItemDetails'] = item_doc
'borrow_user': item_doc.get('User', ''), except Exception as e:
'damage_count': len(reports), app.logger.warning(f"Konnte Item {item_id} für Ausleihe {record.get('_id')} nicht laden: {e}")
'damage_reports': reports,
'latest_damage_description': latest_report.get('description', ''),
'latest_damage_by': latest_report.get('reported_by', ''),
'latest_damage_at': latest_report.get('reported_at'),
'active_borrow': active_borrow,
'last_updated': item_doc.get('LastUpdated'),
})
return render_template( return render_template(
'admin_damaged_items.html', 'admin_damaged_items.html',
damaged_items=damaged_rows, ausleihungen=ausleihungen,
library_module_enabled=cfg.MODULES.is_enabled('library'), library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'), student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail') mail_module_enabled=cfg.MODULES.is_enabled('mail')
) )
except Exception as exc: except Exception as exc:
app.logger.error(f"Error loading damaged-items admin view: {exc}") app.logger.error(f"Fehler beim Laden der Ausleihen-Verwaltung: {exc}")
flash('Fehler beim Laden der Defekte-Items-Verwaltung.', 'error') flash('Fehler beim Laden der Ausleihen-Übersicht.', 'error')
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
finally: finally:
if client: if client:
+3 -2
View File
@@ -24,7 +24,7 @@ import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient from Web.modules.database.settings import MongoClient
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'media', 'schulbuch') LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'other', 'schoolbook', 'Buch', 'Schulbuch', 'schulbuch')
def _non_library_query(extra_query=None): def _non_library_query(extra_query=None):
@@ -49,7 +49,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
ansch_jahr=None, ansch_kost=None, code_4=None, reservierbar=True, ansch_jahr=None, ansch_kost=None, code_4=None, reservierbar=True,
series_group_id=None, series_count=1, series_position=1, series_group_id=None, series_count=1, series_position=1,
is_grouped_sub_item=False, parent_item_id=None, is_grouped_sub_item=False, parent_item_id=None,
isbn=None, item_type='general', library_category=None): isbn=None, item_type='general', library_category=None, is_library=False):
""" """
Add a new item to the inventory. Add a new item to the inventory.
@@ -102,6 +102,7 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
'ISBN': isbn, 'ISBN': isbn,
'library_category': library_category, 'library_category': library_category,
'ItemType': item_type, 'ItemType': item_type,
'is_library': is_library,
'SeriesGroupId': series_group_id, 'SeriesGroupId': series_group_id,
'SeriesCount': series_count, 'SeriesCount': series_count,
'SeriesPosition': series_position, 'SeriesPosition': series_position,
+8 -2
View File
@@ -323,7 +323,8 @@ def get_effective_permissions(username):
if bool(user.get('Admin', False)): if bool(user.get('Admin', False)):
return build_default_permission_payload('full_access') return build_default_permission_payload('full_access')
preset_key = user.get('PermissionPreset') or 'standard_user' preset_key = user.get('PermissionPreset')
print(preset_key)
payload = build_default_permission_payload(preset_key) payload = build_default_permission_payload(preset_key)
payload['actions'] = _normalize_bool_map(user.get('ActionPermissions', {}), payload['actions']) payload['actions'] = _normalize_bool_map(user.get('ActionPermissions', {}), payload['actions'])
payload['pages'] = _normalize_bool_map(user.get('PagePermissions', {}), payload['pages']) payload['pages'] = _normalize_bool_map(user.get('PagePermissions', {}), payload['pages'])
@@ -552,10 +553,15 @@ def add_user(
for key, value in page_permissions.items(): for key, value in page_permissions.items():
permission_defaults['pages'][str(key)] = bool(value) permission_defaults['pages'][str(key)] = bool(value)
if permission_preset == "full_access":
can_admin_preset_based = True
else:
can_admin_preset_based = False
user_doc = { user_doc = {
'Username': username, 'Username': username,
'Password': hashing(password), 'Password': hashing(password),
'Admin': False, 'Admin': can_admin_preset_based,
'active_ausleihung': None, 'active_ausleihung': None,
'name': name.strip() if name else '', 'name': name.strip() if name else '',
'last_name': last_name.strip() if last_name else '', 'last_name': last_name.strip() if last_name else '',
+15 -12
View File
@@ -37,20 +37,23 @@ def generate_library_excel(items):
item.get("ReturnDate", ""), item.get("ReturnDate", ""),
item.get("Anschaffungskosten", "") item.get("Anschaffungskosten", "")
] ]
ws.append(row)
for col in ws.columns: # FIX: Clean the row data INSIDE the loop for every single item
max_length = 0 clean_row = []
column = col[0].column_letter for value in row:
for cell in col: if isinstance(value, list):
try: # Converts ['A', 'B'] to "A, B" and an empty list [] to an empty string ""
if len(str(cell.value)) > max_length: clean_row.append(", ".join(map(str, value)) if value else "")
max_length = len(str(cell.value)) elif isinstance(value, dict):
except: # Just in case there is an embedded MongoDB sub-document
pass clean_row.append(str(value))
ws.column_dimensions[column].width = min(max_length + 2, 50) else:
clean_row.append(value)
# Append the safe, flattened row to the worksheet
ws.append(clean_row)
excel_buffer = io.BytesIO() excel_buffer = io.BytesIO()
wb.save(excel_buffer) wb.save(excel_buffer)
excel_buffer.seek(0) excel_buffer.seek(0)
return excel_buffer return excel_buffer
+173 -70
View File
@@ -1130,7 +1130,7 @@
<li class="nav-item" data-nav-fixed="true"> <li class="nav-item" data-nav-fixed="true">
<a class="nav-link {% if current_path == url_for('terminplan') %}nav-active{% endif %}" href="{{ url_for('terminplan') }}" data-tutorial-tip="Hier sehen Sie den Kalender mit den bestehenden Terminen.">Kalender</a> <a class="nav-link {% if current_path == url_for('terminplan') %}nav-active{% endif %}" href="{{ url_for('terminplan') }}" data-tutorial-tip="Hier sehen Sie den Kalender mit den bestehenden Terminen.">Kalender</a>
</li> </li>
{% if current_permissions.pages.get('terminplan', True) and current_permissions.actions.get('can_insert', True) %} {% if current_permissions.pages.get('terminplan', False) and current_permissions.actions.get('can_insert', False) %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('terminplaner.configure') %}nav-active{% endif %}" href="{{ url_for('terminplaner.configure') }}" data-tutorial-tip="Neuen Terminplan erstellen und an Ihr Team versenden.">Neue Planung</a> <a class="nav-link quick-link-pill {% if current_path == url_for('terminplaner.configure') %}nav-active{% endif %}" href="{{ url_for('terminplaner.configure') }}" data-tutorial-tip="Neuen Terminplan erstellen und an Ihr Team versenden.">Neue Planung</a>
</li> </li>
@@ -1145,20 +1145,22 @@
<a class="nav-link dropdown-toggle" href="#" id="termMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">Mehr Optionen</a> <a class="nav-link dropdown-toggle" href="#" id="termMoreDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false" title="Weitere Optionen">Mehr Optionen</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="termMoreDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="termMoreDropdown">
{% if 'username' in session %} {% if 'username' in session %}
{% if current_permissions.pages.get('tutorial_page', True) %} {% if current_permissions.pages.get('tutorial_page', False) %}
<li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li> <li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('admin_audit_dashboard', True) %} {% endif %}
{% if current_permissions.actions.get('can_view_logs', False) or current_permissions.pages.get('admin_audit_dashboard', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('home', True) %} {% if current_permissions.pages.get('home', False) %}
<li><a class="dropdown-item" href="{{ url_for('home') }}">Inventarsystem</a></li> <li><a class="dropdown-item" href="{{ url_for('home') }}">Inventarsystem</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('library_view', True) and library_module_enabled %} {% if current_permissions.pages.get('library_view', False) and library_module_enabled %}
<li><a class="dropdown-item" href="{{ url_for('library_view') }}">Bibliothek</a></li> <li><a class="dropdown-item" href="{{ url_for('library_view') }}">Bibliothek</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@@ -1191,7 +1193,7 @@
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span> <span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown">
{% if current_permissions.pages.get('notifications_view', True) %} {% if current_permissions.pages.get('notifications_view', False) %}
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li> <li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@@ -1214,13 +1216,13 @@
</button> </button>
<div class="collapse navbar-collapse" id="inventoryNavContent"> <div class="collapse navbar-collapse" id="inventoryNavContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList"> <ul class="navbar-nav me-auto mb-2 mb-lg-0" id="inventoryNavList">
{% if current_permissions.pages.get('home', True) %} {% if current_permissions.pages.get('home', False) %}
<li class="nav-item" data-nav-fixed="true"> <li class="nav-item" data-nav-fixed="true">
<a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}" data-tutorial-tip="Starten Sie hier mit dem Materialbestand und suchen Sie nach Artikeln.">Artikel</a> <a class="nav-link {% if current_path == url_for('home') %}nav-active{% endif %}" href="{{ url_for('home') }}" data-tutorial-tip="Starten Sie hier mit dem Materialbestand und suchen Sie nach Artikeln.">Artikel</a>
</li> </li>
{% endif %} {% endif %}
{% if 'username' in session %} {% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', True) %} {% if current_permissions.pages.get('my_borrowed_items', False) %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Ihre aktuellen Ausleihen und Rückgaben finden Sie hier.">Meine Ausleihen</a> <a class="nav-link quick-link-pill {% if current_path == url_for('my_borrowed_items') %}nav-active{% endif %}" href="{{ url_for('my_borrowed_items') }}" data-tutorial-tip="Ihre aktuellen Ausleihen und Rückgaben finden Sie hier.">Meine Ausleihen</a>
</li> </li>
@@ -1239,42 +1241,44 @@
</a> </a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invMoreDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invMoreDropdown">
{% if 'username' in session %} {% if 'username' in session %}
{% if current_permissions.pages.get('tutorial_page', True) %} {% if current_permissions.pages.get('tutorial_page', False) %}
<li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li> <li><a class="dropdown-item" href="{{ url_for('tutorial_page') }}">Tutorial</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('upload_admin', True) and current_permissions.actions.get('can_insert', True) %} {% if current_permissions.pages.get('upload_admin', False) and current_permissions.actions.get('can_insert', False) %}
<li><a class="dropdown-item" href="{{ url_for('upload_admin') }}"> Hochladen</a></li> <li><a class="dropdown-item" href="{{ url_for('upload_admin') }}"> Hochladen</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% endif %} {% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %} {% if 'username' in session and current_permissions.actions.get('can_manage_settings', False) %}
<li><h6 class="dropdown-header">Verwaltung</h6></li> <li><h6 class="dropdown-header">Verwaltung</h6></li>
{% if current_permissions.pages.get('manage_filters', True) %} {% if current_permissions.pages.get('manage_filters', False) %}
<li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li> <li><a class="dropdown-item" href="{{ url_for('manage_filters') }}">Filter verwalten</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('manage_locations', True) %} {% if current_permissions.pages.get('manage_locations', False) %}
<li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li> <li><a class="dropdown-item" href="{{ url_for('manage_locations') }}">Orte verwalten</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% if current_permissions.pages.get('admin_borrowings', True) %} {% endif %}
{% if current_permissions.pages.get('admin_borrowings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_damaged_items', True) %} {% if current_permissions.pages.get('admin_damaged_items', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
{% endif %} {% endif %}
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('admin_audit_dashboard', True) %} {% if current_permissions.actions.get('can_view_logs', False) and current_permissions.pages.get('admin_audit_dashboard', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
{% endif %} {% endif %}
{% if current_permissions.actions.get('can_view_logs', True) and current_permissions.pages.get('logs', True) %} {% if current_permissions.actions.get('can_view_logs', False) and current_permissions.pages.get('logs', False) %}
<li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li> <li><a class="dropdown-item" href="{{ url_for('logs') }}">Logs</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% if current_permissions.actions.get('can_manage_users', True) %} {% if current_permissions.actions.get('can_manage_users', False) %}
<li><h6 class="dropdown-header">System</h6></li> <li><h6 class="dropdown-header">System</h6></li>
{% if current_permissions.pages.get('user_del', True) %} {% if current_permissions.pages.get('user_del', False) %}
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li> <li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('register', True) %} {% if current_permissions.pages.get('register', False) %}
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li> <li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@@ -1311,7 +1315,7 @@
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span> <span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invUserMenuDropdown">
{% if current_permissions.pages.get('notifications_view', True) %} {% if current_permissions.pages.get('notifications_view', False) %}
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li> <li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@@ -1336,19 +1340,19 @@
</button> </button>
<div class="collapse navbar-collapse" id="libraryNavContent"> <div class="collapse navbar-collapse" id="libraryNavContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList"> <ul class="navbar-nav me-auto mb-2 mb-lg-0" id="libraryNavList">
{% if current_permissions.pages.get('library_view', True) %} {% if current_permissions.pages.get('library_view', False) %}
<li class="nav-item" data-nav-fixed="true"> <li class="nav-item" data-nav-fixed="true">
<a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Medien</a> <a class="nav-link {% if current_path == url_for('library_view') %}nav-active{% endif %}" href="{{ url_for('library_view') }}" data-tutorial-tip="Die Bibliothek zeigt Ihnen alle Medien und verfügbaren Bücher.">Medien</a>
</li> </li>
{% endif %} {% endif %}
{% if 'username' in session %} {% if 'username' in session %}
{% if current_permissions.pages.get('tutorial_page', True) %} {% if current_permissions.pages.get('tutorial_page', False) %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Nutzen Sie das Tutorial, um die Bibliotheksfunktionen kennenzulernen.">Tutorial</a> <a class="nav-link quick-link-pill {% if current_path == url_for('tutorial_page') %}nav-active{% endif %}" href="{{ url_for('tutorial_page') }}" data-tutorial-tip="Nutzen Sie das Tutorial, um die Bibliotheksfunktionen kennenzulernen.">Tutorial</a>
</li> </li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if 'username' in session and current_permissions.actions.get('can_insert', True) and current_permissions.pages.get('library_admin', True) %} {% if 'username' in session and current_permissions.actions.get('can_insert', False) and current_permissions.pages.get('library_admin', False) %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}" data-tutorial-tip="Neue Bibliotheksmedien können hier aufgenommen werden.">📖 Hochladen</a> <a class="nav-link nav-priority-link {% if current_path == url_for('library_admin') %}nav-active{% endif %}" href="{{ url_for('library_admin') }}" data-tutorial-tip="Neue Bibliotheksmedien können hier aufgenommen werden.">📖 Hochladen</a>
</li> </li>
@@ -1365,22 +1369,24 @@
Mehr Optionen Mehr Optionen
</a> </a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libMoreDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libMoreDropdown">
{% if 'username' in session and (session.get('admin', False) or is_admin) and current_permissions.actions.get('can_manage_settings', True) %} {% if 'username' in session and current_permissions.actions.get('can_manage_settings', False) %}
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li> <li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
{% if current_permissions.pages.get('library_loans_admin', True) %} {% if current_permissions.pages.get('library_loans_admin', False) %}
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen / Defekte Items</a></li> <li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Ausleihen / Defekte Items</a></li>
{% endif %} {% endif %}
{% if student_cards_module_enabled %} {% if student_cards_module_enabled %}
<li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li> <li><a class="dropdown-item" href="{{ url_for('student_cards_admin') }}">Bibliotheksausweis</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li> <li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
{% if current_permissions.actions.get('can_manage_users', True) %} {% if current_permissions.actions.get('can_manage_users', False) %}
<li><h6 class="dropdown-header">System</h6></li> <li><h6 class="dropdown-header">System</h6></li>
{% if current_permissions.pages.get('user_del', True) %} {% if current_permissions.pages.get('user_del', False) %}
<li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li> <li><a class="dropdown-item" href="{{ url_for('user_del') }}">Benutzer verwalten</a></li>
{% endif %} {% endif %}
{% if current_permissions.pages.get('register', True) %} {% if current_permissions.pages.get('register', False) %}
<li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li> <li><a class="dropdown-item" href="{{ url_for('register') }}">Neuer Benutzer</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@@ -1414,7 +1420,7 @@
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span> <span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libUserMenuDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="libUserMenuDropdown">
{% if current_permissions.pages.get('notifications_view', True) %} {% if current_permissions.pages.get('notifications_view', False) %}
<li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li> <li><a class="dropdown-item" href="{{ url_for('notifications_view') }}">Benachrichtigungen</a></li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider"></li> <li><hr class="dropdown-divider"></li>
@@ -1642,30 +1648,79 @@
</div> </div>
<datalist id="function-search-options"> <datalist id="function-search-options">
{% if current_permissions.pages.get('home', False) %}
<option value="Artikel"></option> <option value="Artikel"></option>
<option value="Meine Ausleihen"></option> {% endif %}
<option value="Benachrichtigungen"></option>
<option value="Tutorial"></option> {% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', False) %}
<option value="Meine Ausleihen"></option>
{% endif %}
{% if current_permissions.pages.get('notifications_view', False) %}
<option value="Benachrichtigungen"></option>
{% endif %}
{% if current_permissions.pages.get('tutorial_page', False) %}
<option value="Tutorial"></option>
{% endif %}
{% endif %}
<option value="Impressum"></option> <option value="Impressum"></option>
<option value="Lizenz"></option> <option value="Lizenz"></option>
{% if library_module_enabled %} {% if library_module_enabled %}
<option value="Bibliothek"></option> {% if current_permissions.pages.get('library_view', False) %}
<option value="Meine Medien"></option> <option value="Bibliothek"></option>
{% endif %} {% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) %} {% if 'username' in session and current_permissions.pages.get('my_borrowed_items', False) %}
<option value="Hochladen"></option> <option value="Meine Medien"></option>
<option value="Ausleihen Verwaltung"></option> {% endif %}
<option value="Defekte Items"></option>
<option value="Filter verwalten"></option>
<option value="Orte verwalten"></option>
<option value="Schulstammdaten"></option>
<option value="Audit Dashboard"></option>
<option value="Logs"></option>
<option value="Benutzer verwalten"></option>
<option value="Neuer Benutzer"></option>
{% if student_cards_module_enabled %}
<option value="Bibliotheksausweis"></option>
{% endif %} {% endif %}
{% if 'username' in session %}
{% if current_permissions.pages.get('upload_admin', False) and current_permissions.actions.get('can_insert', False) %}
<option value="Hochladen"></option>
{% endif %}
{% if current_permissions.pages.get('admin_borrowings', False) or current_permissions.pages.get('library_loans_admin', False) %}
<option value="Ausleihen Verwaltung"></option>
{% endif %}
{% if current_permissions.pages.get('admin_damaged_items', False) or current_permissions.pages.get('library_loans_admin', False) %}
<option value="Defekte Items"></option>
{% endif %}
{% if current_permissions.pages.get('manage_filters', False) %}
<option value="Filter verwalten"></option>
{% endif %}
{% if current_permissions.pages.get('manage_locations', False) %}
<option value="Orte verwalten"></option>
{% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
<option value="Schulstammdaten"></option>
{% endif %}
{% if current_permissions.actions.get('can_view_logs', False) and current_permissions.pages.get('admin_audit_dashboard', False) %}
<option value="Audit Dashboard"></option>
{% endif %}
{% if current_permissions.actions.get('can_view_logs', False) and current_permissions.pages.get('logs', False) %}
<option value="Logs"></option>
{% endif %}
{% if current_permissions.actions.get('can_manage_users', False) %}
{% if current_permissions.pages.get('user_del', False) %}
<option value="Benutzer verwalten"></option>
{% endif %}
{% if current_permissions.pages.get('register', False) %}
<option value="Neuer Benutzer"></option>
{% endif %}
{% endif %}
{% if student_cards_module_enabled and current_permissions.pages.get('student_cards_admin', False) %}
<option value="Bibliotheksausweis"></option>
{% endif %}
{% endif %} {% endif %}
</datalist> </datalist>
@@ -1713,32 +1768,80 @@
const loginHintKey = username ? ('inventarsystem_notification_login_hint_v1_' + username) : null; const loginHintKey = username ? ('inventarsystem_notification_login_hint_v1_' + username) : null;
const functionSearchEntries = [ const functionSearchEntries = [
{% if current_permissions.pages.get('home', False) %}
{ label: 'Artikel', keywords: ['artikel', 'inventar', 'home'], url: {{ url_for('home')|tojson }} }, { label: 'Artikel', keywords: ['artikel', 'inventar', 'home'], url: {{ url_for('home')|tojson }} },
{ label: 'Meine Ausleihen', keywords: ['meine ausleihen', 'ausleihen', 'borrowed'], url: {{ url_for('my_borrowed_items')|tojson }} }, {% endif %}
{ label: 'Benachrichtigungen', keywords: ['benachrichtigungen', 'nachrichten', 'notifications'], url: {{ url_for('notifications_view')|tojson }} },
{ label: 'Tutorial', keywords: ['tutorial', 'hilfe', 'anleitung'], url: {{ url_for('tutorial_page')|tojson }} }, {% if 'username' in session %}
{% if current_permissions.pages.get('my_borrowed_items', False) %}
{ label: 'Meine Ausleihen', keywords: ['meine ausleihen', 'ausleihen', 'borrowed'], url: {{ url_for('my_borrowed_items')|tojson }} },
{% endif %}
{% if current_permissions.pages.get('notifications_view', False) %}
{ label: 'Benachrichtigungen', keywords: ['benachrichtigungen', 'nachrichten', 'notifications'], url: {{ url_for('notifications_view')|tojson }} },
{% endif %}
{% if current_permissions.pages.get('tutorial_page', False) %}
{ label: 'Tutorial', keywords: ['tutorial', 'hilfe', 'anleitung'], url: {{ url_for('tutorial_page')|tojson }} },
{% endif %}
{% endif %}
{ label: 'Impressum', keywords: ['impressum'], url: {{ url_for('impressum')|tojson }} }, { label: 'Impressum', keywords: ['impressum'], url: {{ url_for('impressum')|tojson }} },
{ label: 'Lizenz', keywords: ['lizenz', 'license'], url: {{ url_for('license')|tojson }} }, { label: 'Lizenz', keywords: ['lizenz', 'license'], url: {{ url_for('license')|tojson }} },
{% if library_module_enabled %} {% if library_module_enabled %}
{ label: 'Bibliothek', keywords: ['bibliothek', 'medien'], url: {{ url_for('library_view')|tojson }} }, {% if current_permissions.pages.get('library_view', False) %}
{% endif %} { label: 'Bibliothek', keywords: ['bibliothek', 'medien'], url: {{ url_for('library_view')|tojson }} },
{% if 'username' in session and (session.get('admin', False) or is_admin) %} {% endif %}
{ label: 'Hochladen', keywords: ['hochladen', 'upload'], url: {{ url_for('upload_admin')|tojson }} },
{ label: 'Ausleihen Verwaltung', keywords: ['ausleihen verwaltung', 'admin borrowings'], url: {{ url_for('admin_borrowings')|tojson }} },
{ label: 'Defekte Items', keywords: ['defekte items', 'defekt', 'schaden'], url: {{ url_for('admin_damaged_items')|tojson }} },
{ label: 'Filter verwalten', keywords: ['filter verwalten', 'filter'], url: {{ url_for('manage_filters')|tojson }} },
{ label: 'Orte verwalten', keywords: ['orte verwalten', 'orte', 'location'], url: {{ url_for('manage_locations')|tojson }} },
{ label: 'Schulstammdaten', keywords: ['schule', 'settings', 'stammdaten', 'school settings'], url: {{ url_for('admin_school_settings')|tojson }} },
{ label: 'Audit Dashboard', keywords: ['audit', 'audit dashboard'], url: {{ url_for('admin_audit_dashboard')|tojson }} },
{ label: 'Logs', keywords: ['logs', 'protokoll'], url: {{ url_for('logs')|tojson }} },
{ label: 'Benutzer verwalten', keywords: ['benutzer verwalten', 'user'], url: {{ url_for('user_del')|tojson }} },
{ label: 'Neuer Benutzer', keywords: ['neuer benutzer', 'register'], url: {{ url_for('register')|tojson }} },
{% if library_module_enabled %}
{ label: 'Bibliotheks-Ausleihen', keywords: ['bibliotheks ausleihen', 'library loans'], url: {{ url_for('library_loans_admin')|tojson }} },
{% endif %}
{% if student_cards_module_enabled %}
{ label: 'Bibliotheksausweis', keywords: ['bibliotheksausweis', 'student card'], url: {{ url_for('student_cards_admin')|tojson }} },
{% endif %} {% endif %}
{% if 'username' in session %}
{% if current_permissions.pages.get('upload_admin', False) and current_permissions.actions.get('can_insert', False) %}
{ label: 'Hochladen', keywords: ['hochladen', 'upload'], url: {{ url_for('upload_admin')|tojson }} },
{% endif %}
{% if current_permissions.pages.get('admin_borrowings', False) %}
{ label: 'Ausleihen Verwaltung', keywords: ['ausleihen verwaltung', 'admin borrowings'], url: {{ url_for('admin_borrowings')|tojson }} },
{% endif %}
{% if current_permissions.pages.get('admin_damaged_items', False) %}
{ label: 'Defekte Items', keywords: ['defekte items', 'defekt', 'schaden'], url: {{ url_for('admin_damaged_items')|tojson }} },
{% endif %}
{% if current_permissions.pages.get('manage_filters', False) %}
{ label: 'Filter verwalten', keywords: ['filter verwalten', 'filter'], url: {{ url_for('manage_filters')|tojson }} },
{% endif %}
{% if current_permissions.pages.get('manage_locations', False) %}
{ label: 'Orte verwalten', keywords: ['orte verwalten', 'orte', 'location'], url: {{ url_for('manage_locations')|tojson }} },
{% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
{ label: 'Schulstammdaten', keywords: ['schule', 'settings', 'stammdaten', 'school settings'], url: {{ url_for('admin_school_settings')|tojson }} },
{% endif %}
{% if current_permissions.actions.get('can_view_logs', False) and current_permissions.pages.get('admin_audit_dashboard', False) %}
{ label: 'Audit Dashboard', keywords: ['audit', 'audit dashboard'], url: {{ url_for('admin_audit_dashboard')|tojson }} },
{% endif %}
{% if current_permissions.actions.get('can_view_logs', False) and current_permissions.pages.get('logs', False) %}
{ label: 'Logs', keywords: ['logs', 'protokoll'], url: {{ url_for('logs')|tojson }} },
{% endif %}
{% if current_permissions.actions.get('can_manage_users', False) %}
{% if current_permissions.pages.get('user_del', False) %}
{ label: 'Benutzer verwalten', keywords: ['benutzer verwalten', 'user'], url: {{ url_for('user_del')|tojson }} },
{% endif %}
{% if current_permissions.pages.get('register', False) %}
{ label: 'Neuer Benutzer', keywords: ['neuer benutzer', 'register'], url: {{ url_for('register')|tojson }} },
{% endif %}
{% endif %}
{% if library_module_enabled and current_permissions.pages.get('library_loans_admin', False) %}
{ label: 'Bibliotheks-Ausleihen', keywords: ['bibliotheks ausleihen', 'library loans'], url: {{ url_for('library_loans_admin')|tojson }} },
{% endif %}
{% if student_cards_module_enabled and current_permissions.pages.get('student_cards_admin', False) %}
{ label: 'Bibliotheksausweis', keywords: ['bibliotheksausweis', 'student card'], url: {{ url_for('student_cards_admin')|tojson }} },
{% endif %}
{% endif %} {% endif %}
]; ];
+1 -1
View File
@@ -22,7 +22,7 @@
<p><strong>Name:</strong> Invario UG</p> <p><strong>Name:</strong> Invario UG</p>
<p><strong>Adresse:</strong> Am Sportplatz 10<br>83052 Bruckmühl<br>Deutschland</p> <p><strong>Adresse:</strong> Am Sportplatz 10<br>83052 Bruckmühl<br>Deutschland</p>
</address> </address>
<p><strong>E-Mail:</strong> <a href="mailto:info@invario.eu">info@invario.eu</a></p> <p><strong>E-Mail:</strong> <a href="mailto:info@invario-software.de">info@invario-software.de</a></p>
</div> </div>
<div class="impressum-section mb-4"> <div class="impressum-section mb-4">
+219 -86
View File
@@ -4,6 +4,57 @@
{% block content %} {% block content %}
<style> <style>
/* The Dark Background Overlay */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.6);
/* Display is managed by JS (none/flex) */
align-items: center;
justify-content: center;
z-index: 1000;
}
/* The White Modal Box */
.modal-content {
background-color: #fff;
padding: 20px;
width: 80%;
max-width: 600px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
/* Flexbox allows the header to stay fixed while the body scrolls */
display: flex;
flex-direction: column;
max-height: 85vh;
}
/* The Close Button */
.close {
align-self: flex-end;
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 1;
margin-bottom: 10px;
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
}
/* The Scrollable Content Area */
#detailContent {
overflow-y: auto; /* Adds scrollbar only if needed */
padding-right: 10px; /* Prevents text from rubbing against the scrollbar */
}
/* Library table-only view styles */ /* Library table-only view styles */
.library-table-container { .library-table-container {
max-width: 1400px; max-width: 1400px;
@@ -418,7 +469,7 @@
} }
</style> </style>
<div class="library-table-container" id="libraryTableContainer" data-can-edit="{{ 1 if is_admin else 0 }}"> <div class="library-table-container" id="libraryTableContainer" data-can-edit="{{ 1 if current_permissions.actions.get('can_edit', False) else 0 }}">
<!-- Header --> <!-- Header -->
<div class="library-header"> <div class="library-header">
<h1>📚 Bibliothek</h1> <h1>📚 Bibliothek</h1>
@@ -475,7 +526,7 @@
<select id="filterType"> <select id="filterType">
<option value="">-- Alle Typen --</option> <option value="">-- Alle Typen --</option>
<option value="book">Buch</option> <option value="book">Buch</option>
<option value="schulbuch">Schulbuch</option> <option value="schoolbook">Schulbuch</option>
<option value="cd">CD</option> <option value="cd">CD</option>
<option value="dvd">DVD</option> <option value="dvd">DVD</option>
<option value="other">Sonstige</option> <option value="other">Sonstige</option>
@@ -503,6 +554,10 @@
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="8" y1="13" x2="16" y2="13"></line><line x1="8" y1="17" x2="16" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="8" y1="13" x2="16" y2="13"></line><line x1="8" y1="17" x2="16" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
Alle ausgeliehen Excel Alle ausgeliehen Excel
</button> </button>
<button class="button" style="background: #10b981; color: white; display: flex; align-items: center; gap: 0.5rem;" onclick="window.location.href='/library/export/all_books'">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="8" y1="13" x2="16" y2="13"></line><line x1="8" y1="17" x2="16" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
Alle Bücher Excel
</button>
</div> </div>
</div> </div>
@@ -536,15 +591,14 @@
</div> </div>
</div> </div>
<!-- Detail Modal --> <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
<div id="detailModal" class="modal" style="display: none;"> <div id="detailModal" class="modal" style="display: none;">
<div class="modal-content"> <div class="modal-content">
<span class="close" onclick="closeDetailModal()">&times;</span> <span class="close" onclick="closeDetailModal()">&times;</span>
<div id="detailContent"></div> <div id="detailContent"></div>
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script> <script src="https://cdn.jsdelivr.net/npm/@ericblade/quagga2/dist/quagga.js"></script>
<script> <script>
// ========================================================================= // =========================================================================
@@ -1033,7 +1087,7 @@
} }
function getItemTypeLabel(type) { function getItemTypeLabel(type) {
const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige' }; const labels = { 'book': 'Buch', 'cd': 'CD', 'dvd': 'DVD', 'other': 'Sonstige', 'schoolbook': 'Schulbuch' };
return labels[type] || type; return labels[type] || type;
} }
@@ -1043,20 +1097,47 @@
return div.innerHTML; return div.innerHTML;
} }
// Opens the modal and fetches the data
function showItemDetail(itemId) { function showItemDetail(itemId) {
const detailContent = document.getElementById('detailContent');
const detailModal = document.getElementById('detailModal');
// 1. Show the loading state immediately
detailContent.innerHTML = '<p>Loading details...</p>';
detailModal.style.display = 'flex';
// 2. Fetch the data
fetch(`/api/item_detail/${itemId}`) fetch(`/api/item_detail/${itemId}`)
.then(r => r.text()) .then(response => {
.then(html => { if (!response.ok) {
document.getElementById('detailContent').innerHTML = html; throw new Error(`HTTP error! status: ${response.status}`);
document.getElementById('detailModal').style.display = 'flex'; }
return response.text();
}) })
.catch(err => console.error('Error loading detail:', err)); .then(html => {
// 3. Clean the HTML and display it
detailContent.innerHTML = DOMPurify.sanitize(html);
})
.catch(err => {
console.error('Error loading detail:', err);
detailContent.innerHTML = '<p>Sorry, we could not load the item details. Please try again later.</p>';
});
} }
// Closes the modal via the 'x' button
function closeDetailModal() { function closeDetailModal() {
document.getElementById('detailModal').style.display = 'none'; document.getElementById('detailModal').style.display = 'none';
} }
// Closes the modal if the user clicks the dark background overlay
window.onclick = function(event) {
const modal = document.getElementById('detailModal');
// If the click happened directly on the dark overlay (not the white box)
if (event.target === modal) {
closeDetailModal();
}
};
// ========================================================================= // =========================================================================
// 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER // 5. EVENT LISTENERS INITIALIZATION & ON-LOAD INITIALIZER
// ========================================================================= // =========================================================================
@@ -1192,142 +1273,194 @@
} }
}); });
function openEditLibraryItem(itemId) { window.openEditLibraryItem = function(itemId) {
const item = libraryItems.find(i => i._id === itemId); const item = libraryItems.find(i => i._id === itemId);
if (!item) return;
if (!item) {
alert('Fehler: Das Medium konnte nicht gefunden werden.');
return;
}
// Felder befüllen // 1. Felder befüllen
document.getElementById('editLibraryItemId').value = item._id || ''; document.getElementById('editLibraryItemId').value = item._id;
document.getElementById('editLibraryName').value = item.Name || ''; document.getElementById('editLibraryName').value = item.Name;
document.getElementById('editLibraryType').value = item.ItemType || 'book'; document.getElementById('editLibraryType').value = item.ItemType;
document.getElementById('editLibraryIsbn').value = item.ISBN || ''; document.getElementById('editLibraryIsbn').value = item.ISBN || '';
document.getElementById('editLibraryCode4').value = item.Code_4 || item.Code4 || ''; document.getElementById('editLibraryCode4').value = item.Code_4 || '';
document.getElementById('editLibraryLocation').value = item.Ort || ''; document.getElementById('editLibraryLocation').value = item.Ort;
document.getElementById('editLibraryDescription').value = item.Beschreibung || ''; document.getElementById('editLibraryDescription').value = item.Beschreibung;
// Gruppen-Info-Box Logik // 2. Gruppen-Logik
const warningDiv = document.getElementById('editLibraryGroupWarning'); const warningDiv = document.getElementById('editLibraryGroupWarning');
const codesContainer = document.getElementById('editLibraryAllCodes'); const codesContainer = document.getElementById('editLibraryAllCodes');
if (item.SeriesGroupId) { if (item.SeriesGroupId) {
const groupMembers = libraryItems // Filtern aus dem aktuell geladenen Array
.filter(i => i.SeriesGroupId === item.SeriesGroupId) let groupMembers = libraryItems.filter(i => i.SeriesGroupId === item.SeriesGroupId);
.sort((a, b) => a.SeriesPosition - b.SeriesPosition);
document.getElementById('editLibraryGroupCount').textContent = groupMembers.length; // SCHLÜSSEL: Wenn die Anzahl der gefundenen Elemente nicht mit SeriesCount übereinstimmt,
// haben wir die Gruppe noch nicht vollständig geladen.
if (groupMembers.length < (item.SeriesCount || 0)) {
console.warn("Gruppe noch nicht vollständig geladen. Anzeige ggf. unvollständig.");
// Optional: Zeige einen Ladehinweis im Modal
codesContainer.textContent = "Lade restliche Gruppenmitglieder...";
} else {
// Daten sind vollständig -> Anzeigen
const codeList = groupMembers
.sort((a, b) => (a.SeriesPosition || 0) - (b.SeriesPosition || 0))
.map(m => m.Code_4 || "---")
.join(', ');
codesContainer.innerHTML = groupMembers.map(member => { codesContainer.textContent = codeList;
const isCurrent = member._id === itemId; }
return `<span style="background: ${isCurrent ? '#0ea5e9' : '#e0e0e0'}; color: ${isCurrent ? '#fff' : '#333'}; padding: 2px 8px; border-radius: 4px; margin-right: 5px; font-size: 12px;">${member.Code_4 || 'n/a'}</span>`;
}).join('');
document.getElementById('editLibraryGroupCount').textContent = groupMembers.length + " / " + (item.SeriesCount || "?");
warningDiv.style.display = 'block'; warningDiv.style.display = 'block';
} else { } else {
warningDiv.style.display = 'none'; warningDiv.style.display = 'none';
} }
document.getElementById('editLibraryModal').style.display = 'flex'; document.getElementById('editLibraryModal').style.display = 'flex';
} };
function closeEditLibraryModal() { function closeEditLibraryModal() {
document.getElementById('editLibraryModal').style.display = 'none'; document.getElementById('editLibraryModal').style.display = 'none';
} }
// --- Sicherer Event-Listener für das Formular --- /**
* Event-Listener für das Formular (Initialisierung)
*/
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const editForm = document.getElementById('editLibraryForm'); const editForm = document.getElementById('editLibraryForm');
if (editForm) { if (editForm) {
editForm.addEventListener('submit', async function(e) { editForm.addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();
const itemId = document.getElementById('editLibraryItemId').value; const itemId = document.getElementById('editLibraryItemId').value;
const updateData = { const currentItem = libraryItems.find(i => i._id === itemId);
id: itemId,
if (!currentItem) return;
// 1. Alle Mitglieder der Gruppe finden, um die Code-Liste aufzubauen
const groupMembers = libraryItems.filter(i => i.SeriesGroupId === currentItem.SeriesGroupId);
const individualUpdates = groupMembers.map(member => ({
id: member._id,
// Wenn dies das bearbeitete Item ist, nimm den neuen Code, sonst den alten
code_4: (member._id === itemId) ? document.getElementById('editLibraryCode4').value : member.Code_4
}));
// 2. Payload für das Backend bauen
const payload = {
series_group_id: currentItem.SeriesGroupId,
name: document.getElementById('editLibraryName').value, name: document.getElementById('editLibraryName').value,
item_type: document.getElementById('editLibraryType').value,
isbn: document.getElementById('editLibraryIsbn').value,
code_4: document.getElementById('editLibraryCode4').value,
ort: document.getElementById('editLibraryLocation').value, ort: document.getElementById('editLibraryLocation').value,
beschreibung: document.getElementById('editLibraryDescription').value beschreibung: document.getElementById('editLibraryDescription').value,
isbn: document.getElementById('editLibraryIsbn').value,
item_type: document.getElementById('editLibraryType').value,
items: individualUpdates
}; };
// 3. Request an die Gruppen-Update Route
try { try {
const response = await fetch('/api/update-item', { const response = await fetch('/update_group', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData) body: JSON.stringify(payload)
}); });
if (response.ok) { const result = await response.json();
const editedItem = libraryItems.find(i => i._id === itemId); if (result.success) {
alert('Gruppe erfolgreich synchronisiert!');
// Lokale Daten im Array synchronisieren
if (editedItem && editedItem.SeriesGroupId) {
libraryItems.forEach(item => {
if (item.SeriesGroupId === editedItem.SeriesGroupId) {
item.Name = updateData.name;
item.ItemType = updateData.item_type;
item.ISBN = updateData.isbn;
item.Ort = updateData.ort;
item.Beschreibung = updateData.beschreibung;
if (item._id === itemId) item.Code_4 = updateData.code_4;
}
});
} else if (editedItem) {
Object.assign(editedItem, updateData);
}
closeEditLibraryModal(); closeEditLibraryModal();
// Optional: renderLibraryTable(); await loadLibraryItems(); // Daten neu laden
// renderTable(); // Ggf. Tabelle neu rendern
} else { } else {
const err = await response.json(); alert('Fehler: ' + result.message);
alert('Fehler beim Speichern: ' + (err.error || 'Unbekannter Fehler'));
} }
} catch (error) { } catch (error) {
console.error('Fetch error:', error); console.error('Update failed:', error);
alert('Netzwerkfehler.'); alert('Netzwerkfehler.');
} }
}); });
} }
}); });
</script> </script>
<div id="editLibraryModal" class="modal" style="display:none;"> <div id="editLibraryModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 760px;"> <div class="modal-content" style="max-width: 760px; padding: 25px; border-radius: 8px;">
<span class="close" onclick="closeEditLibraryModal()">&times;</span> <span class="close" onclick="closeEditLibraryModal()" style="cursor: pointer; float: right; font-size: 24px;">&times;</span>
<h3 style="margin-top:0;">Bibliotheksmedium bearbeiten</h3> <h3 style="margin-top:0;">Bibliotheksmedium bearbeiten</h3>
<!-- Bereich für Gruppen-Informationen (Range) --> <!-- Bereich für Gruppen-Informationen (Hier konsolidiert!) -->
<div id="editLibraryGroupWarning" style="display:none; background-color: #f8f9fa; padding: 15px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #dee2e6;"> <div id="editLibraryGroupWarning" style="display:none; background-color: #fff; padding: 15px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #0ea5e9;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 10px;">
<strong style="color: #0ea5e9;">Gruppen-Synchronisierung aktiv</strong> <strong style="color: #0ea5e9;">Gruppen-Range (Total: <span id="editLibraryGroupCount"></span>)</strong>
<span style="font-size: 12px; color: #666;">Gesamt: <span id="editLibraryGroupCount"></span> Exemplare</span>
</div> </div>
<p style="margin: 0 0 10px 0; font-size: 13px;">Verfügbare Codes in dieser Range:</p> <p style="margin: 5px 0; font-size: 12px; color: #555;">
<div id="editLibraryAllCodes" style="display: flex; flex-wrap: wrap; gap: 5px;"> Alle aufgeführten Codes gehören zu diesem Datensatz:
<!-- Codes werden hier per JS eingefügt -->
</div>
<p style="margin-top: 15px; font-size: 12px; color: #555; border-top: 1px solid #eee; pt: 10px;">
<em>Änderungen an allgemeinen Daten (Titel, Ort, etc.) betreffen <strong>alle</strong> oben aufgeführten Codes.</em>
</p> </p>
<!-- Hier werden die Codes per JS eingefügt -->
<div id="editLibraryAllCodes" style="display: flex; flex-wrap: wrap; gap: 5px; margin-top: 10px;"></div>
<div style="margin-top: 15px; font-size: 11px; background: #e0f2fe; padding: 8px; border-radius: 4px;">
<strong>Hinweis:</strong> Änderungen an Titel/Ort/Beschreibung werden auf <strong>alle</strong> Exemplare der Range übertragen.
</div>
</div> </div>
<form id="editLibraryForm"> <form id="editLibraryForm">
<!-- Formularfelder wie gehabt... -->
<input type="hidden" id="editLibraryItemId"> <input type="hidden" id="editLibraryItemId">
<div class="edit-grid"> <div class="edit-grid">
<!-- ... (Dein restliches Formular) ... --> <div class="full">
<label for="editLibraryName">Titel</label>
<input id="editLibraryName" required style="width: 100%;">
</div>
<div>
<label for="editLibraryType">Medientyp</label>
<select id="editLibraryType" style="width: 100%;">
<option value="book">Buch</option>
<option value="schoolbook">Schulbuch</option>
<option value="cd">CD</option>
<option value="dvd">DVD</option>
<option value="other">Sonstige Medien</option>
</select>
</div>
<div>
<label for="editLibraryIsbn">ISBN</label>
<input id="editLibraryIsbn" placeholder="optional ISBN-10/13" style="width: 100%;">
</div>
<div>
<label for="editLibraryCode4">Code</label>
<input id="editLibraryCode4" placeholder="optional Mediencode" style="width: 100%;">
</div>
<div class="full">
<label for="editLibraryLocation">Ort</label>
<input id="editLibraryLocation" required style="width: 100%;">
</div>
<div class="full">
<label for="editLibraryDescription">Beschreibung</label>
<textarea id="editLibraryDescription" rows="4" required style="width: 100%;"></textarea>
</div>
</div>
<div class="edit-actions" style="margin-top:20px;">
<button type="submit" class="button" style="background:#0ea5e9;color:#fff;">Speichern & Synchronisieren</button>
<button type="button" class="button" onclick="closeEditLibraryModal()">Abbrechen</button>
</div> </div>
<!-- ... -->
</form> </form>
</div> </div>
</div> </div>
<div id="editLibraryGroupWarning" style="display:none; background-color: #fff; padding: 15px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #0ea5e9;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 10px;">
<strong style="color: #0ea5e9;">Gruppen-Range (Total: <span id="editLibraryGroupCount"></span>)</strong>
</div>
<p style="margin: 5px 0; font-size: 12px; color: #555;">
Alle Codes in dieser Gruppe:
</p>
<!-- Hier wird die Liste als Komma-Text eingefügt -->
<div id="editLibraryAllCodes" style="font-family: monospace; font-size: 14px; font-weight: bold; color: #333; margin-top: 5px;"></div>
<div style="margin-top: 15px; font-size: 11px; background: #e0f2fe; padding: 8px; border-radius: 4px;">
<strong>Hinweis:</strong> Änderungen an Titel/Ort/Beschreibung werden auf <strong>alle</strong> Exemplare der Range übertragen.
</div>
</div>
{% endblock %} {% endblock %}
+1 -1
View File
@@ -122,7 +122,7 @@
<h3>Meldung von Sicherheitslücken</h3> <h3>Meldung von Sicherheitslücken</h3>
<div class="license-exception-notice"> <div class="license-exception-notice">
<h3>⚠️ Responsible Disclosure</h3> <h3>⚠️ Responsible Disclosure</h3>
<p>Falls Sie eine Sicherheitslücke entdecken, wenden sie sich umgehend an die Email: info@invario.eu . <p>Falls Sie eine Sicherheitslücke entdecken, wenden sie sich umgehend an die Email: info@invario-software.de .
</div> </div>
</div> </div>
</div><!-- /#pane-security --> </div><!-- /#pane-security -->
+50 -97
View File
@@ -3,7 +3,12 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="docker-compose-multitenant.yml" COMPOSE_FILE="docker-compose-multitenant.yml"
# Directories
INVOICE_ARCHIVE_DIR="${INVOICE_ARCHIVE_DIR:-/var/backups/invoice-archive}" INVOICE_ARCHIVE_DIR="${INVOICE_ARCHIVE_DIR:-/var/backups/invoice-archive}"
FULL_BACKUP_DIR="${FULL_BACKUP_DIR:-/var/backups/inventarsystem}"
LOG_DIR="$SCRIPT_DIR/logs"
KEEP_DAYS="${INVOICE_KEEP_DAYS:-3650}" KEEP_DAYS="${INVOICE_KEEP_DAYS:-3650}"
MODE="auto" MODE="auto"
BASE_NAME="" BASE_NAME=""
@@ -14,9 +19,7 @@ Usage: $0 [options]
Options: Options:
--invoice-archive-dir DIR Directory for invoice archive files --invoice-archive-dir DIR Directory for invoice archive files
(default: $INVOICE_ARCHIVE_DIR) --invoice-keep-days N Remove archived files older than N days
--invoice-keep-days N Remove archived invoice files older than N days
(default: $KEEP_DAYS)
--mode auto|docker|host Backup mode (default: auto) --mode auto|docker|host Backup mode (default: auto)
--base-name NAME Base filename prefix for archive files --base-name NAME Base filename prefix for archive files
-h, --help Show this help message -h, --help Show this help message
@@ -26,127 +29,78 @@ EOF
parse_args() { parse_args() {
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--invoice-archive-dir) --invoice-archive-dir) INVOICE_ARCHIVE_DIR="$2"; shift 2 ;;
INVOICE_ARCHIVE_DIR="$2" --invoice-keep-days) KEEP_DAYS="$2"; shift 2 ;;
shift 2 --mode) MODE="$2"; shift 2 ;;
;; --base-name) BASE_NAME="$2"; shift 2 ;;
--invoice-keep-days) -h|--help) usage; exit 0 ;;
KEEP_DAYS="$2" *) echo "Error: unknown option '$1'" >&2; usage; exit 2 ;;
shift 2
;;
--mode)
MODE="$2"
shift 2
;;
--base-name)
BASE_NAME="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: unknown option '$1'" >&2
usage
exit 2
;;
esac esac
done done
} }
ensure_directory() { ensure_directory() {
mkdir -p "$INVOICE_ARCHIVE_DIR" mkdir -p "$INVOICE_ARCHIVE_DIR"
mkdir -p "$FULL_BACKUP_DIR"
} }
cleanup_old_archives() { cleanup_old_archives() {
if [[ -n "$KEEP_DAYS" ]]; then if [[ -n "$KEEP_DAYS" ]]; then
find "$INVOICE_ARCHIVE_DIR" -maxdepth 1 -type f \( -name 'invoices-*.jsonl' -o -name 'invoices-*.csv' -o -name 'invoices-*.meta.json' \) -mtime +"$KEEP_DAYS" -print -delete || true find "$INVOICE_ARCHIVE_DIR" -maxdepth 1 -type f -mtime +"$KEEP_DAYS" -print -delete 2>/dev/null || true
fi fi
} # Cleanup old bundled backups (older than 30 days)
find "$FULL_BACKUP_DIR" -maxdepth 1 -type f -name "full_backup_*.tar.gz" -mtime +30 -print -delete 2>/dev/null || true
host_backup() {
if ! command -v python3 >/dev/null 2>&1; then
return 1
fi
if ! python3 -c 'import pymongo' >/dev/null 2>&1; then
return 1
fi
python3 "$SCRIPT_DIR/Web/backup_invoices.py" \
--archive-dir "$INVOICE_ARCHIVE_DIR" \
--base-name "$BASE_NAME"
} }
docker_backup() { docker_backup() {
if ! command -v docker >/dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1; then return 1; fi
return 1
fi local timestamp="$(date +'%Y-%m-%d_%H-%M-%S')"
if ! docker compose -f "$COMPOSE_FILE" ps -q app >/dev/null 2>&1; then local staging_dir="/tmp/backup_stage_$timestamp"
return 1
echo "[$(date +'%T')] Starting unified system backup..."
mkdir -p "$staging_dir"
# 1. MongoDB Dump (into staging)
if docker compose -f "$COMPOSE_FILE" ps -q mongodb >/dev/null 2>&1; then
echo "[$(date +'%T')] Dumping Database..."
docker compose -f "$COMPOSE_FILE" exec -T mongodb mongodump --archive --gzip > "$staging_dir/db.archive.gz"
fi fi
local app_container # 2. Archive Assets (into staging)
app_container="$(docker compose -f "$COMPOSE_FILE" ps -q app | tr -d '[:space:]')" if [ -d "./Web/uploads" ]; then
if [[ -z "$app_container" ]]; then echo "[$(date +'%T')] Archiving assets..."
return 1 tar -czf "$staging_dir/assets.tar.gz" -C . Web/uploads Web/thumbnails Web/previews 2>/dev/null || true
fi fi
local timestamp # 3. Archive Logs (into staging)
timestamp="$(date +'%Y-%m-%d_%H-%M-%S')" if [ -d "$LOG_DIR" ]; then
local output_dir echo "[$(date +'%T')] Archiving logs..."
local remote_base tar -czf "$staging_dir/logs.tar.gz" -C "$(dirname "$LOG_DIR")" "$(basename "$LOG_DIR")"
fi
output_dir="/tmp/invoice_archive_${timestamp}" # 4. Bundle everything into one file
remote_base="${output_dir}/${BASE_NAME}" echo "[$(date +'%T')] Bundling archive..."
tar -czf "$FULL_BACKUP_DIR/full_backup_$timestamp.tar.gz" -C "$staging_dir" .
docker compose -f "$COMPOSE_FILE" exec -T app mkdir -p "$output_dir"
docker compose -f "$COMPOSE_FILE" exec -T app python3 /app/Web/backup_invoices.py \ # 5. Cleanup staging
--archive-dir "$output_dir" \ rm -rf "$staging_dir"
--base-name "$BASE_NAME"
echo "[$(date +'%T')] Backup complete: $FULL_BACKUP_DIR/full_backup_$timestamp.tar.gz"
mkdir -p "$INVOICE_ARCHIVE_DIR" return 0
docker cp "$app_container":"${remote_base}.jsonl" "$INVOICE_ARCHIVE_DIR/"
docker cp "$app_container":"${remote_base}.csv" "$INVOICE_ARCHIVE_DIR/"
docker cp "$app_container":"${remote_base}.meta.json" "$INVOICE_ARCHIVE_DIR/"
docker compose -f "$COMPOSE_FILE" exec -T app rm -rf "$output_dir"
} }
main() { main() {
if [[ -z "$BASE_NAME" ]]; then parse_args "$@"
BASE_NAME="invoices-$(date +'%Y-%m-%d_%H-%M-%S')"
fi
ensure_directory ensure_directory
case "$MODE" in case "$MODE" in
auto) auto|docker)
if docker_backup; then if docker_backup; then
cleanup_old_archives cleanup_old_archives
return 0 return 0
fi fi
if host_backup; then echo "Error: Docker backup failed." >&2
cleanup_old_archives
return 0
fi
echo "Error: could not perform invoice backup in auto mode. Docker or host Python with pymongo is required." >&2
return 1
;;
docker)
if docker_backup; then
cleanup_old_archives
return 0
fi
echo "Error: docker backup failed or app container is unavailable." >&2
return 1
;;
host)
if host_backup; then
cleanup_old_archives
return 0
fi
echo "Error: host backup failed. Ensure python3 and pymongo are installed." >&2
return 1 return 1
;; ;;
*) *)
@@ -157,5 +111,4 @@ main() {
esac esac
} }
parse_args "$@" main "$@"
main
+1 -1
View File
@@ -27,7 +27,7 @@
"username": "", "username": "",
"password": "", "password": "",
"from_address": "", "from_address": "",
"default_sender_name": "Invario Inventurprogramm", "default_sender_name": "Invario Inventarprogramm",
"timeout_seconds": 30 "timeout_seconds": 30
}, },
"images": { "images": {
+15 -14
View File
@@ -8,8 +8,8 @@ else
INSTALLER_DIR="$(pwd)" INSTALLER_DIR="$(pwd)"
fi fi
PROJECT_DIR="/opt/Inventarsystem" PROJECT_DIR="/opt/Inventarsystem"
REPO_SLUG="AIIrondev/legendary-octo-garbanzo" REPO_SLUG="Invario/Inventarsystem"
API_URL="https://api.github.com/repos/$REPO_SLUG/releases/latest" API_URL="https://git.invario-software.eu/api/v1/repos/$REPO_SLUG/releases/latest"
BUNDLE_ASSET="inventarsystem-docker-bundle.tar.gz" BUNDLE_ASSET="inventarsystem-docker-bundle.tar.gz"
APP_IMAGE_ASSET_PREFIX="inventarsystem-image-" APP_IMAGE_ASSET_PREFIX="inventarsystem-image-"
LEGACY_DB_NAME="Inventarsystem" LEGACY_DB_NAME="Inventarsystem"
@@ -39,7 +39,7 @@ need_cmd() {
refresh_start_script_from_main() { refresh_start_script_from_main() {
local start_url local start_url
start_url="https://raw.githubusercontent.com/$REPO_SLUG/main/start.sh" start_url="https://git.invario-software.eu/$REPO_SLUG/raw/branch/main/start.sh"
if curl -fsSL "$start_url" -o "$TMP_DIR/start.sh"; then if curl -fsSL "$start_url" -o "$TMP_DIR/start.sh"; then
sudo install -m 755 "$TMP_DIR/start.sh" "$PROJECT_DIR/start.sh" sudo install -m 755 "$TMP_DIR/start.sh" "$PROJECT_DIR/start.sh"
@@ -62,7 +62,7 @@ import re
import sys import sys
compose_file, tag = sys.argv[1], sys.argv[2] compose_file, tag = sys.argv[1], sys.argv[2]
target_image = f"ghcr.io/aiirondev/legendary-octo-garbanzo:{tag}" target_image = f"git.invario-software.eu/invario/inventarsystem:{tag}"
with open(compose_file, "r", encoding="utf-8") as f: with open(compose_file, "r", encoding="utf-8") as f:
lines = f.readlines() lines = f.readlines()
@@ -173,9 +173,9 @@ Usage: $0 [options]
Options: Options:
--migrate-legacy-db Back up host MongoDB and import into Docker MongoDB --migrate-legacy-db Back up host MongoDB and import into Docker MongoDB
--remove-legacy-system Remove old host MongoDB/system after successful import --remove-legacy-system Remove old host MongoDB/system after successful import
--skip-cleanup-old Do not run cleanup-old.sh after install --skip-cleanup-old Do not run cleanup-old.sh after install
--cleanup-old-remove-cron Also remove matching cron entries during old-system cleanup --cleanup-old-remove-cron Also remove matching cron entries during old-system cleanup
--multitenant Use docker-compose-multitenant.yml (default) --multitenant Use docker-compose-multitenant.yml (default)
--legacy-db-name <name> Legacy database name (default: $LEGACY_DB_NAME) --legacy-db-name <name> Legacy database name (default: $LEGACY_DB_NAME)
--legacy-mongo-uri <uri> Legacy Mongo URI (default: $LEGACY_MONGO_URI) --legacy-mongo-uri <uri> Legacy Mongo URI (default: $LEGACY_MONGO_URI)
--legacy-system-dir <path> Optional old system directory to remove after migration --legacy-system-dir <path> Optional old system directory to remove after migration
@@ -432,8 +432,9 @@ main() {
curl -fL "$image_url" -o "$TMP_DIR/inventarsystem-image-$tag.tar.gz" curl -fL "$image_url" -o "$TMP_DIR/inventarsystem-image-$tag.tar.gz"
sudo docker load -i "$TMP_DIR/inventarsystem-image-$tag.tar.gz" >/dev/null sudo docker load -i "$TMP_DIR/inventarsystem-image-$tag.tar.gz" >/dev/null
# Compatibility alias: some legacy compose bundles may still reference :latest.
sudo docker tag "ghcr.io/aiirondev/legendary-octo-garbanzo:$tag" "ghcr.io/aiirondev/legendary-octo-garbanzo:latest" >/dev/null 2>&1 || true # Tagge das geladene Gitea-Image als latest
sudo docker tag "git.invario-software.eu/invario/inventarsystem:$tag" "git.invario-software.eu/invario/inventarsystem:latest" >/dev/null 2>&1 || true
if [ ! -f "$PROJECT_DIR/start.sh" ]; then if [ ! -f "$PROJECT_DIR/start.sh" ]; then
echo "Error: release bundle is missing start.sh" echo "Error: release bundle is missing start.sh"
@@ -470,19 +471,19 @@ EOF
NUITKA_BUILD=0 NUITKA_BUILD=0
INVENTAR_HTTP_PORT=10000 INVENTAR_HTTP_PORT=10000
INVENTAR_HTTPS_PORT=10001 INVENTAR_HTTPS_PORT=10001
INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:$tag INVENTAR_APP_IMAGE=git.invario-software.eu/invario/inventarsystem:$tag
EOF EOF
sudo install -m 644 "$TMP_DIR/.docker-build.env" "$PROJECT_DIR/.docker-build.env" sudo install -m 644 "$TMP_DIR/.docker-build.env" "$PROJECT_DIR/.docker-build.env"
elif sudo grep -q '^INVENTAR_APP_IMAGE=' "$PROJECT_DIR/.docker-build.env"; then elif sudo grep -q '^INVENTAR_APP_IMAGE=' "$PROJECT_DIR/.docker-build.env"; then
sudo sed -i "s|^INVENTAR_APP_IMAGE=.*|INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:$tag|" "$PROJECT_DIR/.docker-build.env" sudo sed -i "s|^INVENTAR_APP_IMAGE=.*|INVENTAR_APP_IMAGE=git.invario-software.eu/invario/inventarsystem:$tag|" "$PROJECT_DIR/.docker-build.env"
else else
echo "INVENTAR_APP_IMAGE=ghcr.io/aiirondev/legendary-octo-garbanzo:$tag" | sudo tee -a "$PROJECT_DIR/.docker-build.env" >/dev/null echo "INVENTAR_APP_IMAGE=git.invario-software.eu/invario/inventarsystem:$tag" | sudo tee -a "$PROJECT_DIR/.docker-build.env" >/dev/null
fi fi
backup_legacy_database backup_legacy_database
echo "Starting stack..." echo "Starting stack..."
sudo env INVENTAR_APP_IMAGE="ghcr.io/aiirondev/legendary-octo-garbanzo:$tag" bash "$PROJECT_DIR/start.sh" sudo env INVENTAR_APP_IMAGE="git.invario-software.eu/invario/inventarsystem:$tag" bash "$PROJECT_DIR/start.sh"
restore_legacy_backup_into_docker restore_legacy_backup_into_docker
cleanup_old_services cleanup_old_services
@@ -492,4 +493,4 @@ EOF
echo "Open: https://localhost" echo "Open: https://localhost"
} }
main "$@" main "$@"
Executable
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/inventarsystem"
COMPOSE_FILE="docker-compose-multitenant.yml"
# 1. Find the latest backup bundle
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/full_backup_*.tar.gz | head -n1 2>/dev/null || true)
if [[ -z "$LATEST_BACKUP" ]]; then
echo "Error: No backup bundle found in $BACKUP_DIR"
exit 1
fi
echo "--- RESTORE PROCEDURE ---"
echo "Found latest bundle: $(basename "$LATEST_BACKUP")"
read -p "WARNING: This will OVERWRITE your current database and assets! Continue? (y/N) " confirm
[[ "$confirm" != "y" ]] && exit 1
# 2. Extract the bundle to a temporary location
RESTORE_TMP="/tmp/restore_$(date +%s)"
mkdir -p "$RESTORE_TMP"
echo "Extracting bundle..."
tar -xzf "$LATEST_BACKUP" -C "$RESTORE_TMP"
# 3. Stop application
echo "Stopping application..."
docker compose -f "$COMPOSE_FILE" stop app
# 4. Restore Database
if [ -f "$RESTORE_TMP/db.archive.gz" ]; then
echo "Restoring MongoDB..."
zcat "$RESTORE_TMP/db.archive.gz" | docker compose -f "$COMPOSE_FILE" exec -T mongodb mongorestore --archive --gzip --drop
else
echo "Warning: Database archive not found in bundle."
fi
# 5. Restore Assets
if [ -f "$RESTORE_TMP/assets.tar.gz" ]; then
echo "Restoring Assets (Uploads/Thumbnails/Previews)..."
tar -xzf "$RESTORE_TMP/assets.tar.gz" -C .
else
echo "Warning: Asset archive not found in bundle."
fi
# 6. Restore Logs (Optional)
if [ -f "$RESTORE_TMP/logs.tar.gz" ]; then
echo "Restoring Logs..."
tar -xzf "$RESTORE_TMP/logs.tar.gz" -C .
fi
# 7. Start application
echo "Restarting application..."
docker compose -f "$COMPOSE_FILE" start app
# 8. Cleanup
rm -rf "$RESTORE_TMP"
echo "Restore complete!"
+2 -2
View File
@@ -5,7 +5,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR" cd "$SCRIPT_DIR"
ENV_FILE="$SCRIPT_DIR/.docker-build.env" ENV_FILE="$SCRIPT_DIR/.docker-build.env"
APP_IMAGE_REPO="ghcr.io/aiirondev/legendary-octo-garbanzo" APP_IMAGE_REPO="git.invario-software.eu/invario/inventarsystem"
DIST_DIR="$SCRIPT_DIR/dist" DIST_DIR="$SCRIPT_DIR/dist"
RUNTIME_COMPOSE_OVERRIDE_FILE="$SCRIPT_DIR/.docker-compose.runtime.override.yml" RUNTIME_COMPOSE_OVERRIDE_FILE="$SCRIPT_DIR/.docker-compose.runtime.override.yml"
@@ -676,4 +676,4 @@ fi
verify_stack_health verify_stack_health
echo "Stack started." echo "Stack started."
echo "Open: http://<server-ip>:$HTTP_PORT_VALUE" echo "Open: http://<server-ip>:$HTTP_PORT_VALUE"
+21 -18
View File
@@ -2,18 +2,18 @@
set -euo pipefail set -euo pipefail
# Release-only updater for Docker deployment. # Release-only updater for Docker deployment.
# Updates are pulled exclusively from GitHub Releases assets. # Updates are pulled exclusively from Gitea Releases assets.
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="$PROJECT_DIR/logs" LOG_DIR="$PROJECT_DIR/logs"
LOG_FILE="$LOG_DIR/update.log" LOG_FILE="$LOG_DIR/update.log"
STATE_FILE="$PROJECT_DIR/.release-version" STATE_FILE="$PROJECT_DIR/.release-version"
REPO_SLUG="AIIrondev/legendary-octo-garbanzo" REPO_SLUG="Invario/Inventarsystem"
API_URL="https://api.github.com/repos/$REPO_SLUG/releases/latest" API_URL="https://git.invario-software.eu/api/v1/repos/$REPO_SLUG/releases/latest"
BUNDLE_ASSET="inventarsystem-docker-bundle.tar.gz" BUNDLE_ASSET="inventarsystem-docker-bundle.tar.gz"
APP_IMAGE_ASSET_PREFIX="inventarsystem-image-" APP_IMAGE_ASSET_PREFIX="inventarsystem-image-"
ENV_FILE="$PROJECT_DIR/.docker-build.env" ENV_FILE="$PROJECT_DIR/.docker-build.env"
APP_IMAGE_REPO="ghcr.io/aiirondev/legendary-octo-garbanzo" APP_IMAGE_REPO="git.invario-software.eu/invario/inventarsystem"
DIST_DIR="$PROJECT_DIR/dist" DIST_DIR="$PROJECT_DIR/dist"
COMPOSE_FILE="docker-compose-multitenant.yml" COMPOSE_FILE="docker-compose-multitenant.yml"
MIN_ROOT_FREE_MB="${INVENTAR_MIN_ROOT_FREE_MB:-2048}" MIN_ROOT_FREE_MB="${INVENTAR_MIN_ROOT_FREE_MB:-2048}"
@@ -153,7 +153,7 @@ Usage: $0 [options]
Options: Options:
--multitenant Use docker-compose-multitenant.yml (default) --multitenant Use docker-compose-multitenant.yml (default)
development Install development build from GHCR or local dist development Install development build from Gitea Registry or local dist
-h, --help Show this help message -h, --help Show this help message
EOF EOF
} }
@@ -204,14 +204,14 @@ create_backup() {
fi fi
fi fi
if [ -x "$PROJECT_DIR/run-backup.sh" ]; then if [ -x "$PROJECT_DIR/backup.sh" ]; then
if "$PROJECT_DIR/run-backup.sh" >> "$LOG_FILE" 2>&1; then if "$PROJECT_DIR/backup.sh" --mode auto >> "$LOG_FILE" 2>&1; then
log_message "Backup completed" log_message "Backup completed"
else else
log_message "WARNING: Backup failed; continuing with release update" log_message "WARNING: Backup failed; continuing with release update"
fi fi
else else
log_message "WARNING: run-backup.sh not found; skipping backup" log_message "WARNING: backup.sh not found; skipping backup"
fi fi
} }
@@ -275,10 +275,10 @@ load_release_image() {
refresh_runtime_scripts_from_main() { refresh_runtime_scripts_from_main() {
local start_url stop_url restart_url update_url local start_url stop_url restart_url update_url
start_url="https://raw.githubusercontent.com/$REPO_SLUG/main/start.sh" start_url="https://git.invario-software.eu/$REPO_SLUG/raw/branch/main/start.sh"
stop_url="https://raw.githubusercontent.com/$REPO_SLUG/main/stop.sh" stop_url="https://git.invario-software.eu/$REPO_SLUG/raw/branch/main/stop.sh"
restart_url="https://raw.githubusercontent.com/$REPO_SLUG/main/restart.sh" restart_url="https://git.invario-software.eu/$REPO_SLUG/raw/branch/main/restart.sh"
update_url="https://raw.githubusercontent.com/$REPO_SLUG/main/update.sh" update_url="https://git.invario-software.eu/$REPO_SLUG/raw/branch/main/update.sh"
curl -fsSL "$start_url" -o "$PROJECT_DIR/start.sh" || log_message "WARNING: Could not refresh start.sh from main" curl -fsSL "$start_url" -o "$PROJECT_DIR/start.sh" || log_message "WARNING: Could not refresh start.sh from main"
curl -fsSL "$stop_url" -o "$PROJECT_DIR/stop.sh" || log_message "WARNING: Could not refresh stop.sh from main" curl -fsSL "$stop_url" -o "$PROJECT_DIR/stop.sh" || log_message "WARNING: Could not refresh stop.sh from main"
@@ -357,7 +357,7 @@ download_and_extract_bundle() {
cp -f "$tmp_dir/update.sh" "$PROJECT_DIR/update.sh" cp -f "$tmp_dir/update.sh" "$PROJECT_DIR/update.sh"
fi fi
# Neu: Multi-Tenant Ressourcen kopieren, falls vorhanden # Multi-Tenant Ressourcen kopieren, falls vorhanden
if [ -f "$tmp_dir/docker-compose-multitenant.yml" ]; then if [ -f "$tmp_dir/docker-compose-multitenant.yml" ]; then
cp -f "$tmp_dir/docker-compose-multitenant.yml" "$PROJECT_DIR/docker-compose-multitenant.yml" cp -f "$tmp_dir/docker-compose-multitenant.yml" "$PROJECT_DIR/docker-compose-multitenant.yml"
fi fi
@@ -374,7 +374,7 @@ download_and_extract_bundle() {
done done
# Ensure executable permissions on all copied scripts # 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/manage-tenant.sh" "$PROJECT_DIR/run-tenant-cmd.sh" 2>/dev/null || true 2>/dev/null || true chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh" "$PROJECT_DIR/manage-tenant.sh" "$PROJECT_DIR/run-tenant-cmd.sh" 2>/dev/null || true
chmod +x "$PROJECT_DIR"/manage-tenant.sh "$PROJECT_DIR"/run-tenant-cmd.sh 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 if [ ! -f "$PROJECT_DIR/config.json" ] && [ -f "$tmp_dir/config.json" ]; then
@@ -412,7 +412,7 @@ EOF
if ! load_local_dist_image "$tag"; then if ! load_local_dist_image "$tag"; then
if ! load_release_image "$meta_file" "$tag"; then if ! load_release_image "$meta_file" "$tag"; then
log_message "Falling back to tagged GHCR image $app_image" log_message "Falling back to tagged Gitea Registry image $app_image"
if ! docker pull "$app_image" >> "$LOG_FILE" 2>&1; then if ! docker pull "$app_image" >> "$LOG_FILE" 2>&1; then
log_message "Falling back to local Docker build for $app_image" log_message "Falling back to local Docker build for $app_image"
docker build -t "$app_image" "$PROJECT_DIR" >> "$LOG_FILE" 2>&1 docker build -t "$app_image" "$PROJECT_DIR" >> "$LOG_FILE" 2>&1
@@ -511,7 +511,7 @@ EOF
printf '\nINVENTAR_APP_IMAGE=%s\n' "$app_image" >> "$ENV_FILE" printf '\nINVENTAR_APP_IMAGE=%s\n' "$app_image" >> "$ENV_FILE"
fi fi
# Try local dist first, then pull from GHCR # Try local dist first, then pull from Gitea Registry
if ! load_local_dist_image "$tag"; then if ! load_local_dist_image "$tag"; then
log_message "Attempting to pull development image $app_image" log_message "Attempting to pull development image $app_image"
if ! docker pull "$app_image" >> "$LOG_FILE" 2>&1; then if ! docker pull "$app_image" >> "$LOG_FILE" 2>&1; then
@@ -540,7 +540,7 @@ EOF
trap 'rm -rf "${tmp_dir:-}"' EXIT trap 'rm -rf "${tmp_dir:-}"' EXIT
log_message "Checking latest GitHub release for $REPO_SLUG..." log_message "Checking latest Gitea release for $REPO_SLUG..."
if ! fetch_release_metadata "$meta_file"; then if ! fetch_release_metadata "$meta_file"; then
log_message "WARNING: Could not fetch release metadata. Falling back to self-healing start path." log_message "WARNING: Could not fetch release metadata. Falling back to self-healing start path."
if INVENTAR_SETUP_CRON=0 bash "$PROJECT_DIR/start.sh" >> "$LOG_FILE" 2>&1; then if INVENTAR_SETUP_CRON=0 bash "$PROJECT_DIR/start.sh" >> "$LOG_FILE" 2>&1; then
@@ -621,6 +621,9 @@ EOF
cleanup_old_dist_artifacts cleanup_old_dist_artifacts
cleanup_docker_dangling_images cleanup_docker_dangling_images
log_message "Update completed successfully to release $latest_tag" log_message "Update completed successfully to release $latest_tag"
sudo ./opt/Inventarsystem/restart.sh
echo "Restart of the Server Completed"
} }
main "$@" main "$@"