add: Installer funktionen mit auto Update check

This commit is contained in:
Rob committed 2026-05-08 14:12:03 +02:00
1 parent 102b485794
commit 1fb6444444
8 files changed
+422 -10

No files matched your search

+35
View File
@@ -61,3 +61,38 @@ Excel-Datei → excel_reader → MonthlyAttendance → pdf_filler → PDF-Datei
| Numerisch | DD.MM.YYYY | X | Wert (dt. Format) | "6" | "1" | | Numerisch | DD.MM.YYYY | X | Wert (dt. Format) | "6" | "1" |
| "K"/"KK"/etc| DD.MM.YYYY | Code | leer | leer | "0" | | "K"/"KK"/etc| DD.MM.YYYY | Code | leer | leer | "0" |
| Leer | (überspringen) | — | — | — | — | | Leer | (überspringen) | — | — | — | — |
---
## Windows-Installer (Inno Setup)
### Ziel
Installer-EXE erstellen, die:
- Von einem Windows SMB-Share aus gestartet werden kann (self-contained .exe)
- Das Programm lokal unter `%ProgramFiles%\AnwesenheitPDF\` installiert
- Einen Startmenü-Eintrag anlegt (optional: Desktop-Verknüpfung)
- Einen Uninstaller registriert (Programme und Features)
### Tool: Inno Setup 6
- Free, open-source, weit verbreitet
- Erzeugt eine einzelne selbstextrahierende .exe (ideal für SMB-Share)
- Unterstützt German.isl (deutschsprachiger Wizard)
- ISCC.exe (Compiler) muss auf Build-Maschine installiert sein
- Installation: `winget install --id JRSoftware.InnoSetup -e -s winget -i`
### Dateien
- `installer\AnwesenheitPDF.iss` — Inno Setup Script
- `build.bat` — um Installer-Schritt erweitert (ruft ISCC auf)
- Ausgabe: `dist\AnwesenheitPDF-Setup-x.y.z.exe`
### Ablauf build.bat (neu: Schritt 4)
1. pip install
2. update_version.py (liest Version aus config.json)
3. PyInstaller → dist\AnwesenheitPDF\
4. Version aus config.json extrahieren → ISCC aufrufen → dist\AnwesenheitPDF-Setup-x.y.z.exe
### Install-Ziel
- `{autopf}\AnwesenheitPDF\` (= Program Files, UAC-geschützt)
- Requires admin (PrivilegesRequired=admin)
- Startmenü: `{group}\AnwesenheitPDF`
- Optional Desktop-Verknüpfung (Checkbox im Wizard)
+1
View File
@@ -13,3 +13,4 @@ tests/fixtures/**/*.xlsx
tests/fixtures/**/*.xlsm tests/fixtures/**/*.xlsm
tests/fixtures/**/reference/*.pdf tests/fixtures/**/reference/*.pdf
tests/fixtures/**/diff/ tests/fixtures/**/diff/
*.log
+45
View File
@@ -198,3 +198,48 @@ Das Programm prüft vor jeder Generierung:
7. Headerzeile mit `Name, Vorname` gefunden 7. Headerzeile mit `Name, Vorname` gefunden
8. Tagesspalten ab Spaltenindex 9 vorhanden 8. Tagesspalten ab Spaltenindex 9 vorhanden
9. PDF-Template unter `template/sab.pdf` erreichbar 9. PDF-Template unter `template/sab.pdf` erreichbar
---
## Windows-Installer erstellen
Der Build-Prozess erzeugt neben dem Programm auch einen Windows-Installer (`.exe`), der vom SMB-Netzlaufwerk gestartet werden kann.
### Voraussetzung: Inno Setup 6
Inno Setup 6 muss auf der Build-Maschine installiert sein. Installation über `winget`:
```bat
winget install --id JRSoftware.InnoSetup -e -s winget -i
```
Alternativ manueller Download: <https://jrsoftware.org/isinfo.php>
### Installer bauen
```bat
build.bat
```
`build.bat` führt alle vier Schritte automatisch aus:
1. Abhängigkeiten installieren (`pip install`)
2. Version aktualisieren (`update_version.py`)
3. Programm packen (`PyInstaller` → `dist\AnwesenheitPDF\`)
4. Installer kompilieren (`ISCC.exe` → `dist\AnwesenheitPDF-Setup-x.y.z.exe`)
Ist Inno Setup **nicht** installiert, wird Schritt 4 mit einer Warnung übersprungen — der PyInstaller-Output in `dist\AnwesenheitPDF\` ist trotzdem nutzbar.
### Installer verteilen
Die fertige Datei `dist\AnwesenheitPDF-Setup-x.y.z.exe` auf das SMB-Netzlaufwerk kopieren. Von dort kann sie direkt gestartet werden — ohne vorherige lokale Kopie.
### Was der Installer macht
| Aktion | Details |
|---|---|
| Installationspfad | `%ProgramFiles%\AnwesenheitPDF\` (Admin erforderlich) |
| Startmenü-Eintrag | `AnwesenheitPDF` |
| Desktop-Verknüpfung | Optional (Checkbox im Wizard, Standard: deaktiviert) |
| Uninstaller | Registriert unter *Programme und Features* |
| Windows-Version | Windows 10 oder höher |
+102 -7
View File
@@ -1,12 +1,14 @@
@echo off @echo off
REM Build-Script fuer AnwesenheitPDF.exe REM Build-Script fuer AnwesenheitPDF.exe + Windows-Installer
REM REM
REM Ergebnis: REM Ergebnis:
REM build/ - temporaere PyInstaller-Artefakte REM build/ - temporaere PyInstaller-Artefakte
REM dist/ - fertiges Programmverzeichnis (AnwesenheitPDF.exe + alle Abhaengigkeiten) REM dist/ - fertiges Programmverzeichnis (AnwesenheitPDF.exe + alle Abhaengigkeiten)
REM + AnwesenheitPDF-Setup-x.y.z.exe (Windows-Installer)
REM REM
REM Voraussetzungen: REM Voraussetzungen:
REM pip install -r requirements-dev.txt REM pip install -r requirements-dev.txt
REM Inno Setup 6 installiert (https://jrsoftware.org/isinfo.php)
cd /d "%~dp0" cd /d "%~dp0"
@@ -14,7 +16,7 @@ echo.
echo === AnwesenheitPDF Build === echo === AnwesenheitPDF Build ===
echo. echo.
echo [1/3] Abhaengigkeiten installieren... echo [1/4] Abhaengigkeiten installieren...
pip install -r requirements.txt --quiet && pip install -r requirements-dev.txt --quiet pip install -r requirements.txt --quiet && pip install -r requirements-dev.txt --quiet
if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% neq 0 (
echo [FEHLER] pip install fehlgeschlagen. echo [FEHLER] pip install fehlgeschlagen.
@@ -22,7 +24,7 @@ if %ERRORLEVEL% neq 0 (
exit /b 1 exit /b 1
) )
echo [2/3] Version aktualisieren... echo [2/4] Version aktualisieren...
python update_version.py python update_version.py
if %ERRORLEVEL% neq 0 ( if %ERRORLEVEL% neq 0 (
echo [FEHLER] Version-Update fehlgeschlagen. echo [FEHLER] Version-Update fehlgeschlagen.
@@ -30,7 +32,7 @@ if %ERRORLEVEL% neq 0 (
exit /b 1 exit /b 1
) )
echo [3/3] Build starten... echo [3/4] Build starten...
echo. echo.
python -m PyInstaller ^ python -m PyInstaller ^
@@ -59,11 +61,104 @@ if not exist "dist\AnwesenheitPDF\template" mkdir "dist\AnwesenheitPDF\template"
if not exist "dist\AnwesenheitPDF\output" mkdir "dist\AnwesenheitPDF\output" if not exist "dist\AnwesenheitPDF\output" mkdir "dist\AnwesenheitPDF\output"
if exist "template\sab.pdf" copy /Y "template\sab.pdf" "dist\AnwesenheitPDF\template\sab.pdf" >nul if exist "template\sab.pdf" copy /Y "template\sab.pdf" "dist\AnwesenheitPDF\template\sab.pdf" >nul
echo.
echo [4/4] Windows-Installer erstellen...
REM Version aus config.json lesen (Format: "x.y.z+hash" -> nur "x.y.z" verwenden)
for /f "tokens=2 delims=:," %%v in ('findstr /c:"\"version\"" config.json') do (
set RAW_VERSION=%%v
)
REM Fuehrende/nachfolgende Leerzeichen und Anfuehrungszeichen entfernen, Hash abschneiden
set RAW_VERSION=%RAW_VERSION: =%
set RAW_VERSION=%RAW_VERSION:"=%
for /f "tokens=1 delims=+" %%c in ("%RAW_VERSION%") do set APP_VERSION=%%c
echo Version: %APP_VERSION%
REM Inno Setup Compiler suchen (Standard-Installationspfade)
set ISCC_PATH=
if exist "%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe" (
set "ISCC_PATH=%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe"
) else if exist "%ProgramFiles%\Inno Setup 6\ISCC.exe" (
set "ISCC_PATH=%ProgramFiles%\Inno Setup 6\ISCC.exe"
) else if exist "%LOCALAPPDATA%\Programs\Inno Setup 6\ISCC.exe" (
set "ISCC_PATH=%LOCALAPPDATA%\Programs\Inno Setup 6\ISCC.exe"
) else (
for /f "delims=" %%i in ('where ISCC.exe 2^>nul') do set "ISCC_PATH=%%i"
)
if "%ISCC_PATH%"=="" (
echo.
echo [WARNUNG] Inno Setup 6 nicht gefunden. Installer wird NICHT erstellt.
echo Download: https://jrsoftware.org/isinfo.php
echo Nach der Installation Inno Setup 6 neu starten und build.bat erneut ausfuehren.
echo.
goto build_done
)
"%ISCC_PATH%" /DMyAppVersion=%APP_VERSION% installer\AnwesenheitPDF.iss > build_iscc.log 2>&1
if %ERRORLEVEL% neq 0 (
echo.
echo [FEHLER] Installer-Erstellung fehlgeschlagen.
echo.
type build_iscc.log
pause
exit /b 1
)
echo.
echo [OK] Installer erstellt: dist\AnwesenheitPDF-Setup-%APP_VERSION%.exe
echo.
echo [5/5] Installer zu update_check_path deployen...
REM update_check_path aus config.json auslesen
for /f "tokens=2 delims=:," %%d in ('findstr /c:"\"update_check_path\"" config.json') do (
set UPDATE_PATH=%%d
)
REM Leerzeichen und Anfuehrungszeichen entfernen
set UPDATE_PATH=%UPDATE_PATH: =%
set UPDATE_PATH=%UPDATE_PATH:"=%
if "%UPDATE_PATH%"=="" (
echo [WARNUNG] update_check_path nicht in config.json gefunden. Deployment uebersprungen.
goto build_done
)
REM Zielverzeichnis pruefen und ggf. erstellen
if not exist "%UPDATE_PATH%" (
echo [INFO] Erstelle Zielverzeichnis: %UPDATE_PATH%
mkdir "%UPDATE_PATH%"
if %ERRORLEVEL% neq 0 (
echo [WARNUNG] Konnte Zielverzeichnis nicht erstellen. Deployment uebersprungen.
goto build_done
)
)
REM Neuen Installer kopieren
echo [INFO] Kopiere Installer nach: %UPDATE_PATH%
copy /Y "dist\AnwesenheitPDF-Setup-%APP_VERSION%.exe" "%UPDATE_PATH%\AnwesenheitPDF-Setup-%APP_VERSION%.exe" >nul
if %ERRORLEVEL% neq 0 (
echo [WARNUNG] Installer-Deployment fehlgeschlagen (Schreibrechte?).
goto build_done
)
echo [OK] Installer deployed zu: %UPDATE_PATH%\AnwesenheitPDF-Setup-%APP_VERSION%.exe
:build_done
echo. echo.
echo [OK] Build abgeschlossen. echo [OK] Build abgeschlossen.
echo Programm: dist\AnwesenheitPDF\AnwesenheitPDF.exe echo Programm: dist\AnwesenheitPDF\AnwesenheitPDF.exe
echo Template: dist\AnwesenheitPDF\template\sab.pdf echo Template: dist\AnwesenheitPDF\template\sab.pdf
echo Output: dist\AnwesenheitPDF\output echo Output: dist\AnwesenheitPDF\output
if not "%ISCC_PATH%"=="" (
echo Installer: dist\AnwesenheitPDF-Setup-%APP_VERSION%.exe
)
if not "%UPDATE_PATH%"=="" (
echo Deployed: %UPDATE_PATH%\AnwesenheitPDF-Setup-%APP_VERSION%.exe
)
echo. echo.
echo ... Programm wird gestartet ... echo ... Programm wird gestartet ...
start "" "dist\AnwesenheitPDF\AnwesenheitPDF.exe" start "" "dist\AnwesenheitPDF\AnwesenheitPDF.exe"
+3 -2
View File
@@ -1,5 +1,5 @@
{ {
"version": "1.0.1+250a473", "version": "1.0.8+47220f9",
"antragsnummer": "100740439", "antragsnummer": "100740439",
"ort": "Dresden", "ort": "Dresden",
"template_pdf": "template/sab.pdf", "template_pdf": "template/sab.pdf",
@@ -42,5 +42,6 @@
"pauschale_present_value": "1", "pauschale_present_value": "1",
"pdf_use_display_values": true, "pdf_use_display_values": true,
"pdf_need_appearances": false, "pdf_need_appearances": false,
"excel_network_source": "J:\\Fachbereiche\\BO\\Produktionsschule\\Maßnahmeunterlagen\\30_Teilnehmende\\01_Anwesenheit\\Anwesenheitsliste\\Anwesenheitsliste 2026.xlsx" "excel_network_source": "J:\\Fachbereiche\\BO\\Produktionsschule\\Maßnahmeunterlagen\\30_Teilnehmende\\01_Anwesenheit\\Anwesenheitsliste\\Anwesenheitsliste 2026.xlsx",
"update_check_path": "j:\\Fachbereiche\\BO\\Produktionsschule\\Maßnahmeunterlagen\\30_Teilnehmende\\02_Abrechnung Aufwand und Fahrtgeld\\Automatisierung"
} }
+133 -1
View File
@@ -2,13 +2,15 @@
# Ergebnis: dist/AnwesenheitPDF/AnwesenheitPDF.exe + alle Abhaengigkeiten # Ergebnis: dist/AnwesenheitPDF/AnwesenheitPDF.exe + alle Abhaengigkeiten
# Build-Artefakte: build/ (temporaer, nicht committen) # Build-Artefakte: build/ (temporaer, nicht committen)
import json
import os import os
import re
import shutil import shutil
import sys import sys
import threading import threading
from datetime import date, datetime from datetime import date, datetime
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Tuple
import customtkinter as ctk import customtkinter as ctk
import tkinter as tk import tkinter as tk
@@ -99,6 +101,78 @@ def _count_output_pdfs(config: Dict[str, Any]) -> int:
return len(list(out_dir.glob("*.pdf"))) return len(list(out_dir.glob("*.pdf")))
# ---------------------------------------------------------------------------
# Update-check helpers
# ---------------------------------------------------------------------------
_UPDATE_CHECK_FILE = _runtime_dir() / "update_check.json"
_INSTALLER_PATTERN = re.compile(r"^AnwesenheitPDF-Setup-(\d+\.\d+\.\d+)\.exe$", re.IGNORECASE)
def _parse_semver(version_str: str) -> Tuple[int, int, int]:
"""Parse semantic version string, ignoring build metadata after '+'."""
base = version_str.split("+")[0].strip()
match = re.match(r"^(\d+)\.(\d+)\.(\d+)$", base)
if not match:
return (0, 0, 0)
return int(match.group(1)), int(match.group(2)), int(match.group(3))
def _should_check_update() -> bool:
"""Return True if no check has been recorded for today."""
try:
if _UPDATE_CHECK_FILE.exists():
with open(_UPDATE_CHECK_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("last_check") != date.today().isoformat()
except Exception:
pass
return True
def _record_update_check() -> None:
"""Persist today's date as the last check timestamp."""
try:
with open(_UPDATE_CHECK_FILE, "w", encoding="utf-8") as f:
json.dump({"last_check": date.today().isoformat()}, f)
except Exception:
pass
def _find_latest_installer(config: Dict[str, Any]) -> Optional[Tuple[str, Path]]:
"""Scan the configured SMB update path for the newest installer.
Returns (version_str, installer_path) if a newer version exists, else None.
"""
update_path_str = str(config.get("update_check_path", "")).strip()
if not update_path_str:
return None
update_dir = Path(update_path_str)
if not update_dir.exists():
return None
current_ver = _parse_semver(str(config.get("version", "0.0.0")))
best_ver = current_ver
best_file: Optional[Path] = None
best_ver_str: Optional[str] = None
for f in update_dir.glob("AnwesenheitPDF-Setup-*.exe"):
m = _INSTALLER_PATTERN.match(f.name)
if not m:
continue
ver_str = m.group(1)
ver = _parse_semver(ver_str)
if ver > best_ver:
best_ver = ver
best_file = f
best_ver_str = ver_str
if best_file and best_ver_str:
return (best_ver_str, best_file)
return None
def _build_month_keys(config: Dict[str, Any]) -> List[str]: def _build_month_keys(config: Dict[str, Any]) -> List[str]:
"""Return deduplicated month keys from config (preserves insertion order).""" """Return deduplicated month keys from config (preserves insertion order)."""
seen: set = set() seen: set = set()
@@ -178,6 +252,7 @@ class AnwesenheitApp(ctk.CTk):
self._tooltip_label: Optional[tk.Label] = None self._tooltip_label: Optional[tk.Label] = None
# Build UI sections # Build UI sections
self._build_update_banner()
self._build_input_section() self._build_input_section()
self._build_button_section() self._build_button_section()
self._build_preview_section() self._build_preview_section()
@@ -185,11 +260,68 @@ class AnwesenheitApp(ctk.CTk):
# Auto-sync after event loop starts # Auto-sync after event loop starts
self.after(100, self._start_initial_sync) self.after(100, self._start_initial_sync)
# Daily update check
if _should_check_update():
self.after(200, self._start_update_check)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# UI builders # UI builders
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _build_update_banner(self) -> None:
"""Create the update-available banner (hidden until an update is found)."""
self._update_banner = ctk.CTkFrame(self, fg_color="#854D0E")
# Banner is NOT packed here — only shown when an update is detected
self._update_banner_label = ctk.CTkLabel(
self._update_banner,
text="",
text_color="#FEF9C3",
font=ctk.CTkFont(weight="bold"),
anchor="w",
)
self._update_banner_label.pack(side="left", padx=12, pady=6, fill="x", expand=True)
self._update_banner_btn = ctk.CTkButton(
self._update_banner,
text="Installieren",
width=110,
height=28,
fg_color="#CA8A04",
hover_color="#A16207",
text_color="white",
command=self._open_update_folder,
)
self._update_banner_btn.pack(side="right", padx=(0, 12), pady=6)
self._update_installer_path: Optional[Path] = None
def _show_update_banner(self, version: str, installer: Path) -> None:
"""Display the update banner with the new version info."""
self._update_installer_path = installer
current = _CONFIG.get("version", "?").split("+")[0]
self._update_banner_label.configure(
text=f"Update verfügbar: v{current} → v{version} "
f"({installer.name})"
)
# Insert banner above everything else
self._update_banner.pack(fill="x", padx=16, pady=(8, 0), before=self.winfo_children()[1])
def _open_update_folder(self) -> None:
"""Open the installer directory in Explorer."""
if self._update_installer_path and self._update_installer_path.parent.exists():
os.startfile(str(self._update_installer_path.parent))
def _start_update_check(self) -> None:
threading.Thread(target=self._run_update_check, daemon=True).start()
def _run_update_check(self) -> None:
try:
result = _find_latest_installer(_CONFIG)
except Exception:
result = None
_record_update_check()
if result:
version, installer = result
self.after(0, self._show_update_banner, version, installer)
def _build_input_section(self) -> None: def _build_input_section(self) -> None:
frame = ctk.CTkFrame(self) frame = ctk.CTkFrame(self)
frame.pack(fill="x", padx=16, pady=(16, 0)) frame.pack(fill="x", padx=16, pady=(16, 0))
+99
View File
@@ -0,0 +1,99 @@
; Inno Setup Script fuer AnwesenheitPDF
; Compiler: Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
;
; Aufruf durch build.bat:
; iscc.exe /DMyAppVersion=1.0.1 installer\AnwesenheitPDF.iss
;
; Ausgabe: dist\AnwesenheitPDF-Setup-{version}.exe
#ifndef MyAppVersion
#define MyAppVersion "0.0.0"
#endif
#define MyAppName "AnwesenheitPDF"
#define MyAppPublisher "SAB Anwesenheitslisten"
#define MyAppExeName "AnwesenheitPDF.exe"
#define MyAppID "{{A3F2C1D7-84BE-4E9A-B5D0-2C7F3A1E9B46}"
[Setup]
; Eindeutige App-ID – niemals aendern (sonst gelten Updates als Neuinstallation)
AppId={#MyAppID}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppComments=Anwesenheitslisten aus Excel-Daten als PDF generieren
; Installations-Zielverzeichnis (Programme-Ordner)
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
; Installer-Ausgabe
OutputDir=..\dist
OutputBaseFilename=AnwesenheitPDF-Setup-{#MyAppVersion}
; Komprimierung
Compression=lzma2/ultra64
SolidCompression=yes
LZMAUseSeparateProcess=yes
; Erscheinungsbild
WizardStyle=modern
SetupIconFile=..\assets\icon.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
; Berechtigungen: Admin erforderlich fuer Installation in Programme-Ordner
PrivilegesRequired=admin
PrivilegesRequiredOverridesAllowed=dialog
; Mindest-Windows-Version: Windows 10 (10.0)
MinVersion=10.0
; Installer kann direkt vom Netzlaufwerk (SMB-Share) ausgefuehrt werden
; (kein lokales Entpacken des Installers noetig)
AllowNetworkDrive=yes
AllowUNCPath=yes
; Versionsinformation fuer die EXE-Eigenschaften
VersionInfoVersion={#MyAppVersion}
VersionInfoCompany={#MyAppPublisher}
VersionInfoDescription={#MyAppName} Setup
VersionInfoProductName={#MyAppName}
VersionInfoProductVersion={#MyAppVersion}
[Languages]
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
[Tasks]
Name: "startmenuicon"; \
Description: "Startmenü-Einträge erstellen"; \
GroupDescription: "Startmenü:"
[Files]
; Gesamten PyInstaller-Output in das Installationsverzeichnis kopieren
Source: "..\dist\AnwesenheitPDF\*"; \
DestDir: "{app}"; \
Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
; Startmenü-Eintrag (nur wenn Task ausgewaehlt)
Name: "{group}\{#MyAppName}"; \
Filename: "{app}\{#MyAppExeName}"; \
Comment: "Anwesenheitslisten als PDF generieren"; \
Tasks: startmenuicon
; Startmenü: Uninstaller (nur wenn Task ausgewaehlt)
Name: "{group}\{#MyAppName} deinstallieren"; \
Filename: "{uninstallexe}"; \
Tasks: startmenuicon
[Run]
; Optionaler Start nach der Installation
Filename: "{app}\{#MyAppExeName}"; \
Description: "{#MyAppName} jetzt starten"; \
Flags: nowait postinstall skipifsilent
[UninstallDelete]
; Beim Deinstallieren: nur generierte Dateien loeschen, keine Nutzerdaten
Type: files; Name: "{app}\*.log"
+4
View File
@@ -100,6 +100,10 @@
"excel_network_source": { "excel_network_source": {
"type": "string", "type": "string",
"description": "Optionaler SMB- oder Dateipfad zur Excel-Datei, die beim GUI-Start nach input/ kopiert und bei Bedarf aktualisiert wird" "description": "Optionaler SMB- oder Dateipfad zur Excel-Datei, die beim GUI-Start nach input/ kopiert und bei Bedarf aktualisiert wird"
},
"update_check_path": {
"type": "string",
"description": "SMB-Pfad zum Verzeichnis mit Installer-Dateien (AnwesenheitPDF-Setup-x.y.z.exe) fuer automatische Update-Pruefung"
} }
} }
} }