Compare commits

...

112 Commits

Author SHA1 Message Date
Aiirondev_dev 3068f44563 implementet automatic including of the price 2026-07-27 20:16:53 +02:00
Aiirondev_dev c4dd18a2e4 change to have automatic clearing of the tmp directory 2026-07-27 15:30:34 +02:00
Aiirondev_dev d0ac21e7dd changes to the Library Type 2026-07-27 15:20:27 +02:00
Aiirondev_dev b4a505c20b changes to the borrow_record creation 2026-07-27 15:09:26 +02:00
Aiirondev_dev 39302d4d1f CHanges to the In and out 2026-07-27 14:53:09 +02:00
Aiirondev_dev e46f8b0c66 finisch of the borrower funktion 2026-07-27 14:43:39 +02:00
Aiirondev_dev 06486f039f changes for the loading of the recent borrowers that only the latest 3 get displayed 2026-07-27 14:34:59 +02:00
Aiirondev_dev feac00f0df changes to the permission development 2026-07-27 14:19:01 +02:00
Aiirondev_dev 0b0169ef96 made the Cookie Banner accoding to the ePrivacy-Richtlinie/TTDSG only a visual informative banner 2026-07-27 11:35:12 +02:00
Aiirondev_dev a6db3a001f changes to maybe have the right passtword 2026-07-27 00:32:21 +02:00
Aiirondev_dev 8fa495a23c changes to allow a easier register process 2026-07-26 23:52:08 +02:00
Aiirondev_dev 9df7a73db1 change to the CRFS 2026-07-26 23:45:49 +02:00
Aiirondev_dev dcfd23b412 changes to work 2026-07-26 23:21:56 +02:00
Aiirondev_dev d523dd0a68 change to allow for the wtf tocken to be read in correctly 2026-07-26 23:08:47 +02:00
Aiirondev_dev d7d96d5567 changes to the register Funktion 2026-07-26 22:56:39 +02:00
Aiirondev_dev a0018eebd5 slight mistake because of disregard for the new package that needs to be in the requirements 2026-07-26 22:37:45 +02:00
Aiirondev_dev eebaaef9ea changes to the register process to make it more streamlined 2026-07-26 22:30:26 +02:00
Aiirondev_dev 86112ff295 changes to the username generation to have a more brother view 2026-07-26 22:10:00 +02:00
Aiirondev_dev 048900058f Fiy for a wrong endpoint 2026-07-26 22:00:53 +02:00
Aiirondev_dev b8923b4417 Debug changes 2026-07-26 20:48:09 +02:00
Aiirondev_dev a864bab713 Fix of issues with the student user getting from the database users to student_cards 2026-07-26 20:31:16 +02:00
Aiirondev_dev 14322e11c0 ahhhhhhhh 2026-07-26 15:56:15 +02:00
Aiirondev_dev 93bb901f45 changes to make home_admmin the only home and also the main home so the style is the same and the authorisations are properly displayed 2026-07-26 15:41:31 +02:00
Aiirondev_dev 4d5ff86f50 Haha sie laufen wieder 2026-07-25 23:07:32 +02:00
Aiirondev_dev ceb50ab29c Hallo bitte geh einfch oder weg ist auch okay aber geh egal was ist 2026-07-25 22:55:43 +02:00
Aiirondev_dev d6ecf419ea the Changes are fixing a problem in the invoice creation 2026-07-25 22:39:01 +02:00
Aiirondev_dev dceea0b047 removal of a stray ) in the Code 2026-07-25 21:26:07 +02:00
Aiirondev_dev a9af17c7ce Changes to the page permissions and action permissions to replace the old is_admin system 2026-07-25 20:15:55 +02:00
Aiirondev_dev 386ecd4409 slight changes to the presets 2026-07-25 15:19:14 +02:00
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
Aiirondev_dev 46176c1741 slight fixes 2026-06-28 16:45:53 +02:00
Aiirondev_dev c57b4a9a44 edit Modal changes to accomidate the library Modul 2026-06-28 16:30:41 +02:00
Aiirondev_dev 6f424cc8aa Backup and restore funktionality 2026-06-28 15:55:49 +02:00
Aiirondev_dev 103b82af15 smal fix 2026-06-28 00:11:16 +02:00
Aiirondev_dev 69c8aa8700 Development fix 2026-06-27 23:59:56 +02:00
Aiirondev_dev 1fa3dace17 changes for a smal issue 2026-06-27 23:51:47 +02:00
Aiirondev_dev 6dced8b1a5 additional fix for the cathergorie 2026-06-27 23:33:01 +02:00
Aiirondev_dev 493ee2ea7f fiy of a funktion issue 2026-06-27 23:28:00 +02:00
Aiirondev_dev 52a2ec0cad Adding of the detailed module 2026-06-27 23:10:14 +02:00
Aiirondev_dev b5f2c3c5c5 Adding of the category 2026-06-27 23:09:24 +02:00
Aiirondev_dev 403c281c93 Debug? 2026-06-27 22:52:40 +02:00
Aiirondev_dev 6d121f6110 Slight adjustment 2026-06-27 22:37:04 +02:00
Aiirondev_dev 9d940c6151 Denbugging and fix of a Code_4 generation issue 2026-06-27 22:20:10 +02:00
Aiirondev_dev 980b825e07 Test 2026-06-27 22:11:28 +02:00
Aiirondev_dev 94326728eb development debuggig 2026-06-27 22:01:51 +02:00
Aiirondev_dev efe17883a6 Development version 2026-06-27 21:51:50 +02:00
Aiirondev_dev 76f93b3d2e Fix of an unused detail in the Detail Modal 2026-06-27 21:18:51 +02:00
Aiirondev_dev 8f793045c2 removed unused return information 2026-06-27 17:24:42 +02:00
Aiirondev_dev a199439d76 slight fix 2026-06-27 17:20:46 +02:00
Aiirondev_dev 6cec483390 slight change to the mongodb accesing 2026-06-27 17:08:59 +02:00
Aiirondev_dev 10e2f7cc17 fur5ther fixes of to much information getting out 2026-06-27 17:00:40 +02:00
Aiirondev_dev 0f8c5a0fec slight adjustments for the security 2026-06-27 16:29:13 +02:00
Aiirondev_dev 091147ecfd slight security fixes to prevent malisios actors from getting access or retrieving unfit information 2026-06-27 16:07:26 +02:00
25 changed files with 1873 additions and 944 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:
push:
@@ -25,6 +25,7 @@ env:
jobs:
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
steps:
@@ -34,18 +35,20 @@ jobs:
- name: Set metadata
id: meta
env:
# Gitea stellt das GITHUB_TOKEN für Kompatibilität mit GitHub Actions automatisch zur Verfügung
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
EVENT_NAME: ${{ github.event_name }}
REF_NAME: ${{ github.ref_name }}
BUMP_TYPE: ${{ github.event.inputs.bump || 'patch' }}
REPO: ${{ gitea.repository }}
EVENT_NAME: ${{ gitea.event_name }}
REF_NAME: ${{ gitea.ref_name }}
BUMP_TYPE: ${{ gitea.event.inputs.bump || 'patch' }}
DOCKER_API_VERSION: '1.44'
run: |
if [ "$EVENT_NAME" = "push" ] && [ -n "$REF_NAME" ]; then
TAG="$REF_NAME"
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"
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)
if [ -n "$tag_name" ]; then
latest_tag="$tag_name"
@@ -108,19 +111,70 @@ jobs:
TAG="${base}.${i}"
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 "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
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: git.invario-software.eu
username: ${{ gitea.actor }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push release image
uses: docker/build-push-action@v6
@@ -128,10 +182,9 @@ jobs:
context: .
file: ./Dockerfile
push: true
load: true # Lädt das Image in den lokalen Docker-Daemon für den 'docker save' Schritt
tags: |
${{ 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
run: |
@@ -139,20 +192,10 @@ jobs:
IMG="${{ steps.meta.outputs.image }}"
TAG="${{ steps.meta.outputs.tag }}"
if docker image inspect "$IMG" >/dev/null 2>&1; then
echo "Using local image $IMG"
docker save "$IMG" | gzip > "inventarsystem-image-${TAG}.tar.gz"
exit 0
fi
echo "Pulling freshly pushed image from registry: $IMG"
docker pull "$IMG"
echo "Local image $IMG not found, trying to pull"
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" .
echo "Saving image to offline tar archive..."
docker save "$IMG" | gzip > "inventarsystem-image-${TAG}.tar.gz"
- name: Create release-only docker bundle
@@ -161,7 +204,7 @@ jobs:
cat > release-bundle/docker-compose.yml <<EOF
services:
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
restart: unless-stopped
ports:
@@ -225,7 +268,7 @@ jobs:
EOF
# 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
cp "$f" "release-bundle/$(basename "$f")"
fi
@@ -242,12 +285,10 @@ jobs:
find release-bundle -maxdepth 1 -type f -name '*.sh' -exec chmod +x {} \; || true
tar -czf inventarsystem-docker-bundle.tar.gz -C release-bundle .
- name: Create or update GitHub Release
uses: softprops/action-gh-release@v2
- name: Create or update Gitea Release
uses: https://gitea.com/actions/gitea-release-action@v1
with:
tag_name: ${{ steps.meta.outputs.tag }}
files: |
inventarsystem-docker-bundle.tar.gz
inventarsystem-image-${{ steps.meta.outputs.tag }}.tar.gz
fail_on_unmatched_files: false
generate_release_notes: true
inventarsystem-image-${{ steps.meta.outputs.tag }}.tar.gz
+3 -3
View File
@@ -120,19 +120,19 @@ Das Inventarsystem stellt folgende Wartungsskripte bereit:
**Option 1**
```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**
```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:
```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
```
+751 -452
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1235,5 +1235,5 @@ def reset_item_completely(item_id):
except Exception as e:
return {
'success': False,
'message': f'Fehler beim Zurücksetzen: {str(e)}'
'message': f'Fehler beim Zurücksetzen.'
}
+37 -40
View File
@@ -24,7 +24,7 @@ import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'media')
LIBRARY_ITEM_TYPES = ('book', 'cd', 'dvd', 'other', 'schoolbook', 'Buch', 'Schulbuch', 'schulbuch')
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,
series_group_id=None, series_count=1, series_position=1,
is_grouped_sub_item=False, parent_item_id=None,
isbn=None, item_type='general'):
isbn=None, item_type='general', library_category=None, is_library=False):
"""
Add a new item to the inventory.
@@ -70,6 +70,9 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
series_position (int, optional): Position inside the batch (1-based)
is_grouped_sub_item (bool, optional): Whether this item is hidden as sub-item
parent_item_id (str, optional): Parent item id if this is a sub-item
isbn (str, optional): ISBN for books or media items
item_type (str, optional): Type of the item (e.g., 'general', 'book', 'cd')
library_category (str, optional): Library category for the item
Returns:
ObjectId: ID of the new item or None if failed
@@ -97,7 +100,9 @@ def add_item(name, ort, beschreibung, images=None, filter=None, filter2=None, fi
'Anschaffungskosten': ansch_kost,
'Code_4': code_4,
'ISBN': isbn,
'library_category': library_category,
'ItemType': item_type,
'is_library': is_library,
'SeriesGroupId': series_group_id,
'SeriesCount': series_count,
'SeriesPosition': series_position,
@@ -194,69 +199,61 @@ def get_group_item_ids(id):
return []
def update_item(id, name, ort, beschreibung, images=None, verfuegbar=True,
filter=None, filter2=None, filter3=None, ansch_jahr=None, ansch_kost=None, code_4=None, reservierbar=True,
isbn=None, item_type='general'):
"""
Update an existing inventory item.
Args:
id (str): ID of the item to update
name (str): Name of the item
ort (str): Location of the item
beschreibung (str): Description of the item
images (list, optional): List of image filenames for the item
verfuegbar (bool, optional): Availability status of the item
filter (str, optional): Primary filter/category for the item
filter2 (str, optional): Secondary filter/category for the item
filter3 (str, optional): Tertiary filter/category for the item
ansch_jahr (int, optional): Year of acquisition
ansch_kost (float, optional): Cost of acquisition
code_4 (str, optional): 4-digit identification code
reservierbar (bool, optional): Whether the item can be reserved in advance
Returns:
bool: True if successful, False otherwise
"""
def update_item(id, name, ort, beschreibung, images, verfuegbar, filter1, filter2, filter3,
ansch_jahr, ansch_kost, code_4, reservierbar, isbn=None, item_type='general'):
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
items = db['items']
# Set default values for optional parameters
if images is None:
images = []
# 1. Altes Item laden, um SeriesGroupId zu bestimmen
old_item = items.find_one({'_id': ObjectId(id)})
if not old_item:
return False
series_group_id = old_item.get('SeriesGroupId')
update_data = {
# 2. Shared Data: Daten, die für ALLE in der Gruppe gleich sind
shared_update = {
'Name': name,
'Ort': ort,
'Beschreibung': beschreibung,
'Images': images,
'Verfuegbar': verfuegbar,
'Reservierbar': reservierbar,
'Filter': filter,
'Filter': filter1,
'Filter2': filter2,
'Filter3': filter3,
'Anschaffungsjahr': ansch_jahr,
'Anschaffungskosten': ansch_kost,
'Code_4': code_4,
'Reservierbar': reservierbar,
'ISBN': isbn,
'ItemType': item_type,
'Verfuegbar': verfuegbar, # Wir behalten den Status bei
'LastUpdated': datetime.datetime.now()
}
result = items.update_one(
{'_id': ObjectId(id)},
{'$set': update_data}
)
# 3. Spezifische Daten: Was NICHT synchronisiert wird
specific_update = shared_update.copy()
specific_update['Code_4'] = code_4
# 4. Das aktuelle Item updaten
items.update_one({'_id': ObjectId(id)}, {'$set': specific_update})
# 5. Alle anderen Gruppen-Mitglieder synchronisieren
if series_group_id:
items.update_many(
{
'SeriesGroupId': series_group_id,
'_id': {'$ne': ObjectId(id)}
},
{'$set': shared_update}
)
client.close()
return result.modified_count > 0
return True
except Exception as e:
print(f"Error updating item: {e}")
return False
def update_item_status(id, verfuegbar, user=None):
"""
Update the availability status of an inventory item.
+18 -12
View File
@@ -97,18 +97,18 @@ def build_name_synonym(first_name, last_name=''):
last = _clean_name_fragment(last_name)
if first and last:
return (first[:2] + last[:2]).title()
return (first[:3] + last[:3]).title()
combined = (first + last)
if not combined:
return 'User'
return combined[:4].title()
return combined[:6].title()
def build_username_from_name(first_name, last_name=''):
"""
Build a deterministic username abbreviation from first and last name.
Uses 2 letters from each name and stores it lowercase.
Uses 3 letters from each name and stores it lowercase.
Args:
first_name (str): First name
@@ -123,12 +123,12 @@ def build_username_from_name(first_name, last_name=''):
def build_unique_username_from_name(first_name, last_name=''):
"""
Build a unique username from the first 2 letters of the first name and
the first 2 letters of the last name.
Build a unique username from the first 3 letters of the first name and
the first 3 letters of the last name.
"""
first = _clean_name_fragment(first_name)
last = _clean_name_fragment(last_name)
base_username = (first[:2] + last[:2]).lower()
base_username = (first[:3] + last[:3]).lower()
if not base_username:
base_username = 'user'
@@ -323,7 +323,8 @@ def get_effective_permissions(username):
if bool(user.get('Admin', False)):
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['actions'] = _normalize_bool_map(user.get('ActionPermissions', {}), payload['actions'])
payload['pages'] = _normalize_bool_map(user.get('PagePermissions', {}), payload['pages'])
@@ -552,10 +553,15 @@ def add_user(
for key, value in page_permissions.items():
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 = {
'Username': username,
'Password': hashing(password),
'Admin': False,
'Admin': can_admin_preset_based,
'active_ausleihung': None,
'name': name.strip() if name else '',
'last_name': last_name.strip() if last_name else '',
@@ -588,8 +594,8 @@ def student_card_exists(student_card_id):
return False
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client)
users = db['users']
exists = users.find_one({'StudentCardId': normalized}) is not None
users = db['student_cards']
exists = users.find_one({'SchülerName': normalized}) is not None
client.close()
return exists
@@ -601,8 +607,8 @@ def get_user_by_student_card(student_card_id):
return None
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = _get_tenant_db(client)
users = db['users']
found_user = users.find_one({'StudentCardId': normalized})
users = db['student_cards']
found_user = users.find_one({'SchülerName': normalized})
client.close()
return found_user
+15 -12
View File
@@ -37,20 +37,23 @@ def generate_library_excel(items):
item.get("ReturnDate", ""),
item.get("Anschaffungskosten", "")
]
ws.append(row)
for col in ws.columns:
max_length = 0
column = col[0].column_letter
for cell in col:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
ws.column_dimensions[column].width = min(max_length + 2, 50)
# FIX: Clean the row data INSIDE the loop for every single item
clean_row = []
for value in row:
if isinstance(value, list):
# Converts ['A', 'B'] to "A, B" and an empty list [] to an empty string ""
clean_row.append(", ".join(map(str, value)) if value else "")
elif isinstance(value, dict):
# Just in case there is an embedded MongoDB sub-document
clean_row.append(str(value))
else:
clean_row.append(value)
# Append the safe, flattened row to the worksheet
ws.append(clean_row)
excel_buffer = io.BytesIO()
wb.save(excel_buffer)
excel_buffer.seek(0)
return excel_buffer
return excel_buffer
+1
View File
@@ -1,4 +1,5 @@
flask
flask-wtf
werkzeug
gunicorn
pymongo
+197 -85
View File
@@ -1130,7 +1130,7 @@
<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>
</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">
<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>
@@ -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>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="termMoreDropdown">
{% 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>
{% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
<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>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% 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>
{% 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>
{% endif %}
<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>
</button>
<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>
{% endif %}
<li><hr class="dropdown-divider"></li>
@@ -1214,13 +1216,13 @@
</button>
<div class="collapse navbar-collapse" id="inventoryNavContent">
<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">
<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>
{% endif %}
{% 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">
<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>
@@ -1239,42 +1241,44 @@
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="invMoreDropdown">
{% 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>
{% 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>
{% endif %}
<li><hr class="dropdown-divider"></li>
{% 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>
{% 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>
{% 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>
{% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
<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>
{% 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>
{% 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>
{% 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>
{% endif %}
<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>
{% 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>
{% 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>
{% endif %}
<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>
</button>
<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>
{% endif %}
<li><hr class="dropdown-divider"></li>
@@ -1336,19 +1340,19 @@
</button>
<div class="collapse navbar-collapse" id="libraryNavContent">
<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">
<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>
{% endif %}
{% if 'username' in session %}
{% if current_permissions.pages.get('tutorial_page', True) %}
{% if current_permissions.pages.get('tutorial_page', False) %}
<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>
</li>
{% 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">
<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>
@@ -1365,22 +1369,26 @@
Mehr Optionen
</a>
<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>
{% 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>
{% endif %}
{% if student_cards_module_enabled %}
{% if current_permissions.actions.get('can_manage_users', False) %}
<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>
{% endif %}
<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>
{% 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>
{% 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>
{% endif %}
<li><hr class="dropdown-divider"></li>
@@ -1414,7 +1422,7 @@
<span class="user-notification-dot {% if unread_notification_count and unread_notification_count > 0 %}visible{% endif %}" aria-hidden="true"></span>
</button>
<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>
{% endif %}
<li><hr class="dropdown-divider"></li>
@@ -1613,11 +1621,10 @@
<div id="cookie-banner" role="dialog" aria-live="polite" aria-label="Cookie-Hinweis">
<div class="cb-inner">
<div class="cb-text">
Wir verwenden technisch notwendige Cookies, um Ihre Sitzung zu verwalten und die Anwendung bereitzustellen. Bitte akzeptieren Sie Cookies, um fortzufahren.
Wir verwenden ausschließlich technisch notwendige Cookies, um Ihre Sitzung zu verwalten und die Anwendung bereitzustellen. Bitte bestätigen Sie dies, um fortzufahren.
</div>
<div class="cb-actions">
<button class="btn-decline" id="cookie-decline">Ablehnen</button>
<button class="btn-accept" id="cookie-accept">Akzeptieren</button>
<button class="btn-accept" id="cookie-accept">Notwendige Cookies akzeptieren</button>
</div>
</div>
</div>
@@ -1642,30 +1649,79 @@
</div>
<datalist id="function-search-options">
{% if current_permissions.pages.get('home', False) %}
<option value="Artikel"></option>
<option value="Meine Ausleihen"></option>
<option value="Benachrichtigungen"></option>
<option value="Tutorial"></option>
{% endif %}
{% 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="Lizenz"></option>
{% if library_module_enabled %}
<option value="Bibliothek"></option>
<option value="Meine Medien"></option>
{% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) %}
<option value="Hochladen"></option>
<option value="Ausleihen Verwaltung"></option>
<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>
{% if current_permissions.pages.get('library_view', False) %}
<option value="Bibliothek"></option>
{% endif %}
{% if 'username' in session and current_permissions.pages.get('my_borrowed_items', False) %}
<option value="Meine Medien"></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 %}
</datalist>
@@ -1673,31 +1729,39 @@
(function(){
function getCookie(name){
const v = document.cookie.split(';').map(s=>s.trim());
for(const c of v){ if(c.startsWith(name+'=')) return decodeURIComponent(c.split('=')[1]); }
for(const c of v){
if(c.startsWith(name+'=')) return decodeURIComponent(c.split('=')[1]);
}
return null;
}
function setCookie(name, value, days){
const d = new Date(); d.setTime(d.getTime() + (days*24*60*60*1000));
const d = new Date();
d.setTime(d.getTime() + (days*24*60*60*1000));
document.cookie = name + '=' + encodeURIComponent(value) + ';expires=' + d.toUTCString() + ';path=/;SameSite=Lax';
}
function showBanner(){ var el = document.getElementById('cookie-banner'); if(el) el.style.display = 'block'; }
function hideBanner(){ var el = document.getElementById('cookie-banner'); if(el) el.style.display = 'none'; }
// If not decided yet, show banner and block app until decision
function showBanner(){
var el = document.getElementById('cookie-banner');
if(el) el.style.display = 'block';
}
function hideBanner(){
var el = document.getElementById('cookie-banner');
if(el) el.style.display = 'none';
}
// Prüfen, ob der Nutzer bereits zugestimmt hat
const consent = getCookie('cookie_consent');
if(!consent){
showBanner();
// Optionally blur content until consent
document.body.style.filter = 'none';
}
// Nur noch der Akzeptieren-Button für vitale Cookies ist vorhanden
document.getElementById('cookie-accept')?.addEventListener('click', function(){
setCookie('cookie_consent','accepted',365);
setCookie('cookie_consent', 'vital_accepted', 365);
hideBanner();
});
document.getElementById('cookie-decline')?.addEventListener('click', function(){
setCookie('cookie_consent','declined',365);
window.location.href = 'https://www.ecosia.org/';
});
const username = {{ (session['username'] if 'username' in session else '')|tojson }};
const isTutorialPage = window.location.pathname === {{ url_for('tutorial_page')|tojson }};
@@ -1713,32 +1777,80 @@
const loginHintKey = username ? ('inventarsystem_notification_login_hint_v1_' + username) : null;
const functionSearchEntries = [
{% if current_permissions.pages.get('home', False) %}
{ 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 }} },
{ label: 'Benachrichtigungen', keywords: ['benachrichtigungen', 'nachrichten', 'notifications'], url: {{ url_for('notifications_view')|tojson }} },
{ label: 'Tutorial', keywords: ['tutorial', 'hilfe', 'anleitung'], url: {{ url_for('tutorial_page')|tojson }} },
{% endif %}
{% 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: 'Lizenz', keywords: ['lizenz', 'license'], url: {{ url_for('license')|tojson }} },
{% if library_module_enabled %}
{ label: 'Bibliothek', keywords: ['bibliothek', 'medien'], url: {{ url_for('library_view')|tojson }} },
{% endif %}
{% if 'username' in session and (session.get('admin', False) or is_admin) %}
{ 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 }} },
{% if current_permissions.pages.get('library_view', False) %}
{ label: 'Bibliothek', keywords: ['bibliothek', 'medien'], url: {{ url_for('library_view')|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 %}
];
+1 -1
View File
@@ -22,7 +22,7 @@
<p><strong>Name:</strong> Invario UG</p>
<p><strong>Adresse:</strong> Am Sportplatz 10<br>83052 Bruckmühl<br>Deutschland</p>
</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 class="impressum-section mb-4">
@@ -530,6 +530,8 @@
const itemCode = row.dataset.itemCode || '';
const itemCost = row.dataset.itemCost || '';
document.getElementById("damage-invoice-amount").value = "{{ item.acquisition_costs }}";
damageInvoiceForm.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
damageInvoiceItem.value = itemName;
damageInvoiceBorrower.value = borrower;
+242 -42
View File
@@ -4,6 +4,57 @@
{% block content %}
<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-container {
max-width: 1400px;
@@ -418,7 +469,7 @@
}
</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 -->
<div class="library-header">
<h1>📚 Bibliothek</h1>
@@ -475,7 +526,7 @@
<select id="filterType">
<option value="">-- Alle Typen --</option>
<option value="book">Buch</option>
<option value="schulbuch">Schulbuch</option>
<option value="schoolbook">Schulbuch</option>
<option value="cd">CD</option>
<option value="dvd">DVD</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>
Alle ausgeliehen Excel
</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>
@@ -536,15 +591,14 @@
</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 class="modal-content">
<span class="close" onclick="closeDetailModal()">&times;</span>
<div id="detailContent"></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>
// =========================================================================
@@ -1033,7 +1087,7 @@
}
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;
}
@@ -1043,21 +1097,48 @@
return div.innerHTML;
}
// Opens the modal and fetches the data
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}`)
.then(r => r.text())
.then(html => {
document.getElementById('detailContent').innerHTML = html;
document.getElementById('detailModal').style.display = 'flex';
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
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() {
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
// =========================================================================
function wireScannerUi() {
@@ -1192,75 +1273,194 @@
}
});
// Modal Control Functions (accessible globally from table buttons)
function openEditLibraryItem(itemId) {
window.openEditLibraryItem = function(itemId) {
const item = libraryItems.find(i => i._id === itemId);
if (!item) {
alert('Fehler: Das Medium konnte nicht gefunden werden.');
return;
if (!item) return;
// 1. Felder befüllen
document.getElementById('editLibraryItemId').value = item._id;
document.getElementById('editLibraryName').value = item.Name;
document.getElementById('editLibraryType').value = item.ItemType;
document.getElementById('editLibraryIsbn').value = item.ISBN || '';
document.getElementById('editLibraryCode4').value = item.Code_4 || '';
document.getElementById('editLibraryLocation').value = item.Ort;
document.getElementById('editLibraryDescription').value = item.Beschreibung;
// 2. Gruppen-Logik
const warningDiv = document.getElementById('editLibraryGroupWarning');
const codesContainer = document.getElementById('editLibraryAllCodes');
if (item.SeriesGroupId) {
// Filtern aus dem aktuell geladenen Array
let groupMembers = libraryItems.filter(i => i.SeriesGroupId === item.SeriesGroupId);
// 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.textContent = codeList;
}
document.getElementById('editLibraryGroupCount').textContent = groupMembers.length + " / " + (item.SeriesCount || "?");
warningDiv.style.display = 'block';
} else {
warningDiv.style.display = 'none';
}
// Populate the form fields - Using correct German properties from MongoDB fields
document.getElementById('editLibraryItemId').value = item._id || '';
document.getElementById('editLibraryName').value = item.Name || '';
document.getElementById('editLibraryType').value = item.ItemType || 'book';
document.getElementById('editLibraryIsbn').value = item.ISBN || '';
document.getElementById('editLibraryCode4').value = item.Code_4 || item.Code4 || '';
document.getElementById('editLibraryLocation').value = item.Ort || '';
document.getElementById('editLibraryDescription').value = item.Beschreibung || '';
document.getElementById('editLibraryModal').style.display = 'flex';
}
document.getElementById('editLibraryModal').style.display = 'flex';
};
function closeEditLibraryModal() {
document.getElementById('editLibraryModal').style.display = 'none';
}
/**
* Event-Listener für das Formular (Initialisierung)
*/
document.addEventListener('DOMContentLoaded', function() {
const editForm = document.getElementById('editLibraryForm');
if (editForm) {
editForm.addEventListener('submit', async function(e) {
e.preventDefault();
const itemId = document.getElementById('editLibraryItemId').value;
const currentItem = libraryItems.find(i => i._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,
ort: document.getElementById('editLibraryLocation').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 {
const response = await fetch('/update_group', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await response.json();
if (result.success) {
alert('Gruppe erfolgreich synchronisiert!');
closeEditLibraryModal();
await loadLibraryItems(); // Daten neu laden
// renderTable(); // Ggf. Tabelle neu rendern
} else {
alert('Fehler: ' + result.message);
}
} catch (error) {
console.error('Update failed:', error);
alert('Netzwerkfehler.');
}
});
}
});
</script>
<div id="editLibraryModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 760px;">
<span class="close" onclick="closeEditLibraryModal()">&times;</span>
<div class="modal-content" style="max-width: 760px; padding: 25px; border-radius: 8px;">
<span class="close" onclick="closeEditLibraryModal()" style="cursor: pointer; float: right; font-size: 24px;">&times;</span>
<h3 style="margin-top:0;">Bibliotheksmedium bearbeiten</h3>
<!-- Bereich für Gruppen-Informationen (Hier konsolidiert!) -->
<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 aufgeführten Codes gehören zu diesem Datensatz:
</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>
<form id="editLibraryForm">
<input type="hidden" id="editLibraryItemId">
<div class="edit-grid">
<div class="full">
<label for="editLibraryName">Titel</label>
<input id="editLibraryName" required>
<input id="editLibraryName" required style="width: 100%;">
</div>
<div>
<label for="editLibraryType">Medientyp</label>
<select id="editLibraryType">
<option value="book">Buch</option>
<option value="schulbuch">Schulbuch</option>
<select id="editLibraryType" style="width: 100%;">
<option value="Buch">Buch</option>
<option value="Schulbuch">Schulbuch</option>
<option value="cd">CD</option>
<option value="dvd">DVD</option>
<option value="media">Sonstige Medien</option>
<option value="other">Sonstige Medien</option>
</select>
</div>
<div>
<label for="editLibraryIsbn">ISBN</label>
<input id="editLibraryIsbn" placeholder="optional ISBN-10/13">
<input id="editLibraryIsbn" placeholder="optional ISBN-10/13" style="width: 100%;">
</div>
<div>
<label for="editLibraryCode4">Code</label>
<input id="editLibraryCode4" placeholder="optional Mediencode">
<input id="editLibraryCode4" placeholder="optional Mediencode" style="width: 100%;">
</div>
<div class="full">
<label for="editLibraryLocation">Ort</label>
<input id="editLibraryLocation" required>
<input id="editLibraryLocation" required style="width: 100%;">
</div>
<div class="full">
<label for="editLibraryDescription">Beschreibung</label>
<textarea id="editLibraryDescription" rows="4" required></textarea>
<textarea id="editLibraryDescription" rows="4" required style="width: 100%;"></textarea>
</div>
</div>
<div class="edit-actions">
<button type="submit" class="button" style="background:#0ea5e9;color:#fff;">Speichern</button>
<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>
</form>
</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 %}
+1 -1
View File
@@ -122,7 +122,7 @@
<h3>Meldung von Sicherheitslücken</h3>
<div class="license-exception-notice">
<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><!-- /#pane-security -->
+59 -35
View File
@@ -2309,6 +2309,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</script>
<div class="admin-content-container">
{% if current_permissions.actions.get('can_edit', False) %}
<!-- Admin conflict warning banner (populated by JS) -->
<div id="conflict-banner" style="display:none; background:#fff3cd; border:1px solid #ffc107; border-left:4px solid #fd7e14; color:#856404; padding:12px 16px; border-radius:4px; margin-bottom:16px; position:relative;">
<strong>⚠ Buchungskonflikte erkannt:</strong>
@@ -2335,6 +2336,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
.catch(() => {});
});
</script>
{% endif %}
<div class="content">
<h1 style="position:relative;">Inventar Objekte
<div class="view-switch-group">
@@ -2344,14 +2347,17 @@ document.addEventListener('DOMContentLoaded', ()=>{
<button id="toggle-favorites-view" class="view-toggle-btn" title="Nur Merkliste anzeigen">
<span id="favorites-view-icon">🔖</span>
</button>
{% if current_permissions.actions.get('can_delete', False) %}
<div class="bulk-delete-inline-wrap">
<button type="button" id="bulk-delete-drawer-toggle" class="view-toggle-btn bulk-delete-drawer-toggle" aria-expanded="false" title="Massenlöschung" onclick="toggleBulkDeleteDrawer()">
<span class="drawer-icon"></span>
</button>
</div>
{% endif %}
</div>
</h1>
<div id="items-indicator" class="items-indicator">Objekte im System: 0</div>
{% if current_permissions.actions.get('can_delete', False) %}
<div id="bulk-delete-drawer" class="bulk-delete-drawer" aria-hidden="true">
<div class="bulk-delete-content">
<strong>Massenlöschung nach Kriterien</strong>
@@ -2364,6 +2370,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
<button type="button" id="bulk-delete-button" class="danger" onclick="deleteSelectedItems()" disabled>Auswahl löschen</button>
</div>
</div>
{% endif %}
<div class="filter-container">
<div class="filter-group">
<div class="filter-header">
@@ -2469,6 +2476,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
<!-- Add the edit modal form -->
{% if current_permissions.actions.get('can_edit', False) %}
<div id="edit-modal" class="item-modal">
<div class="modal-content">
<span class="close-modal" onclick="closeEditModal()">&times;</span>
@@ -2646,6 +2654,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</form>
</div>
</div>
{% endif %}
</div>
<!-- Schedule Appointment Modal -->
@@ -2724,7 +2733,6 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
</div>
<!-- Initialize template variables before any JavaScript code -->
<script>
// Create a global object to hold server-side template values
window.serverVars = {};
@@ -3239,7 +3247,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
return;
}
fetch('{{ url_for('bulk_delete_items') }}', {
fetch("{{ url_for('bulk_delete_items') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -3703,6 +3711,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
Für Löschung markieren
</label>
</div>
{% if current_permissions.actions.get('can_borrow', False) %}
${isAvailableForBorrow && !item.BlockedNow ?
`<form method="POST" action="{{ url_for('ausleihen', id='') }}${item._id}">
${isGroupedItem ? `
@@ -3724,12 +3733,21 @@ document.addEventListener('DOMContentLoaded', ()=>{
:
`<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
}
{% endif %}
{% if current_permissions.actions.get('can_delete', False) %}
<form method="POST" action="{{ url_for('delete_item', id='') }}${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher, dass Sie dieses Objekt löschen möchten?')">
<button class="delete-button" type="submit">Löschen</button>
</form>
{% endif %}
{% if current_permissions.actions.get('can_edit', False) %}
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-card-${item._id}')">Bearbeiten</button>
{% endif %}
{% if current_permissions.actions.get('can_insert', False) %}
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
{% endif %}
{% if current_permissions.pages.get('admin_school_settings', False) %}
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
{% endif %}
</div>
`;
itemsContainer.appendChild(card);
@@ -4506,10 +4524,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
: '<div style="font-size:0.92rem;color:#64748b;">Keine Beschädigungs-Historie vorhanden.</div>';
modalContent.innerHTML = `
<h2>${item.Name}</h2>
<h2>${escapeHtml(item.Name || '')}</h2>
${borrowerInfoHtml}
${appointmentInfoHtml}
<div class="modal-image-container">
${imagesHtml}
${item.Images && item.Images.length > 1 ? `
@@ -4518,66 +4536,67 @@ document.addEventListener('DOMContentLoaded', ()=>{
<div class="image-counter">1/${item.Images.length}</div>
` : ''}
</div>
<div class="modal-details">
<div class="detail-group">
<div class="detail-label">Ort:</div>
<div class="detail-value">${item.Ort || '-'}</div>
<div class="detail-value">${escapeHtml(item.Ort || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Status:</div>
<div class="detail-value ${isBorrowed ? 'status-borrowed' : ''}">
${isBorrowed ? 'Ausgeliehen' : 'Verfügbar'}
</div>
</div>
<div class="detail-group">
<div class="detail-label">Unterrichtsfach:</div>
<div class="detail-value">${filter1Array.join(', ') || '-'}</div>
<div class="detail-value">${escapeHtml(filter1Array.join(', ') || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Jahrgangsstufe:</div>
<div class="detail-value">${filter2Array.join(', ') || '-'}</div>
<div class="detail-value">${escapeHtml(filter2Array.join(', ') || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Schlagwort:</div>
<div class="detail-value">${filter3Array.join(', ') || '-'}</div>
<div class="detail-value">${escapeHtml(filter3Array.join(', ') || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Code:</div>
<div class="detail-value">${item.Code_4 || '-'}</div>
<div class="detail-value">${escapeHtml(item.Code_4 || '-')}</div>
</div>
<div class="detail-group">
<div class="detail-label">Anzahl:</div>
<div class="detail-value">${item.GroupedDisplayCount || 1}</div>
<div class="detail-value">${escapeHtml(String(item.GroupedDisplayCount || 1))}</div>
</div>
${isGroupedItem ? `
<div class="detail-group">
<div class="detail-label">Verfügbar:</div>
<div class="detail-value">${availableGroupedCount}</div>
<div class="detail-value">${escapeHtml(String(availableGroupedCount))}</div>
</div>` : ''}
<div class="detail-group">
<div class="detail-label">Anschaffungsjahr:</div>
<div class="detail-value">${item.Anschaffungsjahr || '-'}</div>
</div>
<div class="detail-group">
<div class="detail-label">Anschaffungskosten:</div>
<div class="detail-value">${item.Anschaffungskosten ? item.Anschaffungskosten + ' €' : '-'}</div>
</div>
<div class="detail-group full-width">
<div class="detail-label">Beschreibung:</div>
<div class="detail-value">${item.Beschreibung || '-'}</div>
<div class="detail-value">${escapeHtml(String(item.Anschaffungsjahr || '-'))}</div>
</div>
<div class="detail-group">
<div class="detail-label">Anschaffungskosten:</div>
<div class="detail-value">${item.Anschaffungskosten ? escapeHtml(String(item.Anschaffungskosten)) + ' €' : '-'}</div>
</div>
<div class="detail-group full-width">
<div class="detail-label">Beschreibung:</div>
<div class="detail-value">${escapeHtml(item.Beschreibung || '-')}</div>
</div>
{% if current_permissions.actions.get('can_view_logs', False) %}
<div class="detail-group full-width" style="margin-top:12px;">
<div class="detail-label" style="font-weight:600; color:#374151;">Beschädigungs-Historie</div>
<div class="detail-value">
@@ -4590,7 +4609,8 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
</div>
</div>
{% endif %}
<div class="detail-group full-width" style="margin-top:12px; padding:10px; border:1px solid #e3e3e3; border-radius:8px;">
<div class="detail-label">Verfügbarkeit prüfen:</div>
<div class="detail-value">
@@ -4611,7 +4631,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
</div>
</div>
<div class="detail-group full-width" style="margin-top:15px;">
<div class="detail-label">Geplante Ausleihen:</div>
<div class="detail-value">
@@ -4635,9 +4655,10 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>
</div>
</div>
<div class="modal-actions">
${!isBorrowed ?
{% if current_permissions.actions.get('can_borrow', False) %}
`<form method="POST" action="/ausleihen/${item._id}">
${isGroupedItem ? `
<div style="display:flex; gap:8px; flex-wrap:wrap; margin-bottom:8px; align-items:center;">
@@ -4651,6 +4672,9 @@ document.addEventListener('DOMContentLoaded', ()=>{
</div>` : ''}
<button class="ausleihen" type="submit">Ausleihen</button>
</form>`
{% else %}
`<button class="ausleihen disabled-button" disabled title="Keine Berechtigung">Ausleihen nicht möglich</button>`
{% endif %}
: (!isGroupedItem && item.User === currentUsername) ?
`<form method="POST" action="/zurueckgeben/${item._id}">
<button class="ausleihen" type="submit">Zurückgeben</button>
@@ -4659,7 +4683,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
}
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-modal-${item._id}')">Bearbeiten</button>
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
${damageReports.length > 0 ? `<button class="damage-button" onclick="markDamageAsRepaired('${item._id}')">Repariert</button>` : `<button class="damage-button" onclick="registerDamage('${item._id}')">Schaden melden</button>`}
${damageReports && damageReports.length > 0 ? `<button class="damage-button" onclick="markDamageAsRepaired('${item._id}')">Repariert</button>` : `<button class="damage-button" onclick="registerDamage('${item._id}')">Schaden melden</button>`}
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
<form method="POST" action="/delete_item/${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher?')">
<button class="delete-button" type="submit">Löschen</button>
+133 -83
View File
@@ -1,11 +1,3 @@
<!--
Copyright 2025-2026 AIIrondev
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
See Legal/LICENSE for the full license text.
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
For commercial licensing inquiries: https://github.com/AIIrondev
-->
{% extends "base.html" %}
{% block title %}Register{% endblock %}
@@ -33,6 +25,7 @@
<div class="content">
<div class="form-card">
<form method="POST" action="{{ url_for('register') }}">
<div class="form-group">
<label for="name">Vorname</label>
<div class="input-container">
@@ -44,7 +37,7 @@
<span class="input-icon">👤</span>
<input type="text" id="last-name" name="last-name" placeholder="Geben Sie den Nachnamen ein" required onchange="generateUsername()" oninput="generateUsername()">
</div>
<label for="username">Benutzername <span style="color: #9ca3af;">(wird automatisch generiert)</span></label>
<label for="username">Benutzername <span style="color: #9ca3af;">(Vorschau - wird serverseitig finalisiert)</span></label>
<div class="input-container">
<span class="input-icon">👤</span>
<input type="text" id="username" name="username" placeholder="Automatisch aus Name und Nachname" readonly style="background-color: #f3f4f6; cursor: not-allowed;">
@@ -64,9 +57,21 @@
<li id="pw-rule-symbol" class="pw-rule">Mindestens ein Sonderzeichen</li>
</ul>
</div>
<div class="input-container">
<span class="input-icon">🔒</span>
<input type="password" id="password" name="password" placeholder="Geben Sie ein sicheres Passwort ein" required>
<div class="input-wrapper">
<div class="input-container">
<span class="input-icon">🔒</span>
<!-- HTML5 Pattern blockiert unsichere Passwörter vor dem Absenden -->
<input
id="password"
name="password"
placeholder="Geben Sie ein sicheres Passwort ein"
required
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{12,}">
</div>
</div>
<div class="pw-actions">
<button type="button" class="btn-secondary" onclick="generateSecurePassword()">Passwort generieren</button>
</div>
</div>
@@ -258,31 +263,6 @@ input::placeholder {
background-color: #27ae60;
}
.navigation-buttons {
text-align: center;
margin: 1.5rem 0;
}
.back-button {
display: inline-flex;
align-items: center;
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
transition: var(--transition);
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
}
.back-button:hover {
background-color: rgba(52, 152, 219, 0.1);
}
.back-icon {
margin-right: 0.5rem;
font-size: 1.2rem;
}
.flash-container {
margin-bottom: 1.5rem;
}
@@ -422,49 +402,147 @@ input::placeholder {
margin: 4px 0;
color: #1f2937;
}
/* Neue Styles für die Passwort-Erweiterungen */
.pw-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.btn-secondary {
padding: 8px 12px;
border: 1px solid #d1d5db;
background-color: #f3f4f6;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
}
.btn-secondary:hover {
background-color: #e5e7eb;
}
.input-wrapper {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.input-wrapper .input-container {
flex-grow: 1;
margin: 0;
}
</style>
<script>
// Function to generate username from first and last name (helper function)
// Hilfsfunktion: Umlaute auflösen und Sonderzeichen entfernen
function cleanNameForUsername(text) {
if (!text) return '';
// Remove special characters, convert umlauts, lowercase
let cleaned = text
.replace(/[^a-zA-Zäöüß\s-]/g, '')
.trim()
.toLowerCase();
// Convert German umlauts to ASCII
cleaned = cleaned
let cleaned = text.trim().toLowerCase()
.replace(/ä/g, 'ae')
.replace(/ö/g, 'oe')
.replace(/ü/g, 'ue')
.replace(/ß/g, 'ss');
.replace(/ß/g, 'ss')
.replace(/[^a-z]/g, '');
return cleaned;
}
// Generate username from name and last_name fields
// Hilfsfunktion: Erster Buchstabe groß
function formatPart(str, len) {
if (!str) return '';
const part = str.slice(0, len);
return part.charAt(0).toUpperCase() + part.slice(1);
}
// Generiert die Vorschau des Benutzernamens (z.B. SimFri)
function generateUsername() {
const firstName = cleanNameForUsername(document.getElementById('name').value || '');
const lastName = cleanNameForUsername(document.getElementById('last-name').value || '');
const firstName = cleanNameForUsername(document.getElementById('name').value);
const lastName = cleanNameForUsername(document.getElementById('last-name').value);
let username = '';
if (firstName && lastName) {
username = (firstName.slice(0, 3) + lastName.slice(0, 3));
username = formatPart(firstName, 3) + formatPart(lastName, 3);
} else if (firstName) {
username = firstName.slice(0, 6);
username = formatPart(firstName, 6);
} else if (lastName) {
username = lastName.slice(0, 6);
username = formatPart(lastName, 6);
}
// Set the username field
const usernameField = document.getElementById('username');
if (usernameField) {
usernameField.value = username || '';
}
}
// PASSWORT GENERATOR
function generateSecurePassword() {
const length = 16;
const charsetLower = "abcdefghijklmnopqrstuvwxyz";
const charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const charsetNum = "0123456789";
const charsetSym = "!@#$%^&*()_+~|}{[]:;?><,.-=";
let password = "";
// Garantiert mindestens 1 Zeichen aus jeder Kategorie
password += charsetLower[Math.floor(Math.random() * charsetLower.length)];
password += charsetUpper[Math.floor(Math.random() * charsetUpper.length)];
password += charsetNum[Math.floor(Math.random() * charsetNum.length)];
password += charsetSym[Math.floor(Math.random() * charsetSym.length)];
const allChars = charsetLower + charsetUpper + charsetNum + charsetSym;
// Restliche Zeichen auffüllen
for (let i = 4; i < length; i++) {
password += allChars[Math.floor(Math.random() * allChars.length)];
}
// Passwort durchmischen
password = password.split('').sort(() => 0.5 - Math.random()).join('');
const pwField = document.getElementById('password');
pwField.value = password;
// Automatisch sichtbar machen, damit der User es kopieren kann
pwField.type = 'text';
document.getElementById('toggle-pw-btn').textContent = '🙈';
updatePasswordRules();
}
// PASSWORT SICHTBARKEIT UMSCHALTEN
function togglePasswordVisibility() {
const pwField = document.getElementById('password');
const toggleBtn = document.getElementById('toggle-pw-btn');
if (pwField.type === "password") {
pwField.type = "text";
toggleBtn.textContent = '🙈';
} else {
pwField.type = "password";
toggleBtn.textContent = '👁️';
}
}
// Globale Funktion für Passwort-Update
function updatePasswordRules() {
const passwordInput = document.getElementById('password');
if (!passwordInput) return;
const value = String(passwordInput.value || '');
const setRuleState = (id, ok) => {
const node = document.getElementById(id);
if (node) node.classList.toggle('ok', !!ok);
};
setRuleState('pw-rule-length', value.length >= 12);
setRuleState('pw-rule-lower', /[a-z]/.test(value));
setRuleState('pw-rule-upper', /[A-Z]/.test(value));
setRuleState('pw-rule-digit', /[0-9]/.test(value));
setRuleState('pw-rule-symbol', /[^A-Za-z0-9]/.test(value));
}
document.addEventListener('DOMContentLoaded', function () {
const permissionPresets = {{ permission_presets | tojson }};
const presetSelect = document.getElementById('permission-preset');
@@ -507,34 +585,6 @@ document.addEventListener('DOMContentLoaded', function () {
}
const passwordInput = document.getElementById('password');
const passwordRules = {
length: document.getElementById('pw-rule-length'),
lower: document.getElementById('pw-rule-lower'),
upper: document.getElementById('pw-rule-upper'),
digit: document.getElementById('pw-rule-digit'),
symbol: document.getElementById('pw-rule-symbol')
};
function setRuleState(node, ok) {
if (!node) {
return;
}
node.classList.toggle('ok', !!ok);
}
function updatePasswordRules() {
if (!passwordInput) {
return;
}
const value = String(passwordInput.value || '');
setRuleState(passwordRules.length, value.length >= 12);
setRuleState(passwordRules.lower, /[a-z]/.test(value));
setRuleState(passwordRules.upper, /[A-Z]/.test(value));
setRuleState(passwordRules.digit, /[0-9]/.test(value));
setRuleState(passwordRules.symbol, /[^A-Za-z0-9]/.test(value));
}
if (passwordInput) {
passwordInput.addEventListener('input', updatePasswordRules);
passwordInput.addEventListener('blur', updatePasswordRules);
+50 -97
View File
@@ -3,7 +3,12 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="docker-compose-multitenant.yml"
# Directories
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}"
MODE="auto"
BASE_NAME=""
@@ -14,9 +19,7 @@ Usage: $0 [options]
Options:
--invoice-archive-dir DIR Directory for invoice archive files
(default: $INVOICE_ARCHIVE_DIR)
--invoice-keep-days N Remove archived invoice files older than N days
(default: $KEEP_DAYS)
--invoice-keep-days N Remove archived files older than N days
--mode auto|docker|host Backup mode (default: auto)
--base-name NAME Base filename prefix for archive files
-h, --help Show this help message
@@ -26,127 +29,78 @@ EOF
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--invoice-archive-dir)
INVOICE_ARCHIVE_DIR="$2"
shift 2
;;
--invoice-keep-days)
KEEP_DAYS="$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
;;
--invoice-archive-dir) INVOICE_ARCHIVE_DIR="$2"; shift 2 ;;
--invoice-keep-days) KEEP_DAYS="$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
done
}
ensure_directory() {
mkdir -p "$INVOICE_ARCHIVE_DIR"
mkdir -p "$FULL_BACKUP_DIR"
}
cleanup_old_archives() {
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
}
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"
# 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
}
docker_backup() {
if ! command -v docker >/dev/null 2>&1; then
return 1
fi
if ! docker compose -f "$COMPOSE_FILE" ps -q app >/dev/null 2>&1; then
return 1
if ! command -v docker >/dev/null 2>&1; then return 1; fi
local timestamp="$(date +'%Y-%m-%d_%H-%M-%S')"
local staging_dir="/tmp/backup_stage_$timestamp"
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
local app_container
app_container="$(docker compose -f "$COMPOSE_FILE" ps -q app | tr -d '[:space:]')"
if [[ -z "$app_container" ]]; then
return 1
# 2. Archive Assets (into staging)
if [ -d "./Web/uploads" ]; then
echo "[$(date +'%T')] Archiving assets..."
tar -czf "$staging_dir/assets.tar.gz" -C . Web/uploads Web/thumbnails Web/previews 2>/dev/null || true
fi
local timestamp
timestamp="$(date +'%Y-%m-%d_%H-%M-%S')"
local output_dir
local remote_base
# 3. Archive Logs (into staging)
if [ -d "$LOG_DIR" ]; then
echo "[$(date +'%T')] Archiving logs..."
tar -czf "$staging_dir/logs.tar.gz" -C "$(dirname "$LOG_DIR")" "$(basename "$LOG_DIR")"
fi
output_dir="/tmp/invoice_archive_${timestamp}"
remote_base="${output_dir}/${BASE_NAME}"
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 \
--archive-dir "$output_dir" \
--base-name "$BASE_NAME"
mkdir -p "$INVOICE_ARCHIVE_DIR"
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"
# 4. Bundle everything into one file
echo "[$(date +'%T')] Bundling archive..."
tar -czf "$FULL_BACKUP_DIR/full_backup_$timestamp.tar.gz" -C "$staging_dir" .
# 5. Cleanup staging
rm -rf "$staging_dir"
echo "[$(date +'%T')] Backup complete: $FULL_BACKUP_DIR/full_backup_$timestamp.tar.gz"
return 0
}
main() {
if [[ -z "$BASE_NAME" ]]; then
BASE_NAME="invoices-$(date +'%Y-%m-%d_%H-%M-%S')"
fi
parse_args "$@"
ensure_directory
case "$MODE" in
auto)
auto|docker)
if docker_backup; then
cleanup_old_archives
return 0
fi
if host_backup; then
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
echo "Error: Docker backup failed." >&2
return 1
;;
*)
@@ -157,5 +111,4 @@ main() {
esac
}
parse_args "$@"
main
main "$@"
+1 -1
View File
@@ -27,7 +27,7 @@
"username": "",
"password": "",
"from_address": "",
"default_sender_name": "Invario Inventurprogramm",
"default_sender_name": "Invario Inventarprogramm",
"timeout_seconds": 30
},
"images": {
+15 -14
View File
@@ -8,8 +8,8 @@ else
INSTALLER_DIR="$(pwd)"
fi
PROJECT_DIR="/opt/Inventarsystem"
REPO_SLUG="AIIrondev/legendary-octo-garbanzo"
API_URL="https://api.github.com/repos/$REPO_SLUG/releases/latest"
REPO_SLUG="Invario/Inventarsystem"
API_URL="https://git.invario-software.eu/api/v1/repos/$REPO_SLUG/releases/latest"
BUNDLE_ASSET="inventarsystem-docker-bundle.tar.gz"
APP_IMAGE_ASSET_PREFIX="inventarsystem-image-"
LEGACY_DB_NAME="Inventarsystem"
@@ -39,7 +39,7 @@ need_cmd() {
refresh_start_script_from_main() {
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
sudo install -m 755 "$TMP_DIR/start.sh" "$PROJECT_DIR/start.sh"
@@ -62,7 +62,7 @@ import re
import sys
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:
lines = f.readlines()
@@ -173,9 +173,9 @@ Usage: $0 [options]
Options:
--migrate-legacy-db Back up host MongoDB and import into Docker MongoDB
--remove-legacy-system Remove old host MongoDB/system after successful import
--skip-cleanup-old Do not run cleanup-old.sh after install
--cleanup-old-remove-cron Also remove matching cron entries during old-system cleanup
--multitenant Use docker-compose-multitenant.yml (default)
--skip-cleanup-old Do not run cleanup-old.sh after install
--cleanup-old-remove-cron Also remove matching cron entries during old-system cleanup
--multitenant Use docker-compose-multitenant.yml (default)
--legacy-db-name <name> Legacy database name (default: $LEGACY_DB_NAME)
--legacy-mongo-uri <uri> Legacy Mongo URI (default: $LEGACY_MONGO_URI)
--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"
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
echo "Error: release bundle is missing start.sh"
@@ -470,19 +471,19 @@ EOF
NUITKA_BUILD=0
INVENTAR_HTTP_PORT=10000
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
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
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
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
backup_legacy_database
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
cleanup_old_services
@@ -492,4 +493,4 @@ EOF
echo "Open: https://localhost"
}
main "$@"
main "$@"
+187
View File
@@ -880,6 +880,193 @@ PY
fi
;;
backup)
TENANT_ID="${2:-}"
if [ -z "$TENANT_ID" ]; then
echo "Error: Please provide a tenant_id."
exit 1
fi
OUTPUT_FILE="${3:-backup_${TENANT_ID}_$(date +%Y%m%d_%H%M%S).zip}"
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
if [ -z "$APP_CONTAINER" ]; then
echo "Error: Application container not running."
exit 1
fi
echo "Creating complete backup (.zip) for tenant '$TENANT_ID'..."
# We generate the zip temporarily inside the container
CONTAINER_TMP_ZIP="/tmp/backup_${TENANT_ID}_$(date +%s).zip"
docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" "$CONTAINER_TMP_ZIP" <<'PY'
import sys, os, zipfile
from bson import json_util
sys.path.insert(0, '/app')
sys.path.insert(0, '/app/Web')
from Web.modules.database import settings
from pymongo import MongoClient
tenant_id = sys.argv[1]
zip_path = sys.argv[2]
# CHANGE THIS if your app stores files in a different directory
UPLOADS_BASE = f"/app/uploads/{tenant_id}"
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
db_name = f"inventar_{sanitized}"
try:
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
db = client[db_name]
dump = {}
for coll_name in db.list_collection_names():
if coll_name.startswith('system.'):
continue
dump[coll_name] = list(db[coll_name].find())
db_json = json_util.dumps(dump)
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
# 1. Write the database dump
zf.writestr('database.json', db_json)
# 2. Add uploaded photos/invoices if the directory exists
if os.path.isdir(UPLOADS_BASE):
for root, dirs, files in os.walk(UPLOADS_BASE):
for file in files:
file_path = os.path.join(root, file)
# Create a relative path for the zip internal structure
arcname = os.path.relpath(file_path, start=UPLOADS_BASE)
zf.write(file_path, arcname=f"uploads/{arcname}")
except Exception as e:
print(f"Error creating backup: {e}", file=sys.stderr)
sys.exit(1)
PY
if [ $? -eq 0 ]; then
# Copy the zip out of the container to the host
docker cp "$APP_CONTAINER:$CONTAINER_TMP_ZIP" "$OUTPUT_FILE"
# Cleanup inside the container
docker exec "$APP_CONTAINER" rm -f "$CONTAINER_TMP_ZIP"
echo "Backup successfully created: $OUTPUT_FILE"
else
echo "Error: Backup failed."
docker exec "$APP_CONTAINER" rm -f "$CONTAINER_TMP_ZIP"
exit 1
fi
;;
restore)
TENANT_ID="${2:-}"
INPUT_FILE="${3:-}"
if [ -z "$TENANT_ID" ] || [ -z "$INPUT_FILE" ]; then
echo "Error: Usage: $0 restore <tenant_id> <backup_file.zip>"
exit 1
fi
if [ ! -f "$INPUT_FILE" ]; then
echo "Error: Backup file '$INPUT_FILE' not found."
exit 1
fi
echo -n "WARNING: This will DROP the existing database and completely overwrite data AND files for tenant '$TENANT_ID'. Are you sure? (y/N) "
read confirm
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
echo "Restore canceled."
exit 0
fi
APP_CONTAINER=$(docker ps -qf "name=app" | head -n 1)
if [ -z "$APP_CONTAINER" ]; then
echo "Error: Application container not running."
exit 1
fi
echo "Uploading backup archive to container..."
CONTAINER_TMP_ZIP="/tmp/restore_${TENANT_ID}_$(date +%s).zip"
docker cp "$INPUT_FILE" "$APP_CONTAINER:$CONTAINER_TMP_ZIP"
echo "Restoring database and files for tenant '$TENANT_ID'..."
docker exec "$APP_CONTAINER" python3 - "$TENANT_ID" "$CONTAINER_TMP_ZIP" <<'PY'
import sys, os, zipfile, shutil
from bson import json_util
sys.path.insert(0, '/app')
sys.path.insert(0, '/app/Web')
from Web.modules.database import settings
from pymongo import MongoClient
tenant_id = sys.argv[1]
zip_path = sys.argv[2]
# CHANGE THIS if your app stores files in a different directory
UPLOADS_BASE = f"/app/uploads/{tenant_id}"
sanitized = "".join(c for c in tenant_id if c.isalnum() or c == "_")
db_name = f"inventar_{sanitized}"
try:
with zipfile.ZipFile(zip_path, 'r') as zf:
# 1. Restore Database
db_json = zf.read('database.json').decode('utf-8')
data = json_util.loads(db_json)
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
client.drop_database(db_name)
db = client[db_name]
restored_count = 0
for coll_name, docs in data.items():
if docs:
db[coll_name].insert_many(docs)
restored_count += len(docs)
# 2. Restore Files
# Wipe existing uploads to ensure an exact replica of the backup
if os.path.exists(UPLOADS_BASE):
shutil.rmtree(UPLOADS_BASE)
for item in zf.namelist():
# Extract only file items that were saved under 'uploads/'
if item.startswith('uploads/') and not item.endswith('/'):
rel_path = item[len('uploads/'):]
target_path = os.path.join(UPLOADS_BASE, rel_path)
os.makedirs(os.path.dirname(target_path), exist_ok=True)
with open(target_path, 'wb') as f:
f.write(zf.read(item))
print(f"Successfully restored {restored_count} database documents and tenant files.")
except Exception as e:
print(f"Error restoring backup: {e}", file=sys.stderr)
sys.exit(1)
PY
if [ $? -eq 0 ]; then
# Clean up tmp zip inside container
docker exec "$APP_CONTAINER" rm -f "$CONTAINER_TMP_ZIP"
echo "Restore completed successfully."
echo "Clearing session cache..."
docker exec "$APP_CONTAINER" python3 -c "
import sys; sys.path.insert(0, '/app'); sys.path.insert(0, '/app/Web'); from Web.modules.database import settings; from pymongo import MongoClient
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))
db = client[f'inventar_{"".join(c for c in sys.argv[1] if c.isalnum() or c == "_")}']
db.sessions.drop()
" "$TENANT_ID"
else
echo "Error: Restore failed."
docker exec "$APP_CONTAINER" rm -f "$CONTAINER_TMP_ZIP"
exit 1
fi
;;
*)
echo "Unknown command: $COMMAND"
show_help
+1
View File
@@ -1,4 +1,5 @@
flask
flask-wtf
werkzeug
gunicorn
pymongo==4.6.3
+2
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
sudo find /tmp -maxdepth 1 -name "tmp.*" -exec rm -rf {} +
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
cd "$SCRIPT_DIR"
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"
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"
RUNTIME_COMPOSE_OVERRIDE_FILE="$SCRIPT_DIR/.docker-compose.runtime.override.yml"
@@ -676,4 +676,4 @@ fi
verify_stack_health
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
# 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)"
LOG_DIR="$PROJECT_DIR/logs"
LOG_FILE="$LOG_DIR/update.log"
STATE_FILE="$PROJECT_DIR/.release-version"
REPO_SLUG="AIIrondev/legendary-octo-garbanzo"
API_URL="https://api.github.com/repos/$REPO_SLUG/releases/latest"
REPO_SLUG="Invario/Inventarsystem"
API_URL="https://git.invario-software.eu/api/v1/repos/$REPO_SLUG/releases/latest"
BUNDLE_ASSET="inventarsystem-docker-bundle.tar.gz"
APP_IMAGE_ASSET_PREFIX="inventarsystem-image-"
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"
COMPOSE_FILE="docker-compose-multitenant.yml"
MIN_ROOT_FREE_MB="${INVENTAR_MIN_ROOT_FREE_MB:-2048}"
@@ -153,7 +153,7 @@ Usage: $0 [options]
Options:
--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
EOF
}
@@ -204,14 +204,14 @@ create_backup() {
fi
fi
if [ -x "$PROJECT_DIR/run-backup.sh" ]; then
if "$PROJECT_DIR/run-backup.sh" >> "$LOG_FILE" 2>&1; then
if [ -x "$PROJECT_DIR/backup.sh" ]; then
if "$PROJECT_DIR/backup.sh" --mode auto >> "$LOG_FILE" 2>&1; then
log_message "Backup completed"
else
log_message "WARNING: Backup failed; continuing with release update"
fi
else
log_message "WARNING: run-backup.sh not found; skipping backup"
log_message "WARNING: backup.sh not found; skipping backup"
fi
}
@@ -275,10 +275,10 @@ load_release_image() {
refresh_runtime_scripts_from_main() {
local start_url stop_url restart_url update_url
start_url="https://raw.githubusercontent.com/$REPO_SLUG/main/start.sh"
stop_url="https://raw.githubusercontent.com/$REPO_SLUG/main/stop.sh"
restart_url="https://raw.githubusercontent.com/$REPO_SLUG/main/restart.sh"
update_url="https://raw.githubusercontent.com/$REPO_SLUG/main/update.sh"
start_url="https://git.invario-software.eu/$REPO_SLUG/raw/branch/main/start.sh"
stop_url="https://git.invario-software.eu/$REPO_SLUG/raw/branch/main/stop.sh"
restart_url="https://git.invario-software.eu/$REPO_SLUG/raw/branch/main/restart.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 "$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"
fi
# Neu: Multi-Tenant Ressourcen kopieren, falls vorhanden
# Multi-Tenant Ressourcen kopieren, falls vorhanden
if [ -f "$tmp_dir/docker-compose-multitenant.yml" ]; then
cp -f "$tmp_dir/docker-compose-multitenant.yml" "$PROJECT_DIR/docker-compose-multitenant.yml"
fi
@@ -374,7 +374,7 @@ download_and_extract_bundle() {
done
# Ensure executable permissions on all copied scripts
chmod +x "$PROJECT_DIR/start.sh" "$PROJECT_DIR/stop.sh" "$PROJECT_DIR/restart.sh" "$PROJECT_DIR/update.sh" "$PROJECT_DIR/backup.sh" "$PROJECT_DIR/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
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_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
log_message "Falling back to local Docker build for $app_image"
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"
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
log_message "Attempting to pull development image $app_image"
if ! docker pull "$app_image" >> "$LOG_FILE" 2>&1; then
@@ -540,7 +540,7 @@ EOF
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
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
@@ -621,6 +621,9 @@ EOF
cleanup_old_dist_artifacts
cleanup_docker_dangling_images
log_message "Update completed successfully to release $latest_tag"
sudo ./opt/Inventarsystem/restart.sh
echo "Restart of the Server Completed"
}
main "$@"
main "$@"