Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0553c27ea2 |
@@ -1,6 +0,0 @@
|
||||
root = true
|
||||
|
||||
[*.{bat,cmd}]
|
||||
charset = latin1
|
||||
end_of_line = crlf
|
||||
insert_final_newline = true
|
||||
@@ -95,11 +95,6 @@ COOLIFY_RESOURCE_UUID=
|
||||
COOLIFY_TRIGGER_MODE=webhook
|
||||
COOLIFY_DEPLOY_METHOD=GET
|
||||
COOLIFY_DEPLOY_ENDPOINT=/api/v1/deploy?uuid={uuid}&force={force}
|
||||
COOLIFY_DEPLOYMENT_STATUS_ENDPOINT=/api/v1/deployments/{deployment_id}
|
||||
VENDOO_DEPLOY_TIMEOUT_SECONDS=900
|
||||
VENDOO_DEPLOY_HEALTH_POLL_SECONDS=15
|
||||
VENDOO_BUILD_REVISION=
|
||||
VENDOO_COOLIFY_AUTO_DEPLOY=false
|
||||
VENDOO_GITHUB_REPOSITORY=Masterluke77/vendoo
|
||||
VENDOO_GITHUB_BRANCH=main
|
||||
VENDOO_GHCR_IMAGE=ghcr.io/masterluke77/vendoo
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
@@ -48,10 +48,6 @@ jobs:
|
||||
- name: Syntaxprüfung
|
||||
run: npm run check
|
||||
|
||||
|
||||
- name: PowerShell-Regressionen statisch prüfen
|
||||
run: npm run verify:powershell-static
|
||||
|
||||
- name: Produktiven Source-Root und Archivgrenzen prüfen
|
||||
run: npm run verify:source-boundaries
|
||||
|
||||
@@ -118,31 +114,3 @@ jobs:
|
||||
|
||||
- name: Docker-Image testweise bauen
|
||||
run: docker build --pull --tag vendoo:ci .
|
||||
|
||||
windows-release-contract:
|
||||
name: Windows – vollständiger Releasevertrag
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 35
|
||||
steps:
|
||||
- name: Repository auschecken
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Node.js 22 aktivieren
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
cache: npm
|
||||
|
||||
- name: Abhängigkeiten reproduzierbar installieren
|
||||
run: npm ci
|
||||
|
||||
- name: Alle PowerShell-Dateien mit Windows PowerShell 5.1 parsen
|
||||
shell: powershell
|
||||
run: .\tools\verify-powershell-parser.ps1
|
||||
|
||||
- name: Vollständigen Vendoo-Releasevertrag unter Windows ausführen
|
||||
shell: pwsh
|
||||
run: npm run verify:all
|
||||
|
||||
|
||||
@@ -16,20 +16,7 @@ env:
|
||||
IMAGE_NAME: masterluke77/vendoo
|
||||
|
||||
jobs:
|
||||
powershell-parser:
|
||||
name: Windows PowerShell 5.1 Parser
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Repository auschecken
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Alle PowerShell-Dateien mit Windows PowerShell 5.1 parsen
|
||||
shell: powershell
|
||||
run: .\tools\verify-powershell-parser.ps1
|
||||
|
||||
release-assets:
|
||||
needs: powershell-parser
|
||||
name: Release-Paket und Prüfsummen
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -55,21 +42,31 @@ jobs:
|
||||
EXPECTED="v$(node -p "require('./package.json').version")"
|
||||
test "$RELEASE_TAG" = "$EXPECTED" || { echo "Release-Tag $RELEASE_TAG passt nicht zu $EXPECTED." >&2; exit 1; }
|
||||
|
||||
- name: ESM-Exportverträge explizit prüfen
|
||||
run: npm run verify:esm-exports
|
||||
|
||||
- name: FLUX-Loop-Hotfix explizit prüfen
|
||||
run: npm run verify:flux-loop
|
||||
|
||||
- name: Vollständige Prüfung
|
||||
run: npm run verify:all
|
||||
|
||||
- name: Release-Manifest reproduzierbar prüfen
|
||||
run: |
|
||||
node tools/generate-release-manifest.mjs .
|
||||
git diff --exit-code -- release-manifest.json
|
||||
npm run verify:git
|
||||
npm run verify:git-regression
|
||||
npm run verify:git-staging
|
||||
npm run verify:ignore-boundaries
|
||||
npm run check
|
||||
npm run verify:source-boundaries
|
||||
npm run verify:esm-exports
|
||||
npm run verify:flux-loop
|
||||
npm run verify:capabilities
|
||||
npm run verify:coolify
|
||||
npm run verify:initial-modules
|
||||
npm run verify:architecture
|
||||
npm run verify:themes
|
||||
npm run verify:security
|
||||
npm run verify:identity
|
||||
npm run verify:catalog
|
||||
npm run verify:modules
|
||||
npm run verify:deployment
|
||||
npm run verify:config
|
||||
npm run verify:installer-shell
|
||||
npm run verify:db
|
||||
|
||||
- name: Manifestbasiertes Release-Staging erzeugen
|
||||
- name: Release-Staging erzeugen
|
||||
run: node tools/build-release.mjs
|
||||
|
||||
- name: ZIP und SHA-256 erzeugen
|
||||
@@ -117,7 +114,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Docker Buildx aktivieren
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Bei GHCR anmelden
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Vendoo Release Manifest Sync
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- release/1.43.0-production-update-v3
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: vendoo-release-manifest-sync-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Release-Manifest reproduzierbar synchronisieren
|
||||
if: ${{ !contains(github.event.head_commit.message, '[manifest-sync]') }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Release-Branch auschecken
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Node.js 22 aktivieren
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
- name: Release-Manifest erzeugen
|
||||
run: node tools/generate-release-manifest.mjs .
|
||||
|
||||
- name: Geändertes Manifest committen
|
||||
shell: bash
|
||||
run: |
|
||||
if git diff --quiet -- release-manifest.json; then
|
||||
echo "Release-Manifest ist bereits aktuell."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "vendoo-release-bot"
|
||||
git config user.email "vendoo-release-bot@users.noreply.github.com"
|
||||
git add release-manifest.json
|
||||
git commit -m "chore: Release-Manifest synchronisieren [manifest-sync]"
|
||||
git push origin HEAD:${GITHUB_REF_NAME}
|
||||
@@ -1,83 +0,0 @@
|
||||
name: Vendoo Production Deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target_version:
|
||||
description: Expected Vendoo version
|
||||
required: true
|
||||
type: string
|
||||
public_base_url:
|
||||
description: Public Vendoo URL
|
||||
required: true
|
||||
default: https://vendoo.flatlined.de
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: vendoo-production-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
shell: bash
|
||||
env:
|
||||
TARGET_VERSION: ${{ inputs.target_version }}
|
||||
PUBLIC_BASE_URL: ${{ inputs.public_base_url }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
[[ "$TARGET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||
[[ "$PUBLIC_BASE_URL" =~ ^https://[^/]+/?$ ]]
|
||||
|
||||
- name: Trigger Coolify
|
||||
shell: bash
|
||||
env:
|
||||
COOLIFY_DEPLOY_WEBHOOK_URL: ${{ secrets.COOLIFY_DEPLOY_WEBHOOK_URL }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -n "$COOLIFY_DEPLOY_WEBHOOK_URL"
|
||||
curl --fail --silent --show-error --location --request GET \
|
||||
--connect-timeout 20 --max-time 90 \
|
||||
"$COOLIFY_DEPLOY_WEBHOOK_URL" >/tmp/coolify-response.txt
|
||||
echo "Coolify accepted the deployment trigger."
|
||||
|
||||
- name: Wait for exact production version
|
||||
shell: bash
|
||||
env:
|
||||
TARGET_VERSION: ${{ inputs.target_version }}
|
||||
PUBLIC_BASE_URL: ${{ inputs.public_base_url }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE_URL="${PUBLIC_BASE_URL%/}"
|
||||
deadline=$((SECONDS + 1500))
|
||||
last_version="unknown"
|
||||
while (( SECONDS < deadline )); do
|
||||
if body=$(curl --fail --silent --show-error --connect-timeout 10 --max-time 30 \
|
||||
-H 'Accept: application/json' -H 'Cache-Control: no-cache' \
|
||||
"$BASE_URL/readyz?github_run=${GITHUB_RUN_ID}&attempt=${GITHUB_RUN_ATTEMPT}"); then
|
||||
ready=$(jq -r '.ok // false' <<<"$body")
|
||||
version=$(jq -r '.version // empty' <<<"$body")
|
||||
if [[ -n "$version" ]]; then last_version="$version"; fi
|
||||
echo "ready=$ready version=${version:-unknown} expected=$TARGET_VERSION"
|
||||
if [[ "$ready" == "true" && "$version" == "$TARGET_VERSION" ]]; then exit 0; fi
|
||||
fi
|
||||
sleep 15
|
||||
done
|
||||
echo "Timeout waiting for Vendoo $TARGET_VERSION. Last version: $last_version" >&2
|
||||
exit 1
|
||||
|
||||
- name: Final health response
|
||||
shell: bash
|
||||
env:
|
||||
PUBLIC_BASE_URL: ${{ inputs.public_base_url }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BASE_URL="${PUBLIC_BASE_URL%/}"
|
||||
curl --fail --silent --show-error -H 'Accept: application/json' \
|
||||
"$BASE_URL/healthz?github_run=${GITHUB_RUN_ID}" | jq .
|
||||
@@ -1,125 +0,0 @@
|
||||
name: Vendoo Windows Release Diagnostics
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: vendoo-windows-diagnostics-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
windows-release-diagnostics:
|
||||
name: Windows – Release-Gates einzeln
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 35
|
||||
steps:
|
||||
- name: Repository auschecken
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Node.js 22 aktivieren
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.x'
|
||||
cache: npm
|
||||
|
||||
- name: Abhängigkeiten reproduzierbar installieren
|
||||
run: npm ci
|
||||
|
||||
- name: Alle PowerShell-Dateien mit Windows PowerShell 5.1 parsen
|
||||
shell: powershell
|
||||
run: .\tools\verify-powershell-parser.ps1
|
||||
|
||||
- name: Git-Sicherheitsgate
|
||||
run: npm run verify:git
|
||||
|
||||
- name: Git-Sicherheitsgate Regressionstest
|
||||
run: npm run verify:git-regression
|
||||
|
||||
- name: Git-Staging-Filter Regressionstest
|
||||
run: npm run verify:git-staging
|
||||
|
||||
- name: Release-Manifest prüfen
|
||||
run: npm run verify:release-manifest
|
||||
|
||||
- name: Git-Ignore-Grenzen prüfen
|
||||
run: npm run verify:ignore-boundaries
|
||||
|
||||
- name: Syntax und Identity-Datenbank prüfen
|
||||
run: npm run check
|
||||
|
||||
- name: PowerShell-Regressionen statisch prüfen
|
||||
run: npm run verify:powershell-static
|
||||
|
||||
- name: Source-Root-Grenzen prüfen
|
||||
run: npm run verify:source-boundaries
|
||||
|
||||
- name: ESM-Exportverträge prüfen
|
||||
run: npm run verify:esm-exports
|
||||
|
||||
- name: FLUX-Loop-Hotfix prüfen
|
||||
run: npm run verify:flux-loop
|
||||
|
||||
- name: Capability-Graph prüfen
|
||||
run: npm run verify:capabilities
|
||||
|
||||
- name: Coolify- und Produktionsoperationen prüfen
|
||||
run: npm run verify:coolify
|
||||
|
||||
- name: Initial deaktivierte Module prüfen
|
||||
run: npm run verify:initial-modules
|
||||
|
||||
- name: Plattformarchitektur prüfen
|
||||
run: npm run verify:architecture
|
||||
|
||||
- name: Theme Manager prüfen
|
||||
run: npm run verify:themes
|
||||
|
||||
- name: Security Foundation prüfen
|
||||
run: npm run verify:security
|
||||
|
||||
- name: Identity und Settings prüfen
|
||||
run: npm run verify:identity
|
||||
|
||||
- name: Einladungen prüfen
|
||||
run: npm run verify:invitations
|
||||
|
||||
- name: SQLite-Cleanup unter Windows prüfen
|
||||
run: npm run verify:sqlite-cleanup
|
||||
|
||||
- name: Katalog und Medien prüfen
|
||||
run: npm run verify:catalog
|
||||
|
||||
- name: Modulmanager prüfen
|
||||
run: npm run verify:modules
|
||||
|
||||
- name: Produktionsoperationen prüfen
|
||||
run: npm run verify:production-operations
|
||||
|
||||
- name: One-Click-Updater prüfen
|
||||
run: npm run verify:one-click-update
|
||||
|
||||
- name: Modulnavigation prüfen
|
||||
run: npm run verify:module-navigation
|
||||
|
||||
- name: Updater-UI prüfen
|
||||
run: npm run verify:updater-ui
|
||||
|
||||
- name: Updater-Hotfix-Vertrag prüfen
|
||||
run: npm run verify:updater-hotfix
|
||||
|
||||
- name: Deployment prüfen
|
||||
run: npm run verify:deployment
|
||||
|
||||
- name: Konfigurationsspeicher prüfen
|
||||
run: npm run verify:config
|
||||
|
||||
- name: Installer-Shell-Portabilität prüfen
|
||||
run: npm run verify:installer-shell
|
||||
|
||||
- name: Datenbankmigrationen prüfen
|
||||
run: npm run verify:db
|
||||
@@ -1,36 +0,0 @@
|
||||
# Vendoo 1.43.0 – Start hier
|
||||
|
||||
## Production Updater 3.3 starten
|
||||
|
||||
1. Dieses ZIP vollständig in einen neuen lokalen Ordner entpacken, zum Beispiel `C:\Vendoo-Update`.
|
||||
2. Im Ordner `vendoo` doppelt auf `UPDATE_VENDOO.bat` klicken.
|
||||
3. Den Vendoo Production Updater bis zum eindeutigen Ergebnis geöffnet lassen.
|
||||
|
||||
Der Updater zeigt zehn klare Phasen, Fortschritt, Live-Protokoll und einen dauerhaft sichtbaren Fehlerzustand. Beim ersten Lauf kann einmalig die GitHub-Anmeldung im Browser erforderlich sein. GitHub CLI wird bei Bedarf als verwaltete portable Kopie geladen und per SHA-256 geprüft.
|
||||
|
||||
Ein Coolify-Webhook wird nicht abgefragt. Nach einem erfolgreichen Merge verwendet Vendoo das bereits eingerichtete Coolify Auto Deploy der GitHub-Verknüpfung und bestätigt anschließend `/readyz` mit exakt Version `1.43.0`.
|
||||
|
||||
## Release-Architektur 3.0
|
||||
|
||||
- Die geklonte `main`-Arbeitskopie wird nicht geleert.
|
||||
- `release-manifest.json` enthält jede auszuliefernde Datei mit Größe und SHA-256.
|
||||
- Pflichtdateien wie `db/schema.sql` werden vor den Tests hart geprüft.
|
||||
- Dateien werden als Overlay angewendet und im Ziel erneut gehasht.
|
||||
- `release-manifest.json` wird anschließend als bytegeprüfte Steuerdatei in die Arbeitskopie übernommen.
|
||||
- Löschungen sind nur über die explizite Liste `removedFiles` erlaubt.
|
||||
- Alte Updater-2.x-Sitzungen werden nicht übernommen.
|
||||
- GitHub Actions führt den vollständigen Releasevertrag auf Windows und Linux aus.
|
||||
|
||||
## Inhalt von Vendoo 1.43.0
|
||||
|
||||
- modulabhängige Navigation: deaktivierte Module verschwinden sofort aus der Seitenleiste,
|
||||
- optionales, standardmäßig deaktiviertes Modul **Produktbild Studio**,
|
||||
- grundlegend überarbeitete Seite **Neuer Artikel**,
|
||||
- dauerhaft sichtbare Aktionen **Artikel generieren** und **Speichern**,
|
||||
- manifestbasierter Production Updater 3.3.
|
||||
|
||||
## Sicherheitsgrenzen
|
||||
|
||||
Der Updater ersetzt oder löscht niemals `/app/data`, Datenbanken, Uploads, Backups, `.env` oder API-Schlüssel. GitHub-Checks, kontrollierter Merge, Coolify Auto Deploy und `/readyz`-Versionskontrolle bleiben verpflichtend.
|
||||
|
||||
Technische Details: `docs/PRODUCTION_UPDATER_3_3.md`.
|
||||
@@ -1,95 +1,5 @@
|
||||
## Vendoo Production Updater 3.3 – Manifest Control Artifact Contract
|
||||
|
||||
- `release-manifest.json` wird nach dem Payload-Overlay ausdrücklich in die Git-Arbeitskopie kopiert.
|
||||
- Die Steuerdatei wird bytegenau gegen das Release-Paket geprüft.
|
||||
- Manifestprüfung in einer Git-Baseline toleriert zusätzliche unveränderte Baseline-Dateien, prüft aber alle manifestierten Dateien vollständig.
|
||||
- `removedFiles` darf die Manifest-Steuerdatei nicht löschen.
|
||||
- Der Windows-Lauf kann dadurch `verify-production-updater-3.3.mjs` innerhalb der geklonten Arbeitskopie ausführen.
|
||||
- Prozessfehler zeigen in Status und Oberfläche zuerst die echte Fehlerausgabe; lange Standardausgaben verdrängen die Ursache nicht mehr.
|
||||
|
||||
## Vendoo Production Updater 3.1 – Manifest-Based Release Application
|
||||
|
||||
- replaces destructive clone cleanup and exclusion-list copying with SHA-256 verified overlay manifest
|
||||
- requires `db/schema.sql` and all release-critical source files before tests can start
|
||||
- preserves baseline files unless deletion is explicitly declared in `removedFiles`
|
||||
- verifies every payload file before and after copying
|
||||
- rejects stale Updater-2.x sessions and uses a dedicated v3 release branch
|
||||
|
||||
## Vendoo Production Updater 2.10 – Windows Installer-Shell Portability
|
||||
|
||||
- `verify:installer-shell` läuft über einen plattformneutralen Node-Wrapper.
|
||||
- Unter Windows wird `sh.exe` aus Git for Windows über `where git`, `git --exec-path` und bekannte Installationspfade ermittelt.
|
||||
- Keine globale `sh`-PATH-Annahme mehr.
|
||||
- Git-Fortschritt auf stderr wird bei Exit-Code 0 als Ausgabe statt als Fehler protokolliert; npm-Warnungen erscheinen als Warnung.
|
||||
- Zielversion der Anwendung bleibt Vendoo 1.43.0.
|
||||
|
||||
## Vendoo Production Updater 2.9 – Systematic Windows SQLite Handle Audit
|
||||
|
||||
- Zentraler Cleanup für temporäre Vendoo-SQLite-Datenbanken.
|
||||
- Einladungs-Verifier schließt `better-sqlite3` vor dem Entfernen des Temp-Ordners.
|
||||
- Aktive Verifier werden transitiv auf offene `lib/db.mjs`-Handles geprüft.
|
||||
- Windows-Cleanup-Regression läuft für Initialmodule und Einladungen jeweils dreifach isoliert.
|
||||
|
||||
## Vendoo Production Updater 2.8 – Windows SQLite Cleanup
|
||||
|
||||
- `verify:initial-modules` schließt die temporäre `better-sqlite3`-Datenbank jetzt vor dem Entfernen des Testordners.
|
||||
- Windows-`EBUSY` beim Löschen von `vendoo.db` behoben.
|
||||
- Neuer dreifacher Cleanup-Regressionstest verhindert offene SQLite-Dateien und Temp-Reste.
|
||||
|
||||
# Vendoo 1.43.0 – Production Updater 2.4
|
||||
|
||||
- GitHub-Browseranmeldung aus dem versteckten Worker entfernt.
|
||||
- Sichtbares, klar beschriftetes Geräte-Anmeldefenster mit `--web --clipboard`.
|
||||
- Stilles Loginstatus-Polling und automatische Fortsetzung nach erfolgreicher Anmeldung.
|
||||
- Downloadfortschritt auf 10-%-Stufen gedrosselt; keine Logflut mehr.
|
||||
- Neues Updater-2.4-Regressionsgate.
|
||||
|
||||
# Changelog
|
||||
|
||||
## Updater 2.1 – Worker Host & Bootstrap Diagnostics
|
||||
|
||||
- überwachten Worker-Host eingeführt
|
||||
- Status und Protokollpfad vor Worker-Start garantiert
|
||||
- Standardausgabe und Standardfehler vollständig erfasst
|
||||
- Lade-, Parser- und Initialisierungsfehler mit Datei und Zeile sichtbar
|
||||
- Diagnoseordner direkt in der Oberfläche erreichbar
|
||||
- Worker-Core beendet den Host nicht mehr eigenmächtig
|
||||
|
||||
## 1.43.0 Updater Revision 2.0 – Production Updater Replacement
|
||||
|
||||
- Bisherige Winget-/Konsolen-Hotfix-Kette vollständig durch einen eigenständigen WPF-Production-Updater ersetzt.
|
||||
- Neutrale dunkle Oberfläche mit zehn klaren Updatephasen, Fortschritt, Laufzeit, Heartbeat und Live-Protokoll.
|
||||
- Fehlerzustände bleiben dauerhaft sichtbar; das Fenster schließt sich weder bei Fehlern noch bei einem unerwarteten Worker-Ende automatisch.
|
||||
- Aktionen für Protokoll öffnen, Fehler kopieren, kontrollierten Abbruch und Wiederaufnahme.
|
||||
- Verwaltete portable GitHub CLI mit offiziellem Release-Download und SHA-256-Prüfung statt Winget-Abhängigkeit.
|
||||
- Feste Timeouts, Prozessbaum-Abbruch und Heartbeats für alle externen Werkzeuge.
|
||||
- Persistente Sitzung für sichere Wiederaufnahme nach Abbruch oder Neustart.
|
||||
- GitHub-, Test-, Merge-, Coolify- und Produktionsprüfung bleiben als getrennte Phasen nachvollziehbar.
|
||||
|
||||
## 1.43.0 – Module-aware Navigation, Listing Studio UX & Updater Status UI
|
||||
|
||||
- Navigationseinträge verschwinden unmittelbar, wenn das zugehörige Modul deaktiviert wird.
|
||||
- Neues authentifiziertes, minimales Modulstatus-API für die Oberfläche.
|
||||
- Produktbild Studio als eigenständiges, standardmäßig deaktiviertes Modul.
|
||||
- Seite „Neuer Artikel“ neu geordnet, lesbarer und konsequent am Theme-System ausgerichtet.
|
||||
- „Artikel generieren“ und „Speichern“ bleiben in einer sticky Aktionsleiste dauerhaft erreichbar.
|
||||
- Ein-Klick-Updater mit grafischer Fortschrittsanzeige, Prozentwert, Phase und Live-Protokoll.
|
||||
|
||||
## 1.42.0 – Production Operations & Controlled Update Completion
|
||||
|
||||
- Persistente Deployment-State-Machine mit Ereignisverlauf und Wiederaufnahme nach Neustart.
|
||||
- Idempotency-Key und Sperre gegen parallele Deployment-Aufträge.
|
||||
- Vorab-Backup wird erstellt, verifiziert und mit dem Auftrag verknüpft.
|
||||
- Coolify-Trigger, Remote-Status, Readiness und Zielversion werden getrennt bewertet.
|
||||
- Webhook-Abschlüsse benötigen eine zusätzliche exakte Bestätigung `CONFIRM`.
|
||||
- Fehler und Zeitüberschreitungen wechseln in `rollback_required` und blockieren neue Aufträge bis zur manuellen Prüfung.
|
||||
- Neuer Produktionscheck und Produktionsstatus im Update Center.
|
||||
- Additive Tabellen `deployment_runs`, `deployment_events`, `backup_verifications` und `production_checks`.
|
||||
- Keine Löschung oder Rücksetzung bestehender Daten, Benutzer, Uploads, Secrets, Backups oder Module.
|
||||
- Neuer `UPDATE_VENDOO.bat`-Assistent automatisiert Voraussetzungen, Volltest, Release-Branch, Pull Request, Merge und Coolify-Deployment.
|
||||
- Coolify-Webhook wird nur einmalig als verschlüsseltes GitHub Actions Secret hinterlegt.
|
||||
- Produktionsworkflow meldet Erfolg ausschließlich bei `/readyz` und exakt Version `1.42.0`.
|
||||
|
||||
## 1.41.2 – Git Ignore Boundary & Module Tracking Hotfix
|
||||
|
||||
- Root-Runtimeordner in `.gitignore` sind jetzt mit `/` verankert.
|
||||
|
||||
-23
@@ -1071,26 +1071,3 @@ Neue Kernel-Routen benötigen zwingend einen Modulbesitzer und eine explizite Po
|
||||
- `app/modules/` und `app/modules/sessions/` bleiben versionierter Source
|
||||
- Git-Ignore-Semantiktest mit echtem temporärem Repository
|
||||
- Pflichtdatei- und Tracking-Gate im GitHub-Deploy-Assistenten
|
||||
|
||||
|
||||
## 1.42.0 – Production Operations & Controlled Update Completion
|
||||
|
||||
- Persistente Deployment-State-Machine: `checking`, `backup_pending`, `backup_running`, `backup_verified`, `deploy_requested`, `deploy_running`, `health_wait`, `succeeded`, `failed`, `rollback_required`.
|
||||
- Idempotente Aufträge und harter Doppelstartschutz.
|
||||
- Verifiziertes Vorab-Backup mit SHA-256-/Manifestbezug.
|
||||
- Coolify-API-Statusabfrage über konfigurierbaren relativen Endpunkt; Webhook-Modus mit expliziter Abschlussbestätigung.
|
||||
- Readiness- und Zielversionsprüfung vor Erfolgsmeldung.
|
||||
- Persistenter Ereignisverlauf, Neustart-Wiederaufnahme und kontrollierte Aufhebung einer Rollback-Sperre.
|
||||
- Produktionsstatus und Produktionscheck im Update Center.
|
||||
- Additive Migration ohne Reset bestehender Produktivdaten.
|
||||
|
||||
|
||||
## 1.43.0 – Module-aware Navigation, Listing Studio & Updater Status UI
|
||||
|
||||
- Authentifizierter Modulstatus-Endpunkt `/api/modules/navigation` mit minimaler Antwortfläche.
|
||||
- Navigationselemente und optionale UI-Panels sind über `data-module` beziehungsweise `data-module-panel` an den realen Modulstatus gekoppelt.
|
||||
- Modulstatus wird vor geschützten Modul-APIs geladen; ein deaktiviertes AI-Modul blockiert den App-Start nicht.
|
||||
- Neuer Manifestvertrag `defaultEnabled`, damit ein Modul standardmäßig aus, aber administrativ aktivierbar sein kann.
|
||||
- `vendoo.product-image-tools` stellt `product-images.variants` bereit und startet standardmäßig deaktiviert.
|
||||
- Listing Studio mit sticky Command Bar, klarer Zwei-Spalten-Arbeitsfläche und dauerhaft sichtbaren Aktionen.
|
||||
- Grafische Update-Oberfläche mit Fortschritt, Phase, Detailmeldung, Live-Protokoll, sicherer einmaliger Coolify-Eingabe und korrektem Exit-Code.
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
@echo off
|
||||
rem Alter Dateiname bleibt als Kompatibilitaetsstarter erhalten.
|
||||
call "%~dp0UPDATE_VENDOO.bat"
|
||||
exit /b %ERRORLEVEL%
|
||||
setlocal EnableExtensions
|
||||
chcp 65001 >nul
|
||||
title Vendoo GitHub Deploy Assistent
|
||||
cd /d "%~dp0"
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -STA -File "%~dp0scripts\github-deploy.ps1"
|
||||
set "EXITCODE=%ERRORLEVEL%"
|
||||
if not "%EXITCODE%"=="0" (
|
||||
echo.
|
||||
echo [FEHLER] GitHub-Deployment wurde mit Exit-Code %EXITCODE% beendet.
|
||||
echo.
|
||||
pause
|
||||
)
|
||||
exit /b %EXITCODE%
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
# Vendoo 1.43.0
|
||||
# Vendoo 1.41.2
|
||||
|
||||
Vendoo 1.43.0 erweitert die kontrollierte Produktionsgrundlage um modulabhängige Navigation, ein optionales Produktbild Studio, ein neu aufgebautes Listing Studio und einen grafischen Ein-Klick-Updater. Der Container bleibt unveränderlich: Vendoo führt weder `git pull` noch Docker-Befehle aus, sondern delegiert freigegebene Deployments nach einem verifizierten Vorab-Backup an Coolify.
|
||||
|
||||
|
||||
## Release 1.43.0: Module-aware Navigation, Listing Studio & Updater UI
|
||||
|
||||
- deaktivierte Fachmodule verschwinden unmittelbar aus der linken Navigation
|
||||
- Modulstatus wird vor modulgeschützten APIs geladen, damit deaktivierte Module den App-Start nicht blockieren
|
||||
- Produktbild Studio als eigenes natives Modul mit `defaultEnabled: false`
|
||||
- überarbeitete Seite **Neuer Artikel** mit theme-konformen Flächen, Abständen und responsivem Arbeitsbereich
|
||||
- sticky Aktionsleiste: **Artikel generieren** und **Speichern** bleiben oben erreichbar
|
||||
- grafischer `UPDATE_VENDOO.bat`-Ablauf mit Phase, Detailtext, Prozentfortschritt, Live-Protokoll und sicherer Webhook-Eingabe
|
||||
|
||||
## Release 1.42.0: Production Operations & Controlled Update Completion
|
||||
|
||||
- persistente Deployment-Aufträge und Ereignisse in SQLite
|
||||
- idempotente Auslösung und Sperre gegen parallele Deployments
|
||||
- verifiziertes Vorab-Backup als fester Bestandteil des Auftrags
|
||||
- getrennte Zustände für Coolify-Annahme, laufendes Deployment, Readiness, Erfolg, Fehler und Rollbackbedarf
|
||||
- Erfolg erst nach Readiness und bestätigter Zielversion beziehungsweise expliziter externer Bestätigung
|
||||
- Produktionscheck für HTTPS, Cookies, Proxy, Hosts/Origins, persistente Pfade, SQLite, Backupziel, FLUX und Coolify-Konfiguration
|
||||
- kein Docker-Socket, kein `git pull`, kein `npm install` und keine Selbstüberschreibung im Produktivcontainer
|
||||
Vendoo 1.41.1 ist die korrigierte Produktionsgrundlage für den Betrieb über **Coolify**, GitHub und `https://vendoo.flatlined.de`. Der Container bleibt unveränderlich: Vendoo führt weder `git pull` noch Docker-Befehle aus, sondern delegiert freigegebene Deployments nach einem verifizierten Vorab-Backup an Coolify.
|
||||
|
||||
## Hotfix 1.41.2: native Module zuverlässig in Git
|
||||
|
||||
@@ -44,16 +24,16 @@ Runtimeordner werden in `.gitignore` ausschließlich am Projektroot ausgeschloss
|
||||
- keine Weitergabe des Docker-Sockets an Vendoo
|
||||
- genaue Anleitung: `docs/COOLIFY_PRODUCTION_1_41_0.md`
|
||||
|
||||
## Kontrollierte Updates
|
||||
## Kontrollierte Web-Updates
|
||||
|
||||
- Production Updater 3.3 mit SHA-256-geprüftem Payload und bytegeprüfter Manifest-Steuerdatei
|
||||
- keine destruktive Leerung der geklonten Git-Baseline
|
||||
- Pflichtdateien wie `db/schema.sql` werden vor Tests und Push geprüft
|
||||
- Pull Request und vollständige GitHub-Checks auf Windows und Linux
|
||||
- Coolify Auto Deploy nach erfolgreichem Merge nach `main`
|
||||
- exakte Produktionsbestätigung über `/readyz` und Zielversion
|
||||
- kein selbstverändernder Container, kein `git pull` in Produktion, kein Docker-Socket
|
||||
- technische Details: `docs/PRODUCTION_UPDATER_3_1.md`
|
||||
- geschütztes natives Modul `vendoo.deployments`
|
||||
- Coolify-Deploy-Webhook oder minimal berechtigter API-Token
|
||||
- Coolify-Zugangsdaten AES-256-GCM-verschlüsselt im Secret Store
|
||||
- exakte Administratorbestätigung `DEPLOY`
|
||||
- verpflichtendes Vorab-Backup vor dem Deploymentauftrag
|
||||
- GitHub-Repository, Produktionsbranch und Update-Kanal im Update Center
|
||||
- kein selbstverändernder Container, kein `git pull`, kein Docker-Socket
|
||||
- vollständiger Ablauf: `docs/WEB_UPDATE_CENTER_1_41_0.md`
|
||||
|
||||
## GitHub-, Merge- und Releaseprozess
|
||||
|
||||
@@ -67,10 +47,10 @@ Runtimeordner werden in `.gitignore` ausschließlich am Projektroot ausgeschloss
|
||||
|
||||
## Plattformstand
|
||||
|
||||
- 18 registrierte Module
|
||||
- 16 native Module, davon Produktbild Studio standardmäßig deaktiviert
|
||||
- 17 registrierte Module
|
||||
- 15 native Module
|
||||
- 2 transparente Legacy Bridges: System und Operations
|
||||
- 41 vollständig aufgelöste Capabilities
|
||||
- 38 vollständig aufgelöste Capabilities
|
||||
- geschützte Kernmodule und kontrollierbare optionale Module
|
||||
- `.vmod`-Import/Export, Quarantäne und Abhängigkeitssteuerung
|
||||
- Theme-, Security-, Identity-, Catalog-, Media-, Publishing-, AI-, FLUX- und Quality-Module bleiben enthalten
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions DisableDelayedExpansion
|
||||
|
||||
cd /d "%~dp0"
|
||||
if errorlevel 1 goto :path_error
|
||||
|
||||
set "VENDOO_PS=%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
if not exist "%VENDOO_PS%" goto :powershell_missing
|
||||
|
||||
echo Vendoo Production Updater 3.3 preflight...
|
||||
"%VENDOO_PS%" -NoLogo -NoProfile -ExecutionPolicy Bypass -File ".\scripts\updater\Vendoo.Updater.Preflight.ps1"
|
||||
set "VENDOO_RC=%ERRORLEVEL%"
|
||||
if not "%VENDOO_RC%"=="0" goto :preflight_failed
|
||||
|
||||
"%VENDOO_PS%" -NoLogo -NoProfile -ExecutionPolicy Bypass -STA -File ".\scripts\update-vendoo-gui.ps1"
|
||||
set "VENDOO_RC=%ERRORLEVEL%"
|
||||
if not "%VENDOO_RC%"=="0" goto :gui_failed
|
||||
|
||||
exit /b 0
|
||||
|
||||
:path_error
|
||||
echo.
|
||||
echo [ERROR] The Vendoo package directory could not be opened.
|
||||
echo Path: %~dp0
|
||||
echo.
|
||||
pause
|
||||
exit /b 30
|
||||
|
||||
:powershell_missing
|
||||
echo.
|
||||
echo [ERROR] Windows PowerShell 5.1 was not found.
|
||||
echo Expected: %VENDOO_PS%
|
||||
echo.
|
||||
pause
|
||||
exit /b 31
|
||||
|
||||
:preflight_failed
|
||||
echo.
|
||||
echo [ERROR] The Windows PowerShell preflight failed with exit code %VENDOO_RC%.
|
||||
echo Log: %CD%\logs\updater-preflight.log
|
||||
echo.
|
||||
pause
|
||||
exit /b %VENDOO_RC%
|
||||
|
||||
:gui_failed
|
||||
echo.
|
||||
echo [ERROR] The updater UI exited with code %VENDOO_RC%.
|
||||
echo Log: %CD%\logs\updater-bootstrap.log
|
||||
echo.
|
||||
pause
|
||||
exit /b %VENDOO_RC%
|
||||
@@ -4,91 +4,31 @@
|
||||
"title": "Vendoo Module Manifest",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schemaVersion",
|
||||
"id",
|
||||
"name",
|
||||
"version",
|
||||
"type",
|
||||
"status",
|
||||
"requires",
|
||||
"permissions",
|
||||
"provides",
|
||||
"consumes",
|
||||
"owns"
|
||||
],
|
||||
"required": ["schemaVersion", "id", "name", "version", "type", "status", "requires", "permissions", "provides", "consumes", "owns"],
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"const": 1
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"pattern": "^vendoo\\.[a-z][a-z0-9-]*$"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 80
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"core",
|
||||
"feature",
|
||||
"adapter",
|
||||
"extension-host"
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"enum": [
|
||||
"native",
|
||||
"legacy-bridge",
|
||||
"disabled"
|
||||
]
|
||||
},
|
||||
"defaultEnabled": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"requires": {
|
||||
"$ref": "#/$defs/moduleIds"
|
||||
},
|
||||
"permissions": {
|
||||
"$ref": "#/$defs/capabilities"
|
||||
},
|
||||
"provides": {
|
||||
"$ref": "#/$defs/capabilities"
|
||||
},
|
||||
"consumes": {
|
||||
"$ref": "#/$defs/capabilities"
|
||||
},
|
||||
"owns": {
|
||||
"$ref": "#/$defs/capabilities"
|
||||
}
|
||||
"schemaVersion": { "const": 1 },
|
||||
"id": { "type": "string", "pattern": "^vendoo\\.[a-z][a-z0-9-]*$" },
|
||||
"name": { "type": "string", "minLength": 1, "maxLength": 80 },
|
||||
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$" },
|
||||
"type": { "enum": ["core", "feature", "adapter", "extension-host"] },
|
||||
"status": { "enum": ["native", "legacy-bridge", "disabled"] },
|
||||
"description": { "type": "string", "maxLength": 500 },
|
||||
"requires": { "$ref": "#/$defs/moduleIds" },
|
||||
"permissions": { "$ref": "#/$defs/capabilities" },
|
||||
"provides": { "$ref": "#/$defs/capabilities" },
|
||||
"consumes": { "$ref": "#/$defs/capabilities" },
|
||||
"owns": { "$ref": "#/$defs/capabilities" }
|
||||
},
|
||||
"$defs": {
|
||||
"moduleIds": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^vendoo\\.[a-z][a-z0-9-]*$"
|
||||
}
|
||||
"items": { "type": "string", "pattern": "^vendoo\\.[a-z][a-z0-9-]*$" }
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)+$"
|
||||
}
|
||||
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*(?:\\.[a-z][a-z0-9-]*)+$" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,38 +296,6 @@ export function validateAuthToken(token) {
|
||||
return row;
|
||||
}
|
||||
|
||||
|
||||
export function listInvitations(limit = 100) {
|
||||
const safeLimit = Math.max(1, Math.min(500, Number(limit) || 100));
|
||||
return db.prepare(`SELECT id, email, role, used, expires_at, created_by, created_at,
|
||||
CASE
|
||||
WHEN used = 1 THEN 'revoked_or_used'
|
||||
WHEN expires_at <= datetime('now') THEN 'expired'
|
||||
ELSE 'pending'
|
||||
END AS status
|
||||
FROM auth_tokens
|
||||
WHERE type = 'invite'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?`).all(safeLimit);
|
||||
}
|
||||
|
||||
export function getInvitation(id) {
|
||||
return db.prepare(`SELECT id, email, role, used, expires_at, created_by, created_at,
|
||||
CASE
|
||||
WHEN used = 1 THEN 'revoked_or_used'
|
||||
WHEN expires_at <= datetime('now') THEN 'expired'
|
||||
ELSE 'pending'
|
||||
END AS status
|
||||
FROM auth_tokens WHERE id = ? AND type = 'invite'`).get(Number(id));
|
||||
}
|
||||
|
||||
export function revokeInvitation(id) {
|
||||
const invitation = getInvitation(id);
|
||||
if (!invitation) return null;
|
||||
if (invitation.status === 'pending') db.prepare('UPDATE auth_tokens SET used = 1 WHERE id = ? AND type = ?').run(Number(id), 'invite');
|
||||
return { ...invitation, status: invitation.status === 'pending' ? 'revoked' : invitation.status, used: 1 };
|
||||
}
|
||||
|
||||
// --- Rate Limiting ---
|
||||
const rateLimits = new Map();
|
||||
|
||||
|
||||
@@ -163,11 +163,7 @@ export function buildModulePackage({ manifest, runtime = { mode: 'builtin-refere
|
||||
const buffer = Buffer.isBuffer(file.content) ? file.content : Buffer.from(String(file.content || ''), 'utf8');
|
||||
return { path, sha256: sha256(buffer), size: buffer.length, content: buffer.toString('base64') };
|
||||
});
|
||||
// Der Digest muss auf demselben normalisierten Manifest basieren wie die spätere
|
||||
// Validierung. Sonst würden additive Manifest-Defaults (z. B. defaultEnabled)
|
||||
// den Digest nachträglich verändern.
|
||||
const normalizedManifest = validateModuleManifest(manifest);
|
||||
const draft = { format: MODULE_PACKAGE_FORMAT, schemaVersion: MODULE_PACKAGE_SCHEMA, manifest: normalizedManifest, runtime, files: normalizedFiles, createdAt, integrity: { algorithm: 'sha256', digest: '' }, signature: null };
|
||||
const draft = { format: MODULE_PACKAGE_FORMAT, schemaVersion: MODULE_PACKAGE_SCHEMA, manifest, runtime, files: normalizedFiles, createdAt, integrity: { algorithm: 'sha256', digest: '' }, signature: null };
|
||||
draft.integrity.digest = calculatePackageDigest(draft);
|
||||
return validateModulePackage(draft, { allowBuiltinReference: true });
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ export function validateModuleManifest(manifest) {
|
||||
version: String(manifest.version || '').trim(),
|
||||
type: String(manifest.type || '').trim(),
|
||||
status: String(manifest.status || 'native').trim(),
|
||||
defaultEnabled: manifest.defaultEnabled !== false,
|
||||
description: String(manifest.description || '').trim(),
|
||||
requires: Object.freeze(uniqueStrings(manifest.requires || [], 'requires', MODULE_ID)),
|
||||
permissions: Object.freeze(uniqueStrings(manifest.permissions || [], 'permissions')),
|
||||
|
||||
@@ -84,7 +84,6 @@ export function createPlatformKernel({ version, hasPermission, moduleTrustKeys =
|
||||
features.define('modules.package-format', { defaultValue: true, owner: 'vendoo.module-manager', description: 'Aktiviert validierte .vmod-Pakete mit SHA-256-Integrität und optionaler Ed25519-Signatur.' });
|
||||
features.define('publishing.native-module', { defaultValue: true, owner: 'vendoo.publishing', description: 'Aktiviert native Publishing-Verträge.' });
|
||||
features.define('ai.native-module', { defaultValue: true, owner: 'vendoo.ai', description: 'Aktiviert native AI-Verträge.' });
|
||||
features.define('product-images.optional-module', { defaultValue: false, owner: 'vendoo.product-image-tools', description: 'Aktiviert optionale Ghost-Mannequin- und Flatlay-Produktbildvarianten.' });
|
||||
features.define('flux.native-module', { defaultValue: true, owner: 'vendoo.flux-studio', description: 'Aktiviert native FLUX-Verträge.' });
|
||||
features.define('quality.native-module', { defaultValue: true, owner: 'vendoo.quality', description: 'Aktiviert native Quality-Verträge.' });
|
||||
features.define('deployments.coolify', { defaultValue: true, owner: 'vendoo.deployments', description: 'Aktiviert kontrollierte Coolify-Deployments ohne Docker-Socket.' });
|
||||
@@ -94,7 +93,7 @@ export function createPlatformKernel({ version, hasPermission, moduleTrustKeys =
|
||||
.split(',').map(value => value.trim()).filter(Boolean));
|
||||
for (const module of loadBuiltInModules()) {
|
||||
const forcedEnabled = PROTECTED_MODULE_IDS.includes(module.manifest.id);
|
||||
const defaultEnabled = module.manifest.status !== 'disabled' && module.manifest.defaultEnabled !== false && !initialDisabledModules.has(module.manifest.id);
|
||||
const defaultEnabled = module.manifest.status !== 'disabled' && !initialDisabledModules.has(module.manifest.id);
|
||||
const enabled = forcedEnabled || moduleStateStore.isEnabled(module.manifest.id, defaultEnabled);
|
||||
modules.register(module, { enabled });
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { createUsersModule } from './users/index.mjs';
|
||||
import { createModuleManagerModule } from './module-manager/index.mjs';
|
||||
import { createAiModule } from './ai/index.mjs';
|
||||
import { createFluxStudioModule } from './flux-studio/index.mjs';
|
||||
import { createProductImageToolsModule } from './product-image-tools/index.mjs';
|
||||
import { createQualityModule } from './quality/index.mjs';
|
||||
import { createPublishingModule } from './publishing/index.mjs';
|
||||
import { createDeploymentsModule } from './deployments/index.mjs';
|
||||
@@ -34,6 +33,6 @@ export function loadBuiltInModules() {
|
||||
return [
|
||||
...legacyModuleNames.map(legacyModule),
|
||||
createSecurityModule(), createAuthModule(), createSessionsModule(), createUsersModule(), createSettingsModule(), createThemesModule(),
|
||||
createMediaModule(), createListingsModule(), createInventoryModule(), createAiModule(), createProductImageToolsModule(), createFluxStudioModule(), createQualityModule(), createPublishingModule(), createModuleManagerModule(), createDeploymentsModule(),
|
||||
createMediaModule(), createListingsModule(), createInventoryModule(), createAiModule(), createFluxStudioModule(), createQualityModule(), createPublishingModule(), createModuleManagerModule(), createDeploymentsModule(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function createDeploymentsModule() {
|
||||
manifest,
|
||||
lifecycle: {
|
||||
async start() { service = createDeploymentService(adapters); },
|
||||
async stop() { service?.stop?.(); service = null; },
|
||||
async stop() { service = null; },
|
||||
async health() { return getDeploymentService().health(); },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
"schemaVersion": 1,
|
||||
"id": "vendoo.deployments",
|
||||
"name": "Deployments & Updates",
|
||||
"version": "1.1.0",
|
||||
"version": "1.0.0",
|
||||
"type": "core",
|
||||
"status": "native",
|
||||
"description": "Persistente, kontrollierte Coolify-Deployments mit Vorab-Backup, Idempotenz, Statusverfolgung, Readiness-, Zielversions- und Produktionschecks ohne Docker-Socket.",
|
||||
"description": "Kontrollierte Coolify-Deployments, verschlüsselte Deployment-Zugangsdaten und sichere Web-Updates ohne Docker-Socket.",
|
||||
"requires": [
|
||||
"vendoo.security",
|
||||
"vendoo.settings",
|
||||
@@ -18,8 +18,6 @@
|
||||
"provides": [
|
||||
"deployments.control",
|
||||
"deployments.coolify",
|
||||
"deployments.production-checks",
|
||||
"deployments.state-machine",
|
||||
"updates.web"
|
||||
],
|
||||
"consumes": [
|
||||
@@ -30,9 +28,7 @@
|
||||
],
|
||||
"owns": [
|
||||
"deployments.configuration",
|
||||
"deployments.events",
|
||||
"deployments.history",
|
||||
"deployments.production-checks",
|
||||
"updates.coolify"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { db } from '../../../lib/db.mjs';
|
||||
import { PlatformError } from '../../kernel/errors.mjs';
|
||||
|
||||
import {
|
||||
ACTIVE_DEPLOYMENT_STATES,
|
||||
DEPLOYMENT_STATES,
|
||||
canTransitionDeployment,
|
||||
isActiveDeploymentState,
|
||||
} from './state-machine.mjs';
|
||||
|
||||
function parseJson(value, fallback) {
|
||||
try { return value ? JSON.parse(value) : fallback; } catch { return fallback; }
|
||||
}
|
||||
|
||||
function serializeJson(value, fallback) {
|
||||
try { return JSON.stringify(value ?? fallback); } catch { return JSON.stringify(fallback); }
|
||||
}
|
||||
|
||||
function normalizeRun(row) {
|
||||
if (!row) return null;
|
||||
return {
|
||||
...row,
|
||||
request: parseJson(row.request_json, {}),
|
||||
result: parseJson(row.result_json, {}),
|
||||
active: isActiveDeploymentState(row.state),
|
||||
};
|
||||
}
|
||||
|
||||
function fail(message, code = 'DEPLOYMENT_STATE_INVALID', status = 409, details = null) {
|
||||
throw new PlatformError(message, { code, status, expose: true, details });
|
||||
}
|
||||
|
||||
export function createDeploymentRepository(database = db) {
|
||||
const insertRun = database.prepare(`INSERT INTO deployment_runs (
|
||||
id, idempotency_key, state, provider, trigger_mode, current_version, target_version,
|
||||
build_revision_before, requested_by, reason, github_repository, github_branch,
|
||||
request_json, timeout_at, started_at, updated_at
|
||||
) VALUES (?, ?, 'checking', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`);
|
||||
|
||||
const insertEvent = database.prepare(`INSERT INTO deployment_events
|
||||
(run_id, state, event_type, message, details_json)
|
||||
VALUES (?, ?, ?, ?, ?)`);
|
||||
|
||||
const getRunStmt = database.prepare('SELECT * FROM deployment_runs WHERE id = ?');
|
||||
const getIdempotentStmt = database.prepare('SELECT * FROM deployment_runs WHERE idempotency_key = ?');
|
||||
const getActiveStmt = database.prepare(`SELECT * FROM deployment_runs
|
||||
WHERE state IN (${ACTIVE_DEPLOYMENT_STATES.map(() => '?').join(',')})
|
||||
ORDER BY created_at DESC LIMIT 1`);
|
||||
|
||||
function addEvent(runId, state, eventType, message, details = {}) {
|
||||
insertEvent.run(runId, state, String(eventType || 'state').slice(0, 80), String(message || '').slice(0, 1000), serializeJson(details, {}));
|
||||
}
|
||||
|
||||
function createRun(input) {
|
||||
const id = input.id || randomUUID();
|
||||
const existing = getIdempotentStmt.get(input.idempotencyKey);
|
||||
if (existing) return { run: normalizeRun(existing), reused: true };
|
||||
const active = getActiveStmt.get(...ACTIVE_DEPLOYMENT_STATES);
|
||||
if (active) fail('Es läuft bereits ein Deployment oder es ist ein Rollback erforderlich.', 'DEPLOYMENT_ALREADY_ACTIVE', 409, { run_id: active.id, state: active.state });
|
||||
|
||||
try {
|
||||
insertRun.run(
|
||||
id,
|
||||
input.idempotencyKey,
|
||||
input.provider || 'coolify',
|
||||
input.triggerMode || 'webhook',
|
||||
input.currentVersion,
|
||||
input.targetVersion,
|
||||
input.buildRevisionBefore || null,
|
||||
input.requestedBy || null,
|
||||
input.reason || 'manual',
|
||||
input.githubRepository || null,
|
||||
input.githubBranch || null,
|
||||
serializeJson(input.request || {}, {}),
|
||||
input.timeoutAt || null,
|
||||
);
|
||||
} catch (error) {
|
||||
if (String(error?.message || '').includes('UNIQUE')) {
|
||||
const raced = getIdempotentStmt.get(input.idempotencyKey);
|
||||
if (raced) return { run: normalizeRun(raced), reused: true };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
addEvent(id, 'checking', 'run.created', 'Deployment-Auftrag wurde angelegt.', {
|
||||
target_version: input.targetVersion,
|
||||
trigger_mode: input.triggerMode,
|
||||
});
|
||||
return { run: normalizeRun(getRunStmt.get(id)), reused: false };
|
||||
}
|
||||
|
||||
function getRun(id) { return normalizeRun(getRunStmt.get(id)); }
|
||||
function findByIdempotencyKey(key) { return normalizeRun(getIdempotentStmt.get(key)); }
|
||||
function getActiveRun() { return normalizeRun(getActiveStmt.get(...ACTIVE_DEPLOYMENT_STATES)); }
|
||||
|
||||
function listRuns(limit = 25) {
|
||||
const safeLimit = Math.max(1, Math.min(200, Number(limit) || 25));
|
||||
return database.prepare('SELECT * FROM deployment_runs ORDER BY created_at DESC LIMIT ?').all(safeLimit).map(normalizeRun);
|
||||
}
|
||||
|
||||
function listEvents(runId, limit = 200) {
|
||||
const safeLimit = Math.max(1, Math.min(1000, Number(limit) || 200));
|
||||
return database.prepare(`SELECT * FROM deployment_events WHERE run_id = ? ORDER BY id ASC LIMIT ?`).all(runId, safeLimit)
|
||||
.map(row => ({ ...row, details: parseJson(row.details_json, {}) }));
|
||||
}
|
||||
|
||||
function transition(runId, nextState, { message = '', eventType = 'state.changed', details = {}, patch = {} } = {}) {
|
||||
if (!DEPLOYMENT_STATES.includes(nextState)) fail('Unbekannter Deployment-Zustand.', 'DEPLOYMENT_STATE_UNKNOWN', 500, { next_state: nextState });
|
||||
const current = getRunStmt.get(runId);
|
||||
if (!current) fail('Deployment-Auftrag wurde nicht gefunden.', 'DEPLOYMENT_RUN_NOT_FOUND', 404);
|
||||
if (current.state === nextState) return normalizeRun(current);
|
||||
if (!canTransitionDeployment(current.state, nextState)) {
|
||||
fail(`Ungültiger Deployment-Übergang von ${current.state} nach ${nextState}.`, 'DEPLOYMENT_TRANSITION_BLOCKED', 409, {
|
||||
run_id: runId, from: current.state, to: nextState,
|
||||
});
|
||||
}
|
||||
|
||||
const completed = ['succeeded', 'failed'].includes(nextState);
|
||||
const columns = ['state = ?', "updated_at = datetime('now')"];
|
||||
const values = [nextState];
|
||||
const allowedPatch = new Set([
|
||||
'deployment_id', 'backup_id', 'result_json', 'error_code', 'error_message',
|
||||
'build_revision_after', 'timeout_at',
|
||||
]);
|
||||
for (const [key, value] of Object.entries(patch || {})) {
|
||||
if (!allowedPatch.has(key)) continue;
|
||||
columns.push(`${key} = ?`);
|
||||
values.push(key.endsWith('_json') ? serializeJson(value, {}) : value);
|
||||
}
|
||||
if (completed) columns.push("completed_at = datetime('now')");
|
||||
values.push(runId);
|
||||
database.prepare(`UPDATE deployment_runs SET ${columns.join(', ')} WHERE id = ?`).run(...values);
|
||||
addEvent(runId, nextState, eventType, message || `Zustand: ${nextState}`, details);
|
||||
return normalizeRun(getRunStmt.get(runId));
|
||||
}
|
||||
|
||||
function patchRun(runId, patch = {}, { eventType = null, message = '', details = {} } = {}) {
|
||||
const current = getRunStmt.get(runId);
|
||||
if (!current) fail('Deployment-Auftrag wurde nicht gefunden.', 'DEPLOYMENT_RUN_NOT_FOUND', 404);
|
||||
const allowedPatch = new Set([
|
||||
'deployment_id', 'backup_id', 'result_json', 'error_code', 'error_message',
|
||||
'build_revision_after', 'timeout_at',
|
||||
]);
|
||||
const columns = ["updated_at = datetime('now')"];
|
||||
const values = [];
|
||||
for (const [key, value] of Object.entries(patch || {})) {
|
||||
if (!allowedPatch.has(key)) continue;
|
||||
columns.push(`${key} = ?`);
|
||||
values.push(key.endsWith('_json') ? serializeJson(value, {}) : value);
|
||||
}
|
||||
values.push(runId);
|
||||
database.prepare(`UPDATE deployment_runs SET ${columns.join(', ')} WHERE id = ?`).run(...values);
|
||||
if (eventType) addEvent(runId, current.state, eventType, message || eventType, details);
|
||||
return normalizeRun(getRunStmt.get(runId));
|
||||
}
|
||||
|
||||
function recordBackupVerification({ backupId, runId = null, status, sha256 = null, fileSize = 0, sqliteIntegrity = null, manifest = {}, errors = [], warnings = [] }) {
|
||||
const id = randomUUID();
|
||||
database.prepare(`INSERT INTO backup_verifications
|
||||
(id, backup_id, deployment_run_id, status, sha256, file_size, sqlite_integrity, manifest_json, errors_json, warnings_json, verified_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`).run(
|
||||
id, backupId, runId, status, sha256, Number(fileSize) || 0, sqliteIntegrity,
|
||||
serializeJson(manifest, {}), serializeJson(errors, []), serializeJson(warnings, []),
|
||||
);
|
||||
return database.prepare('SELECT * FROM backup_verifications WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
function createProductionCheck({ createdBy = null, results = [] } = {}) {
|
||||
const id = randomUUID();
|
||||
const passed = results.filter(item => item.status === 'pass').length;
|
||||
const warnings = results.filter(item => item.status === 'warning').length;
|
||||
const failed = results.filter(item => item.status === 'fail').length;
|
||||
const overall = failed ? 'failed' : (warnings ? 'warning' : 'passed');
|
||||
database.prepare(`INSERT INTO production_checks
|
||||
(id, overall_status, passed_count, warning_count, failed_count, results_json, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, overall, passed, warnings, failed, serializeJson(results, []), createdBy);
|
||||
return { id, overall_status: overall, passed_count: passed, warning_count: warnings, failed_count: failed, results };
|
||||
}
|
||||
|
||||
function latestProductionCheck() {
|
||||
const row = database.prepare('SELECT * FROM production_checks ORDER BY created_at DESC LIMIT 1').get();
|
||||
return row ? { ...row, results: parseJson(row.results_json, []) } : null;
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
createRun, getRun, findByIdempotencyKey, getActiveRun, listRuns, listEvents,
|
||||
transition, patchRun, addEvent, recordBackupVerification,
|
||||
createProductionCheck, latestProductionCheck,
|
||||
});
|
||||
}
|
||||
|
||||
export const deploymentRepository = createDeploymentRepository();
|
||||
@@ -1,5 +1,4 @@
|
||||
import { objectSchema } from '../../kernel/input-schema.mjs';
|
||||
import { PlatformError } from '../../kernel/errors.mjs';
|
||||
import { getDeploymentService } from './index.mjs';
|
||||
|
||||
const settingsSchema = objectSchema({
|
||||
@@ -9,9 +8,6 @@ const settingsSchema = objectSchema({
|
||||
trigger_mode: { type: 'string', enum: ['webhook', 'api'] },
|
||||
deploy_method: { type: 'string', enum: ['GET', 'POST'] },
|
||||
deploy_endpoint: { type: 'string', maxLength: 500 },
|
||||
deployment_status_endpoint: { type: 'string', maxLength: 500 },
|
||||
deployment_timeout_seconds: { type: 'number', integer: true, min: 60, max: 7200 },
|
||||
health_poll_seconds: { type: 'number', integer: true, min: 5, max: 300 },
|
||||
github_repository: { type: 'string', maxLength: 200 },
|
||||
github_branch: { type: 'string', maxLength: 120 },
|
||||
ghcr_image: { type: 'string', maxLength: 300 },
|
||||
@@ -21,33 +17,15 @@ const settingsSchema = objectSchema({
|
||||
api_token: { type: 'string', maxLength: 16384, trim: false },
|
||||
deploy_webhook_url: { type: 'string', maxLength: 2000, trim: false },
|
||||
});
|
||||
|
||||
const triggerSchema = objectSchema({
|
||||
force: { type: 'boolean' },
|
||||
confirmation: { type: 'string', required: true, maxLength: 20 },
|
||||
reason: { type: 'string', maxLength: 120 },
|
||||
idempotency_key: { type: 'string', maxLength: 160 },
|
||||
target_version: { type: 'string', maxLength: 40 },
|
||||
force: { type: 'boolean' }, confirmation: { type: 'string', required: true, maxLength: 20 }, reason: { type: 'string', maxLength: 120 },
|
||||
});
|
||||
|
||||
const confirmSchema = objectSchema({
|
||||
confirmation: { type: 'string', required: true, maxLength: 20 },
|
||||
outcome: { type: 'string', required: true, enum: ['running', 'succeeded', 'failed'] },
|
||||
message: { type: 'string', maxLength: 500 },
|
||||
});
|
||||
|
||||
function requireRun(run) {
|
||||
if (!run) throw new PlatformError('Deployment-Auftrag wurde nicht gefunden.', {
|
||||
code: 'DEPLOYMENT_RUN_NOT_FOUND', status: 404, expose: true,
|
||||
});
|
||||
return run;
|
||||
}
|
||||
|
||||
export function registerDeploymentRoutes({ app, routes, deps }) {
|
||||
routes.register(app, {
|
||||
id: 'deployments.status.read', method: 'GET', path: '/api/admin/deployment', owner: 'vendoo.deployments',
|
||||
policy: 'updates.manage', audit: 'deployments.status.read', rateLimit: 'administration', csrf: false, stability: 'stable',
|
||||
handler: async (_req, res) => res.json(await getDeploymentService().status()),
|
||||
handler: async (_req, res) => res.json(getDeploymentService().status()),
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'deployments.settings.update', method: 'PUT', path: '/api/admin/deployment', owner: 'vendoo.deployments',
|
||||
@@ -70,63 +48,15 @@ export function registerDeploymentRoutes({ app, routes, deps }) {
|
||||
routes.register(app, {
|
||||
id: 'deployments.updates.check', method: 'POST', path: '/api/admin/deployment/updates/check', owner: 'vendoo.deployments',
|
||||
policy: 'updates.manage', audit: 'deployments.updates.checked', rateLimit: 'administration', csrf: true, stability: 'stable',
|
||||
handler: async (req, res) => {
|
||||
const result = await getDeploymentService().checkUpdates();
|
||||
deps.auditLog(req.user.id, req.user.email, 'deployment.updates.checked', 'deployment', 'manifest', JSON.stringify({ update_available: result.update_available || false }), deps.getClientIp(req));
|
||||
res.json(result);
|
||||
},
|
||||
handler: async (_req, res) => res.json(await getDeploymentService().checkUpdates()),
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'deployments.trigger', method: 'POST', path: '/api/admin/deployment/trigger', owner: 'vendoo.deployments',
|
||||
policy: 'updates.manage', audit: 'deployments.triggered', rateLimit: 'administration', csrf: true, stability: 'stable', input: triggerSchema,
|
||||
handler: async (req, res) => {
|
||||
const idempotencyKey = req.get('Idempotency-Key') || req.validatedBody?.idempotency_key || '';
|
||||
const result = await getDeploymentService().trigger(req.validatedBody || {}, { userId: req.user.id, idempotencyKey });
|
||||
deps.auditLog(req.user.id, req.user.email, 'deployment.triggered', 'deployment', result.id, JSON.stringify({ backup_id: result.backup_id, branch: result.github_branch, target_version: result.target_version, reused: result.reused }), deps.getClientIp(req));
|
||||
res.status(result.reused ? 200 : 202).json(result);
|
||||
const result = await getDeploymentService().trigger(req.validatedBody || {});
|
||||
deps.auditLog(req.user.id, req.user.email, 'deployment.triggered', 'deployment', 'coolify', JSON.stringify({ backup_id: result.backup_id, branch: result.github_branch }), deps.getClientIp(req));
|
||||
res.status(202).json(result);
|
||||
},
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'deployments.runs.list', method: 'GET', path: '/api/admin/deployment/runs', owner: 'vendoo.deployments',
|
||||
policy: 'updates.manage', audit: 'deployments.runs.read', rateLimit: 'administration', csrf: false, stability: 'stable',
|
||||
handler: async (req, res) => res.json({ runs: getDeploymentService().listRuns(Number(req.query.limit) || 20) }),
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'deployments.runs.read', method: 'GET', path: '/api/admin/deployment/runs/:id', owner: 'vendoo.deployments',
|
||||
policy: 'updates.manage', audit: 'deployments.run.read', rateLimit: 'administration', csrf: false, stability: 'stable',
|
||||
handler: async (req, res) => res.json(requireRun(getDeploymentService().getRun(req.params.id))),
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'deployments.runs.refresh', method: 'POST', path: '/api/admin/deployment/runs/:id/refresh', owner: 'vendoo.deployments',
|
||||
policy: 'updates.manage', audit: 'deployments.run.refreshed', rateLimit: 'administration', csrf: true, stability: 'stable',
|
||||
handler: async (req, res) => {
|
||||
requireRun(getDeploymentService().getRun(req.params.id));
|
||||
const result = await getDeploymentService().refreshRun(req.params.id);
|
||||
deps.auditLog(req.user.id, req.user.email, 'deployment.run.refreshed', 'deployment', req.params.id, JSON.stringify({ state: result.state }), deps.getClientIp(req));
|
||||
res.json(result);
|
||||
},
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'deployments.runs.confirm', method: 'POST', path: '/api/admin/deployment/runs/:id/confirm', owner: 'vendoo.deployments',
|
||||
policy: 'updates.manage', audit: 'deployments.run.confirmed', rateLimit: 'administration', csrf: true, stability: 'stable', input: confirmSchema,
|
||||
handler: async (req, res) => {
|
||||
const result = await getDeploymentService().confirmExternal(req.params.id, req.validatedBody || {});
|
||||
deps.auditLog(req.user.id, req.user.email, 'deployment.run.confirmed', 'deployment', req.params.id, JSON.stringify({ outcome: req.validatedBody?.outcome, state: result.state }), deps.getClientIp(req));
|
||||
res.json(result);
|
||||
},
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'deployments.production.check.run', method: 'POST', path: '/api/admin/deployment/production-check', owner: 'vendoo.deployments',
|
||||
policy: 'updates.manage', audit: 'deployments.production.checked', rateLimit: 'administration', csrf: true, stability: 'stable',
|
||||
handler: async (req, res) => {
|
||||
const result = await getDeploymentService().runProductionCheck({ userId: req.user.id });
|
||||
deps.auditLog(req.user.id, req.user.email, 'deployment.production.checked', 'deployment', result.id, JSON.stringify({ overall_status: result.overall_status, failed_count: result.failed_count }), deps.getClientIp(req));
|
||||
res.json(result);
|
||||
},
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'deployments.production.check.read', method: 'GET', path: '/api/admin/deployment/production-check', owner: 'vendoo.deployments',
|
||||
policy: 'updates.manage', audit: 'deployments.production.check.read', rateLimit: 'administration', csrf: false, stability: 'stable',
|
||||
handler: async (_req, res) => res.json({ check: getDeploymentService().latestProductionCheck() }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { mkdirSync, existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { readRuntimeConfig, setConfigValue, writeRuntimeConfig } from '../../../lib/config-store.mjs';
|
||||
import { getSecret, getSecretMetadata, setSecret } from '../../../lib/secret-store.mjs';
|
||||
import {
|
||||
APP_ROOT, APP_VERSION, BACKUP_DIR, DATA_ROOT, DB_PATH, LOG_DIR,
|
||||
MODULES_DIR, MODULE_STATE_PATH, OPERATIONS_DIR, UPLOADS_DIR,
|
||||
runtimePathSummary, verifyRuntimeWritable,
|
||||
} from '../../../lib/runtime-paths.mjs';
|
||||
import { db } from '../../../lib/db.mjs';
|
||||
import { getSecret, getSecretMetadata, hasSecret, setSecret } from '../../../lib/secret-store.mjs';
|
||||
import { OPERATIONS_DIR, APP_VERSION } from '../../../lib/runtime-paths.mjs';
|
||||
import { PlatformError } from '../../kernel/errors.mjs';
|
||||
import { deploymentRepository } from './repository.mjs';
|
||||
|
||||
const LAST_DEPLOYMENT_PATH = join(OPERATIONS_DIR, 'last-deployment-request.json');
|
||||
const PROVIDERS = new Set(['none', 'coolify']);
|
||||
const MODES = new Set(['webhook', 'api']);
|
||||
const CHANNELS = new Set(['stable', 'beta']);
|
||||
const METHODS = new Set(['GET', 'POST']);
|
||||
const TERMINAL_COOLIFY_SUCCESS = new Set(['finished', 'success', 'succeeded', 'completed', 'healthy']);
|
||||
const TERMINAL_COOLIFY_FAILURE = new Set(['failed', 'error', 'cancelled', 'canceled', 'stopped']);
|
||||
const MASKED_SECRET = '••••••••';
|
||||
|
||||
function fail(message, code = 'DEPLOYMENT_CONFIGURATION_INVALID', status = 400, details = null) {
|
||||
throw new PlatformError(message, { code, status, expose: true, details });
|
||||
@@ -37,12 +28,6 @@ function cleanUuid(value) {
|
||||
return text;
|
||||
}
|
||||
|
||||
function cleanIdempotencyKey(value) {
|
||||
const text = clean(value || randomUUID(), 160);
|
||||
if (!/^[a-zA-Z0-9._:-]{8,160}$/.test(text)) fail('Idempotency-Key besitzt ein ungültiges Format.', 'IDEMPOTENCY_KEY_INVALID');
|
||||
return text;
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value) {
|
||||
const text = clean(value, 1000).replace(/\/$/, '');
|
||||
if (!text) return '';
|
||||
@@ -69,28 +54,16 @@ function normalizeWebhookUrl(value) {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function normalizeEndpoint(value, fallback, placeholders = []) {
|
||||
const text = clean(value || fallback, 500);
|
||||
if (!text.startsWith('/') || text.startsWith('//') || text.includes('..')) fail('Coolify API-Endpunkt muss ein sicherer relativer Pfad sein.');
|
||||
if (/\r|\n|\0/.test(text)) fail('Coolify API-Endpunkt ist ungültig.');
|
||||
const unknown = [...text.matchAll(/\{([^}]+)\}/g)].map(match => match[1]).filter(name => !placeholders.includes(name));
|
||||
if (unknown.length) fail(`Unbekannter Platzhalter im Coolify-Endpunkt: ${unknown.join(', ')}`);
|
||||
return text;
|
||||
function readLastDeployment() {
|
||||
try { return existsSync(LAST_DEPLOYMENT_PATH) ? JSON.parse(readFileSync(LAST_DEPLOYMENT_PATH, 'utf8')) : null; }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
function boolEnv(name, fallback = false) {
|
||||
const raw = process.env[name];
|
||||
if (raw === undefined || raw === '') return fallback;
|
||||
return /^(1|true|yes|on)$/i.test(String(raw));
|
||||
}
|
||||
|
||||
function intEnv(name, fallback, min, max) {
|
||||
const value = Number(process.env[name]);
|
||||
return Number.isFinite(value) ? Math.max(min, Math.min(max, Math.trunc(value))) : fallback;
|
||||
}
|
||||
|
||||
function buildRevision() {
|
||||
return clean(process.env.VENDOO_BUILD_REVISION || process.env.GIT_COMMIT || process.env.SOURCE_COMMIT || '', 160) || null;
|
||||
function writeLastDeployment(value) {
|
||||
mkdirSync(dirname(LAST_DEPLOYMENT_PATH), { recursive: true });
|
||||
const temporary = `${LAST_DEPLOYMENT_PATH}.${process.pid}.tmp`;
|
||||
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
||||
renameSync(temporary, LAST_DEPLOYMENT_PATH);
|
||||
}
|
||||
|
||||
function currentSettings() {
|
||||
@@ -105,288 +78,88 @@ function currentSettings() {
|
||||
trigger_mode: mode,
|
||||
deploy_method: method,
|
||||
deploy_endpoint: String(process.env.COOLIFY_DEPLOY_ENDPOINT || '/api/v1/deploy?uuid={uuid}&force={force}'),
|
||||
deployment_status_endpoint: String(process.env.COOLIFY_DEPLOYMENT_STATUS_ENDPOINT || '/api/v1/deployments/{deployment_id}'),
|
||||
github_repository: String(process.env.VENDOO_GITHUB_REPOSITORY || 'Masterluke77/vendoo'),
|
||||
github_branch: String(process.env.VENDOO_GITHUB_BRANCH || 'main'),
|
||||
ghcr_image: String(process.env.VENDOO_GHCR_IMAGE || 'ghcr.io/masterluke77/vendoo'),
|
||||
update_channel: channel,
|
||||
backup_before_deploy: !/^(0|false|no)$/i.test(String(process.env.VENDOO_BACKUP_BEFORE_DEPLOY || 'true')),
|
||||
backup_uploads: boolEnv('VENDOO_DEPLOY_BACKUP_UPLOADS', false),
|
||||
deployment_timeout_seconds: intEnv('VENDOO_DEPLOY_TIMEOUT_SECONDS', 900, 60, 7200),
|
||||
health_poll_seconds: intEnv('VENDOO_DEPLOY_HEALTH_POLL_SECONDS', 15, 5, 300),
|
||||
backup_uploads: /^(1|true|yes)$/i.test(String(process.env.VENDOO_DEPLOY_BACKUP_UPLOADS || 'false')),
|
||||
api_token: getSecretMetadata('COOLIFY_API_TOKEN'),
|
||||
deploy_webhook: getSecretMetadata('COOLIFY_DEPLOY_WEBHOOK_URL'),
|
||||
};
|
||||
}
|
||||
|
||||
function configuredState(settings) {
|
||||
return settings.provider === 'coolify' && (
|
||||
function publicSettings() {
|
||||
const settings = currentSettings();
|
||||
const configured = settings.provider === 'coolify' && (
|
||||
(settings.trigger_mode === 'webhook' && settings.deploy_webhook.configured) ||
|
||||
(settings.trigger_mode === 'api' && settings.coolify_url && settings.resource_uuid && settings.api_token.configured)
|
||||
);
|
||||
}
|
||||
|
||||
function publicSettings(repository = deploymentRepository) {
|
||||
const settings = currentSettings();
|
||||
return {
|
||||
...settings,
|
||||
configured: configuredState(settings),
|
||||
configured,
|
||||
api_token: { configured: settings.api_token.configured, masked: settings.api_token.masked, provider: settings.api_token.provider },
|
||||
deploy_webhook: { configured: settings.deploy_webhook.configured, masked: settings.deploy_webhook.masked, provider: settings.deploy_webhook.provider },
|
||||
current_version: APP_VERSION,
|
||||
build_revision: buildRevision(),
|
||||
active_run: repository.getActiveRun(),
|
||||
recent_runs: repository.listRuns(10).map(run => ({ ...run, events: repository.listEvents(run.id, 50) })),
|
||||
last_deployment: readLastDeployment(),
|
||||
docker_socket_access: false,
|
||||
};
|
||||
}
|
||||
|
||||
function buildApiUrl(settings, template, replacements) {
|
||||
let path = template;
|
||||
for (const [name, value] of Object.entries(replacements)) path = path.replaceAll(`{${name}}`, encodeURIComponent(String(value ?? '')));
|
||||
function buildApiDeployUrl(settings, force) {
|
||||
const endpoint = settings.deploy_endpoint || '/api/v1/deploy?uuid={uuid}&force={force}';
|
||||
if (!endpoint.startsWith('/')) fail('Coolify API-Endpunkt muss mit / beginnen.');
|
||||
if (/\r|\n|\0/.test(endpoint) || endpoint.includes('..')) fail('Coolify API-Endpunkt ist ungültig.');
|
||||
const path = endpoint
|
||||
.replaceAll('{uuid}', encodeURIComponent(settings.resource_uuid))
|
||||
.replaceAll('{force}', force ? 'true' : 'false');
|
||||
return `${settings.coolify_url}${path}`;
|
||||
}
|
||||
|
||||
function buildApiDeployUrl(settings, force) {
|
||||
const endpoint = normalizeEndpoint(settings.deploy_endpoint, '/api/v1/deploy?uuid={uuid}&force={force}', ['uuid', 'force']);
|
||||
return buildApiUrl(settings, endpoint, { uuid: settings.resource_uuid, force: force ? 'true' : 'false' });
|
||||
}
|
||||
|
||||
function buildApiStatusUrl(settings, deploymentId) {
|
||||
const endpoint = normalizeEndpoint(settings.deployment_status_endpoint, '/api/v1/deployments/{deployment_id}', ['deployment_id']);
|
||||
return buildApiUrl(settings, endpoint, { deployment_id: deploymentId });
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(fetchImpl, url, options = {}, timeoutMs = 15000) {
|
||||
async function fetchWithTimeout(url, options = {}, timeoutMs = 15000) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try { return await fetchImpl(url, { ...options, signal: controller.signal, redirect: 'error' }); }
|
||||
try { return await fetch(url, { ...options, signal: controller.signal, redirect: 'error' }); }
|
||||
catch (error) {
|
||||
if (error?.name === 'AbortError') fail('Anfrage hat das Zeitlimit überschritten.', 'REQUEST_TIMEOUT', 504);
|
||||
fail(`Ziel ist nicht erreichbar: ${error.message}`, 'REMOTE_UNREACHABLE', 502);
|
||||
if (error?.name === 'AbortError') fail('Coolify-Anfrage hat das Zeitlimit überschritten.', 'COOLIFY_TIMEOUT', 504);
|
||||
fail(`Coolify ist nicht erreichbar: ${error.message}`, 'COOLIFY_UNREACHABLE', 502);
|
||||
} finally { clearTimeout(timeout); }
|
||||
}
|
||||
|
||||
async function readSafeResponse(response) {
|
||||
const text = (await response.text()).slice(0, 64 * 1024);
|
||||
const text = (await response.text()).slice(0, 4000);
|
||||
try { return text ? JSON.parse(text) : null; } catch { return text || null; }
|
||||
}
|
||||
|
||||
function redact(value, depth = 0) {
|
||||
if (depth > 8) return '[depth-limit]';
|
||||
if (Array.isArray(value)) return value.slice(0, 100).map(item => redact(item, depth + 1));
|
||||
if (!value || typeof value !== 'object') return typeof value === 'string' ? value.slice(0, 4000) : value;
|
||||
const result = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
if (/token|secret|password|authorization|webhook|credential|key/i.test(key)) result[key] = '[redacted]';
|
||||
else result[key] = redact(item, depth + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function extractDeploymentId(value, depth = 0) {
|
||||
if (depth > 6 || value == null) return null;
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) { const found = extractDeploymentId(item, depth + 1); if (found) return found; }
|
||||
return null;
|
||||
}
|
||||
if (typeof value !== 'object') return null;
|
||||
for (const key of ['deployment_id', 'deployment_uuid', 'deploymentUuid', 'uuid', 'id']) {
|
||||
const candidate = value[key];
|
||||
if (candidate && /^[a-zA-Z0-9_-]{4,160}$/.test(String(candidate))) return String(candidate);
|
||||
}
|
||||
for (const item of Object.values(value)) { const found = extractDeploymentId(item, depth + 1); if (found) return found; }
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractRemoteStatus(value) {
|
||||
if (!value || typeof value !== 'object') return '';
|
||||
const candidates = [value.status, value.state, value.deployment_status, value.data?.status, value.data?.state];
|
||||
return String(candidates.find(Boolean) || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function semver(value) {
|
||||
const match = String(value || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
||||
if (!match) return null;
|
||||
return { raw: String(value).replace(/^v/, ''), major: +match[1], minor: +match[2], patch: +match[3], prerelease: match[4] || '' };
|
||||
}
|
||||
|
||||
function compareVersions(a, b) {
|
||||
const left = semver(a); const right = semver(b);
|
||||
if (!left || !right) fail('Update-Manifest enthält eine ungültige SemVer-Version.', 'UPDATE_VERSION_INVALID', 502);
|
||||
for (const key of ['major', 'minor', 'patch']) if (left[key] !== right[key]) return left[key] > right[key] ? 1 : -1;
|
||||
if (left.prerelease === right.prerelease) return 0;
|
||||
if (!left.prerelease) return 1;
|
||||
if (!right.prerelease) return -1;
|
||||
return left.prerelease.localeCompare(right.prerelease, undefined, { numeric: true });
|
||||
}
|
||||
|
||||
function validateUpdateResult(result, channel) {
|
||||
if (!result?.configured) return { ...result, current_version: APP_VERSION, channel };
|
||||
const latest = String(result.latest_version || result.version || result.manifest?.version || '');
|
||||
const parsed = semver(latest);
|
||||
if (!parsed) fail('Update-Manifest enthält keine gültige SemVer-Version.', 'UPDATE_VERSION_INVALID', 502);
|
||||
if (channel === 'stable' && parsed.prerelease) fail('Der Stable-Kanal akzeptiert keine Vorabversion.', 'UPDATE_CHANNEL_MISMATCH', 502);
|
||||
const manifest = result.manifest && typeof result.manifest === 'object' ? result.manifest : {};
|
||||
const manifestChannel = String(manifest.channel || channel).toLowerCase();
|
||||
if (!CHANNELS.has(manifestChannel) || (channel === 'stable' && manifestChannel !== 'stable')) {
|
||||
fail('Update-Manifest gehört nicht zum konfigurierten Update-Kanal.', 'UPDATE_CHANNEL_MISMATCH', 502);
|
||||
}
|
||||
const updateAvailable = compareVersions(latest, APP_VERSION) > 0;
|
||||
const sha256 = String(manifest.sha256 || manifest.package_sha256 || result.sha256 || '').trim().toLowerCase();
|
||||
if (updateAvailable && !/^[a-f0-9]{64}$/.test(sha256)) fail('Update-Manifest enthält keine gültige SHA-256-Prüfsumme.', 'UPDATE_CHECKSUM_MISSING', 502);
|
||||
const downloadUrl = String(manifest.download_url || manifest.package_url || result.download_url || '').trim();
|
||||
if (downloadUrl) {
|
||||
let url;
|
||||
try { url = new URL(downloadUrl); } catch { fail('Update-Manifest enthält eine ungültige Paket-URL.', 'UPDATE_URL_INVALID', 502); }
|
||||
if (url.protocol !== 'https:') fail('Update-Pakete müssen über HTTPS bereitgestellt werden.', 'UPDATE_URL_INSECURE', 502);
|
||||
}
|
||||
return {
|
||||
...result,
|
||||
configured: true,
|
||||
current_version: APP_VERSION,
|
||||
latest_version: parsed.raw,
|
||||
update_available: updateAvailable,
|
||||
channel,
|
||||
manifest: { ...manifest, sha256, download_url: downloadUrl || '' },
|
||||
};
|
||||
}
|
||||
|
||||
function directorySize(root, maxEntries = 100000) {
|
||||
let bytes = 0; let files = 0; let truncated = false;
|
||||
const queue = [root];
|
||||
while (queue.length) {
|
||||
const current = queue.pop();
|
||||
if (!existsSync(current)) continue;
|
||||
let entries;
|
||||
try { entries = readdirSync(current, { withFileTypes: true }); } catch { continue; }
|
||||
for (const entry of entries) {
|
||||
if (++files > maxEntries) { truncated = true; queue.length = 0; break; }
|
||||
const absolute = join(current, entry.name);
|
||||
try {
|
||||
if (entry.isDirectory()) queue.push(absolute);
|
||||
else if (entry.isFile()) bytes += statSync(absolute).size;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return { path: root, bytes, files, truncated };
|
||||
}
|
||||
|
||||
function fluxStatus(getPlatformSnapshot) {
|
||||
try {
|
||||
const module = getPlatformSnapshot?.()?.modules?.find(item => item.id === 'vendoo.flux-studio');
|
||||
if (module) return { enabled: Boolean(module.enabled), state: module.state, source: 'platform-kernel' };
|
||||
} catch {}
|
||||
try {
|
||||
if (existsSync(MODULE_STATE_PATH)) {
|
||||
const state = JSON.parse(readFileSync(MODULE_STATE_PATH, 'utf8'));
|
||||
const record = state?.modules?.['vendoo.flux-studio'];
|
||||
if (record) return { enabled: record.enabled !== false, state: record.enabled === false ? 'disabled' : 'configured', source: 'module-state' };
|
||||
}
|
||||
} catch {}
|
||||
const initiallyDisabled = String(process.env.VENDOO_INITIAL_DISABLED_MODULES || '').split(',').map(item => item.trim()).includes('vendoo.flux-studio');
|
||||
return { enabled: !initiallyDisabled, state: initiallyDisabled ? 'disabled' : 'default-enabled', source: 'environment-default' };
|
||||
}
|
||||
|
||||
function internalReadiness() {
|
||||
const checks = [];
|
||||
try {
|
||||
const value = db.prepare('SELECT 1 AS ok').get();
|
||||
checks.push({ name: 'database', ok: Number(value?.ok) === 1 });
|
||||
} catch (error) { checks.push({ name: 'database', ok: false, error: error.message }); }
|
||||
for (const item of verifyRuntimeWritable()) checks.push({ name: 'filesystem', ok: item.writable, path: item.path, error: item.error || null });
|
||||
return { ok: checks.every(item => item.ok), checks };
|
||||
}
|
||||
|
||||
async function probePublicReadiness(fetchImpl, timeoutMs = 8000) {
|
||||
const base = String(process.env.PUBLIC_BASE_URL || `http://127.0.0.1:${process.env.PORT || 8124}`).replace(/\/$/, '');
|
||||
const response = await fetchWithTimeout(fetchImpl, `${base}/readyz`, { headers: { Accept: 'application/json', 'User-Agent': `Vendoo/${APP_VERSION}` } }, timeoutMs);
|
||||
const body = await readSafeResponse(response);
|
||||
return { ok: response.ok && body?.ok !== false, http_status: response.status, version: body?.version || null, body: redact(body) };
|
||||
}
|
||||
|
||||
function productionChecks({ getPlatformSnapshot } = {}) {
|
||||
const results = [];
|
||||
const add = (id, status, title, message, details = {}) => results.push({ id, status, title, message, details });
|
||||
const publicBase = String(process.env.PUBLIC_BASE_URL || '').trim();
|
||||
let publicUrl = null;
|
||||
try { publicUrl = publicBase ? new URL(publicBase) : null; } catch {}
|
||||
add('https', publicUrl?.protocol === 'https:' ? 'pass' : 'fail', 'Öffentliche HTTPS-URL', publicUrl?.protocol === 'https:' ? publicUrl.origin : 'PUBLIC_BASE_URL fehlt oder verwendet nicht HTTPS.');
|
||||
const secureCookieMode = String(process.env.VENDOO_COOKIE_SECURE || 'auto').trim().toLowerCase();
|
||||
const secureCookies = secureCookieMode === 'true' || (secureCookieMode === 'auto' && publicUrl?.protocol === 'https:');
|
||||
add('secure-cookies', secureCookies ? 'pass' : 'fail', 'Sichere Cookies', secureCookies ? `Secure-Cookies sind aktiv (${secureCookieMode}).` : 'VENDOO_COOKIE_SECURE ist nicht aktiv oder Auto-Erkennung hat keine HTTPS-URL.');
|
||||
add('trust-proxy', boolEnv('VENDOO_TRUST_PROXY', false) ? 'pass' : 'warning', 'Trust Proxy', boolEnv('VENDOO_TRUST_PROXY', false) ? 'Reverse-Proxy-Vertrauen ist aktiviert.' : 'VENDOO_TRUST_PROXY ist nicht aktiv.');
|
||||
|
||||
const expectedHost = publicUrl?.host || '';
|
||||
const hosts = String(process.env.VENDOO_ALLOWED_HOSTS || '').split(',').map(item => item.trim()).filter(Boolean);
|
||||
const origins = String(process.env.VENDOO_ALLOWED_ORIGINS || '').split(',').map(item => item.trim()).filter(Boolean);
|
||||
add('allowed-host', expectedHost && hosts.includes(expectedHost) ? 'pass' : 'fail', 'Allowed Host', expectedHost && hosts.includes(expectedHost) ? expectedHost : 'Produktivhost fehlt in VENDOO_ALLOWED_HOSTS.', { expected: expectedHost, configured_count: hosts.length });
|
||||
add('allowed-origin', publicUrl?.origin && origins.includes(publicUrl.origin) ? 'pass' : 'fail', 'Allowed Origin', publicUrl?.origin && origins.includes(publicUrl.origin) ? publicUrl.origin : 'Produktivorigin fehlt in VENDOO_ALLOWED_ORIGINS.', { expected: publicUrl?.origin || '', configured_count: origins.length });
|
||||
|
||||
const persistent = DATA_ROOT !== APP_ROOT && dirname(DB_PATH).startsWith(DATA_ROOT) && UPLOADS_DIR.startsWith(DATA_ROOT) && BACKUP_DIR.startsWith(DATA_ROOT);
|
||||
add('persistent-paths', persistent ? 'pass' : 'fail', 'Persistente Pfade', persistent ? `Daten liegen unter ${DATA_ROOT}.` : 'Mindestens ein Laufzeitpfad liegt nicht im persistenten Datenbereich.', runtimePathSummary());
|
||||
const writable = verifyRuntimeWritable();
|
||||
add('writable', writable.every(item => item.writable) ? 'pass' : 'fail', 'Schreibrechte', writable.every(item => item.writable) ? 'Alle Laufzeitpfade sind beschreibbar.' : 'Mindestens ein Laufzeitpfad ist nicht beschreibbar.', { paths: writable });
|
||||
|
||||
let integrity = 'unknown';
|
||||
try { integrity = String(db.pragma('integrity_check', { simple: true })); } catch (error) { integrity = error.message; }
|
||||
add('sqlite-integrity', integrity === 'ok' ? 'pass' : 'fail', 'SQLite-Integrität', integrity === 'ok' ? 'SQLite meldet ok.' : `SQLite-Integritätsprüfung: ${integrity}`);
|
||||
add('backup-target', existsSync(BACKUP_DIR) ? 'pass' : 'fail', 'Backupziel', existsSync(BACKUP_DIR) ? BACKUP_DIR : 'Backupziel fehlt.');
|
||||
|
||||
const flux = fluxStatus(getPlatformSnapshot);
|
||||
add('flux-disabled', flux.enabled ? 'fail' : 'pass', 'FLUX Studio deaktiviert', flux.enabled ? 'FLUX Studio ist aktiv und widerspricht dem Produktions-Scope.' : 'FLUX Studio ist deaktiviert.', flux);
|
||||
const settings = currentSettings();
|
||||
add('deployment-secret', configuredState(settings) ? 'pass' : 'warning', 'Coolify-Zugang', configuredState(settings) ? 'Deployment-Zugang ist gesetzt; Secretwerte werden nicht angezeigt.' : 'Coolify-Deployment ist noch nicht vollständig konfiguriert.', {
|
||||
mode: settings.trigger_mode,
|
||||
api_token_configured: settings.api_token.configured,
|
||||
webhook_configured: settings.deploy_webhook.configured,
|
||||
});
|
||||
const autoDeploy = String(process.env.VENDOO_COOLIFY_AUTO_DEPLOY || '').trim().toLowerCase();
|
||||
add('auto-deploy', ['false', '0', 'off', 'no'].includes(autoDeploy) ? 'pass' : 'warning', 'Coolify Auto Deploy', ['false', '0', 'off', 'no'].includes(autoDeploy) ? 'Auto Deploy ist als deaktiviert dokumentiert.' : 'Vendoo kann den Coolify-Schalter nicht sicher auslesen. Auto Deploy manuell deaktiviert lassen.', { configured: autoDeploy || 'unknown' });
|
||||
return results;
|
||||
}
|
||||
|
||||
export function createDeploymentService(adapters = {}) {
|
||||
const {
|
||||
createBackup,
|
||||
verifyStoredBackup,
|
||||
checkForUpdates,
|
||||
repository = deploymentRepository,
|
||||
fetchImpl = globalThis.fetch,
|
||||
getPlatformSnapshot,
|
||||
} = adapters;
|
||||
export function createDeploymentService({ createBackup, checkForUpdates } = {}) {
|
||||
if (typeof createBackup !== 'function') fail('Backup-Adapter fehlt.', 'DEPLOYMENT_ADAPTER_MISSING', 500);
|
||||
if (typeof verifyStoredBackup !== 'function') fail('Backup-Verifikationsadapter fehlt.', 'DEPLOYMENT_ADAPTER_MISSING', 500);
|
||||
if (typeof fetchImpl !== 'function') fail('Fetch-Adapter fehlt.', 'DEPLOYMENT_ADAPTER_MISSING', 500);
|
||||
|
||||
let monitorTimer = null;
|
||||
let monitoring = false;
|
||||
|
||||
async function checkUpdates() {
|
||||
if (typeof checkForUpdates !== 'function') return { configured: false, current_version: APP_VERSION, message: 'Updateprüfung ist nicht verfügbar.' };
|
||||
return validateUpdateResult(await checkForUpdates({ channel: currentSettings().update_channel }), currentSettings().update_channel);
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
const settings = currentSettings();
|
||||
if (settings.provider !== 'coolify') fail('Coolify ist nicht als Deployment-Provider aktiviert.');
|
||||
if (!settings.coolify_url) fail('Coolify URL fehlt.');
|
||||
const headers = { Accept: 'application/json', 'User-Agent': `Vendoo/${APP_VERSION}` };
|
||||
if (settings.trigger_mode === 'api') {
|
||||
const token = getSecret('COOLIFY_API_TOKEN');
|
||||
if (!token) fail('Coolify API-Token fehlt.');
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
const response = await fetchWithTimeout(fetchImpl, `${settings.coolify_url}/api/health`, { headers }, 10000);
|
||||
const response = await fetchWithTimeout(`${settings.coolify_url}/api/health`, { headers: { Accept: 'application/json' } }, 10000);
|
||||
const body = await readSafeResponse(response);
|
||||
if (!response.ok) fail(`Coolify Healthcheck antwortet mit HTTP ${response.status}.`, 'COOLIFY_HEALTH_FAILED', 502, { response: redact(body) });
|
||||
return { ok: true, status: response.status, response: redact(body), url: settings.coolify_url };
|
||||
if (!response.ok) fail(`Coolify Healthcheck antwortet mit HTTP ${response.status}.`, 'COOLIFY_HEALTH_FAILED', 502, { response: body });
|
||||
return { ok: true, status: response.status, response: body, url: settings.coolify_url };
|
||||
}
|
||||
|
||||
async function requestCoolify(settings, force) {
|
||||
async function trigger({ force = false, confirmation = '', reason = 'manual' } = {}) {
|
||||
if (confirmation !== 'DEPLOY') fail('Zur Bestätigung muss exakt DEPLOY übermittelt werden.', 'DEPLOYMENT_CONFIRMATION_REQUIRED');
|
||||
const settings = currentSettings();
|
||||
if (settings.provider !== 'coolify') fail('Coolify ist nicht als Deployment-Provider aktiviert.');
|
||||
|
||||
let backup = null;
|
||||
if (settings.backup_before_deploy) {
|
||||
backup = await createBackup({
|
||||
kind: 'manual', includeUploads: settings.backup_uploads, includeConfig: true,
|
||||
label: `pre-deploy-${APP_VERSION}`,
|
||||
});
|
||||
}
|
||||
|
||||
let url;
|
||||
const headers = { Accept: 'application/json', 'User-Agent': `Vendoo/${APP_VERSION}` };
|
||||
const method = settings.deploy_method;
|
||||
let method = settings.deploy_method;
|
||||
if (settings.trigger_mode === 'webhook') {
|
||||
url = normalizeWebhookUrl(getSecret('COOLIFY_DEPLOY_WEBHOOK_URL'));
|
||||
if (!url) fail('Coolify Deploy-Webhook ist nicht konfiguriert.');
|
||||
@@ -396,233 +169,23 @@ export function createDeploymentService(adapters = {}) {
|
||||
url = buildApiDeployUrl(settings, Boolean(force));
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
const response = await fetchWithTimeout(fetchImpl, url, { method, headers }, 20000);
|
||||
|
||||
const requestedAt = new Date().toISOString();
|
||||
const response = await fetchWithTimeout(url, { method, headers }, 20000);
|
||||
const body = await readSafeResponse(response);
|
||||
if (!response.ok) fail(`Coolify hat den Deployment-Auftrag mit HTTP ${response.status} abgewiesen.`, 'COOLIFY_DEPLOY_REJECTED', 502, { response: redact(body) });
|
||||
return { method, http_status: response.status, body: redact(body), deployment_id: extractDeploymentId(body) };
|
||||
}
|
||||
|
||||
async function trigger(input = {}, context = {}) {
|
||||
if (input.confirmation !== 'DEPLOY') fail('Zur Bestätigung muss exakt DEPLOY übermittelt werden.', 'DEPLOYMENT_CONFIRMATION_REQUIRED');
|
||||
const settings = currentSettings();
|
||||
if (!configuredState(settings)) fail('Coolify ist nicht vollständig konfiguriert.', 'DEPLOYMENT_NOT_CONFIGURED');
|
||||
const targetVersion = clean(input.target_version || APP_VERSION, 40).replace(/^v/, '');
|
||||
if (!semver(targetVersion)) fail('Zielversion ist keine gültige SemVer-Version.', 'DEPLOYMENT_TARGET_VERSION_INVALID');
|
||||
const idempotencyKey = cleanIdempotencyKey(input.idempotency_key || context.idempotencyKey);
|
||||
const existing = repository.findByIdempotencyKey(idempotencyKey);
|
||||
if (existing) return { ...existing, reused: true, events: repository.listEvents(existing.id) };
|
||||
|
||||
if (compareVersions(targetVersion, APP_VERSION) > 0) {
|
||||
const update = await checkUpdates();
|
||||
if (!update.configured || update.latest_version !== targetVersion || !update.update_available) {
|
||||
fail('Zielversion ist nicht durch das freigegebene Update-Manifest bestätigt.', 'DEPLOYMENT_TARGET_NOT_APPROVED', 409, { target_version: targetVersion, update });
|
||||
}
|
||||
}
|
||||
|
||||
const timeoutAt = new Date(Date.now() + settings.deployment_timeout_seconds * 1000).toISOString();
|
||||
const created = repository.createRun({
|
||||
idempotencyKey,
|
||||
provider: settings.provider,
|
||||
triggerMode: settings.trigger_mode,
|
||||
currentVersion: APP_VERSION,
|
||||
targetVersion,
|
||||
buildRevisionBefore: buildRevision(),
|
||||
requestedBy: context.userId || null,
|
||||
reason: clean(input.reason || 'manual', 120),
|
||||
githubRepository: settings.github_repository,
|
||||
githubBranch: settings.github_branch,
|
||||
timeoutAt,
|
||||
request: { force: Boolean(input.force), target_version: targetVersion, backup_required: settings.backup_before_deploy },
|
||||
});
|
||||
if (created.reused) return { ...created.run, reused: true, events: repository.listEvents(created.run.id) };
|
||||
let run = created.run;
|
||||
|
||||
try {
|
||||
if (settings.backup_before_deploy) {
|
||||
run = repository.transition(run.id, 'backup_pending', { message: 'Vorab-Backup ist verpflichtend.' });
|
||||
run = repository.transition(run.id, 'backup_running', { message: 'Vorab-Backup wird erstellt.' });
|
||||
const backup = await createBackup({
|
||||
kind: 'manual', includeUploads: settings.backup_uploads, includeConfig: true,
|
||||
label: `pre-deploy-${APP_VERSION}-to-${targetVersion}`,
|
||||
});
|
||||
const verification = verifyStoredBackup(backup.id);
|
||||
repository.recordBackupVerification({
|
||||
backupId: backup.id,
|
||||
runId: run.id,
|
||||
status: verification.ok ? 'verified' : 'invalid',
|
||||
sha256: backup.sha256,
|
||||
fileSize: backup.file_size,
|
||||
sqliteIntegrity: verification.ok ? 'ok' : 'failed',
|
||||
manifest: verification.manifest || backup.manifest || {},
|
||||
errors: verification.errors || [],
|
||||
warnings: verification.warnings || [],
|
||||
});
|
||||
if (!verification.ok) fail(`Vorab-Backup ist nicht verifiziert: ${(verification.errors || []).join('; ')}`, 'DEPLOYMENT_BACKUP_INVALID', 409);
|
||||
run = repository.transition(run.id, 'backup_verified', {
|
||||
message: 'Vorab-Backup wurde erstellt und verifiziert.',
|
||||
patch: { backup_id: backup.id },
|
||||
details: { backup_id: backup.id, sha256: backup.sha256, file_size: backup.file_size },
|
||||
});
|
||||
}
|
||||
|
||||
run = repository.transition(run.id, 'deploy_requested', { message: 'Coolify-Deployment wird angefordert.' });
|
||||
const remote = await requestCoolify(settings, Boolean(input.force));
|
||||
run = repository.transition(run.id, settings.trigger_mode === 'api' ? 'deploy_running' : 'health_wait', {
|
||||
message: settings.trigger_mode === 'api' ? 'Coolify hat den Deployment-Auftrag angenommen.' : 'Webhook wurde angenommen; externe Bestätigung oder neue Zielversion wird abgewartet.',
|
||||
eventType: 'coolify.accepted',
|
||||
patch: { deployment_id: remote.deployment_id || null, result_json: remote },
|
||||
details: { http_status: remote.http_status, deployment_id: remote.deployment_id || null },
|
||||
});
|
||||
scheduleMonitor(1000);
|
||||
return { ...run, reused: false, events: repository.listEvents(run.id), message: 'Deployment-Auftrag angenommen. Erfolg wird erst nach Status- und Readinessprüfung gemeldet.' };
|
||||
} catch (error) {
|
||||
const latest = repository.getRun(run.id);
|
||||
if (latest && !['failed', 'succeeded'].includes(latest.state)) {
|
||||
try {
|
||||
repository.transition(run.id, 'failed', {
|
||||
message: error.message || 'Deployment-Auftrag ist fehlgeschlagen.',
|
||||
eventType: 'run.failed',
|
||||
patch: { error_code: error.code || 'DEPLOYMENT_FAILED', error_message: String(error.message || error).slice(0, 2000) },
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function pollCoolifyStatus(run, settings) {
|
||||
if (settings.trigger_mode !== 'api' || !run.deployment_id || !settings.coolify_url) return { available: false };
|
||||
const token = getSecret('COOLIFY_API_TOKEN');
|
||||
if (!token) return { available: false, error: 'API-Token fehlt.' };
|
||||
const url = buildApiStatusUrl(settings, run.deployment_id);
|
||||
const response = await fetchWithTimeout(fetchImpl, url, {
|
||||
headers: { Accept: 'application/json', Authorization: `Bearer ${token}`, 'User-Agent': `Vendoo/${APP_VERSION}` },
|
||||
}, 10000);
|
||||
const body = await readSafeResponse(response);
|
||||
if (!response.ok) return { available: true, ok: false, http_status: response.status, body: redact(body) };
|
||||
const status = extractRemoteStatus(body);
|
||||
return { available: true, ok: true, http_status: response.status, status, body: redact(body) };
|
||||
}
|
||||
|
||||
async function refreshRun(runId = null, { externalConfirmed = false } = {}) {
|
||||
const run = runId ? repository.getRun(runId) : repository.getActiveRun();
|
||||
if (!run) return { state: 'idle', message: 'Kein aktives Deployment.' };
|
||||
if (['succeeded', 'failed', 'rollback_required'].includes(run.state)) return { ...run, events: repository.listEvents(run.id) };
|
||||
if (run.timeout_at && Date.parse(run.timeout_at) <= Date.now()) {
|
||||
const timedOut = repository.transition(run.id, 'rollback_required', {
|
||||
message: 'Deployment-Zeitlimit überschritten. Rollback oder manuelle Prüfung erforderlich.',
|
||||
eventType: 'run.timeout',
|
||||
patch: { error_code: 'DEPLOYMENT_TIMEOUT', error_message: 'Zeitlimit überschritten.' },
|
||||
});
|
||||
return { ...timedOut, events: repository.listEvents(run.id) };
|
||||
}
|
||||
|
||||
const settings = currentSettings();
|
||||
let remote = { available: false };
|
||||
try { remote = await pollCoolifyStatus(run, settings); }
|
||||
catch (error) {
|
||||
repository.patchRun(run.id, {}, { eventType: 'coolify.status_error', message: 'Coolify-Status konnte nicht gelesen werden.', details: { error: error.message } });
|
||||
}
|
||||
if (remote.status && TERMINAL_COOLIFY_FAILURE.has(remote.status)) {
|
||||
const failed = repository.transition(run.id, 'rollback_required', {
|
||||
message: `Coolify meldet den Fehlerstatus ${remote.status}.`,
|
||||
eventType: 'coolify.failed',
|
||||
patch: { result_json: remote, error_code: 'COOLIFY_DEPLOYMENT_FAILED', error_message: `Coolify-Status: ${remote.status}` },
|
||||
});
|
||||
return { ...failed, remote, events: repository.listEvents(run.id) };
|
||||
}
|
||||
if (run.state === 'deploy_running' && remote.available && remote.ok) {
|
||||
repository.transition(run.id, 'health_wait', { message: 'Deploymentstatus wurde gelesen; Readiness und Zielversion werden geprüft.', eventType: 'health.wait', details: { remote_status: remote.status || 'unknown' } });
|
||||
}
|
||||
|
||||
let readiness;
|
||||
try { readiness = await probePublicReadiness(fetchImpl, 8000); }
|
||||
catch (error) { readiness = { ok: false, error: error.message }; }
|
||||
const targetMatches = readiness.ok && String(readiness.version || '').replace(/^v/, '') === String(run.target_version).replace(/^v/, '');
|
||||
const remoteSucceeded = remote.available && remote.ok && TERMINAL_COOLIFY_SUCCESS.has(remote.status);
|
||||
const persistedConfirmation = repository.listEvents(run.id).some(event => event.event_type === 'external.succeeded');
|
||||
const confirmed = Boolean(externalConfirmed || persistedConfirmation);
|
||||
const deploymentConfirmed = settings.trigger_mode === 'api' ? (remoteSucceeded || confirmed) : confirmed;
|
||||
if (readiness.ok && targetMatches && deploymentConfirmed) {
|
||||
const succeeded = repository.transition(run.id, 'succeeded', {
|
||||
message: 'Deployment, Readiness und Zielversion wurden erfolgreich bestätigt.',
|
||||
eventType: 'run.succeeded',
|
||||
patch: { build_revision_after: buildRevision(), result_json: { remote, readiness } },
|
||||
details: { target_version: run.target_version, readiness_version: readiness.version, remote_status: remote.status || null },
|
||||
});
|
||||
return { ...succeeded, remote, readiness, events: repository.listEvents(run.id) };
|
||||
}
|
||||
repository.patchRun(run.id, { result_json: { remote, readiness } }, {
|
||||
eventType: 'health.pending',
|
||||
message: readiness.ok ? 'Readiness ist grün, aber Zielversion oder externe Bestätigung fehlt.' : 'Readiness ist noch nicht grün.',
|
||||
details: { readiness_ok: Boolean(readiness.ok), readiness_version: readiness.version || null, target_version: run.target_version, remote_status: remote.status || null, external_confirmation: confirmed, deployment_confirmed: deploymentConfirmed },
|
||||
});
|
||||
return { ...repository.getRun(run.id), remote, readiness, events: repository.listEvents(run.id) };
|
||||
}
|
||||
|
||||
async function confirmExternal(runId, input = {}) {
|
||||
if (input.confirmation !== 'CONFIRM') fail('Zur externen Bestätigung muss exakt CONFIRM übermittelt werden.', 'DEPLOYMENT_EXTERNAL_CONFIRMATION_REQUIRED');
|
||||
const run = repository.getRun(runId);
|
||||
if (!run) fail('Deployment-Auftrag wurde nicht gefunden.', 'DEPLOYMENT_RUN_NOT_FOUND', 404);
|
||||
if (!['deploy_requested', 'deploy_running', 'health_wait', 'rollback_required'].includes(run.state)) fail('Dieser Deployment-Auftrag kann nicht extern bestätigt werden.', 'DEPLOYMENT_CONFIRMATION_STATE_INVALID', 409);
|
||||
const outcome = clean(input.outcome || 'succeeded', 20);
|
||||
if (outcome === 'failed') {
|
||||
const nextState = run.state === 'rollback_required' ? 'failed' : 'rollback_required';
|
||||
return repository.transition(run.id, nextState, {
|
||||
message: clean(input.message || 'Externes Deployment wurde als fehlgeschlagen bestätigt.', 500),
|
||||
eventType: 'external.failed',
|
||||
patch: { error_code: 'EXTERNAL_DEPLOYMENT_FAILED', error_message: clean(input.message || 'Externe Fehlermeldung.', 1000) },
|
||||
});
|
||||
}
|
||||
if (outcome === 'running') {
|
||||
if (run.state === 'deploy_requested') repository.transition(run.id, 'deploy_running', { message: 'Externes System bestätigt laufendes Deployment.', eventType: 'external.running' });
|
||||
return refreshRun(run.id);
|
||||
}
|
||||
if (outcome !== 'succeeded') fail('Externe Bestätigung muss running, succeeded oder failed verwenden.');
|
||||
repository.addEvent(run.id, run.state, 'external.succeeded', clean(input.message || 'Externes System meldet Abschluss.', 500), {});
|
||||
return refreshRun(run.id, { externalConfirmed: true });
|
||||
}
|
||||
|
||||
function scheduleMonitor(delayMs = null) {
|
||||
if (monitorTimer) clearTimeout(monitorTimer);
|
||||
const interval = currentSettings().health_poll_seconds * 1000;
|
||||
monitorTimer = setTimeout(async () => {
|
||||
if (monitoring) return scheduleMonitor(interval);
|
||||
monitoring = true;
|
||||
try { await refreshRun(); } catch (error) { console.error('Deployment-Monitor:', error.message); }
|
||||
finally {
|
||||
monitoring = false;
|
||||
const active = repository.getActiveRun();
|
||||
if (active && active.state !== 'rollback_required') scheduleMonitor(interval);
|
||||
}
|
||||
}, delayMs ?? interval);
|
||||
monitorTimer.unref?.();
|
||||
}
|
||||
|
||||
async function status() {
|
||||
const settings = publicSettings(repository);
|
||||
const lastBackup = db.prepare(`SELECT id, kind, label, file_name, file_size, sha256, status, verified_at, created_at
|
||||
FROM operation_backups WHERE status = 'verified' ORDER BY created_at DESC LIMIT 1`).get() || null;
|
||||
return {
|
||||
...settings,
|
||||
production: {
|
||||
version: APP_VERSION,
|
||||
build_revision: buildRevision(),
|
||||
deployment_provider: settings.provider,
|
||||
domain: (() => { try { return new URL(String(process.env.PUBLIC_BASE_URL || '')).host; } catch { return ''; } })(),
|
||||
public_base_url: String(process.env.PUBLIC_BASE_URL || ''),
|
||||
readiness: internalReadiness(),
|
||||
database: { path: DB_PATH, integrity: (() => { try { return db.pragma('integrity_check', { simple: true }); } catch (error) { return error.message; } })() },
|
||||
storage: {
|
||||
data: directorySize(DATA_ROOT), uploads: directorySize(UPLOADS_DIR), backups: directorySize(BACKUP_DIR),
|
||||
operations: directorySize(OPERATIONS_DIR), logs: directorySize(LOG_DIR), modules: directorySize(MODULES_DIR),
|
||||
},
|
||||
last_backup: lastBackup,
|
||||
last_deployment: repository.listRuns(1)[0] || null,
|
||||
flux: fluxStatus(getPlatformSnapshot),
|
||||
},
|
||||
latest_production_check: repository.latestProductionCheck(),
|
||||
const record = {
|
||||
requested_at: requestedAt,
|
||||
provider: 'coolify', trigger_mode: settings.trigger_mode, method,
|
||||
resource_uuid: settings.resource_uuid || null,
|
||||
github_repository: settings.github_repository, github_branch: settings.github_branch,
|
||||
version: APP_VERSION, force: Boolean(force), reason: clean(reason, 120),
|
||||
accepted: response.ok, http_status: response.status,
|
||||
backup_id: backup?.id || null, backup_file: backup?.filename || null,
|
||||
response: body,
|
||||
};
|
||||
writeLastDeployment(record);
|
||||
if (!response.ok) fail(`Coolify hat den Deployment-Auftrag mit HTTP ${response.status} abgewiesen.`, 'COOLIFY_DEPLOY_REJECTED', 502, { response: body, backup_id: backup?.id || null });
|
||||
return { ...record, message: 'Coolify-Deployment wurde angefordert. Der laufende Container wird nicht selbst verändert.' };
|
||||
}
|
||||
|
||||
function update(input = {}) {
|
||||
@@ -641,16 +204,13 @@ export function createDeploymentService(adapters = {}) {
|
||||
COOLIFY_RESOURCE_UUID: cleanUuid(input.resource_uuid || ''),
|
||||
COOLIFY_TRIGGER_MODE: mode,
|
||||
COOLIFY_DEPLOY_METHOD: method,
|
||||
COOLIFY_DEPLOY_ENDPOINT: normalizeEndpoint(input.deploy_endpoint, '/api/v1/deploy?uuid={uuid}&force={force}', ['uuid', 'force']),
|
||||
COOLIFY_DEPLOYMENT_STATUS_ENDPOINT: normalizeEndpoint(input.deployment_status_endpoint, '/api/v1/deployments/{deployment_id}', ['deployment_id']),
|
||||
COOLIFY_DEPLOY_ENDPOINT: clean(input.deploy_endpoint || '/api/v1/deploy?uuid={uuid}&force={force}', 500),
|
||||
VENDOO_GITHUB_REPOSITORY: clean(input.github_repository || 'Masterluke77/vendoo', 200),
|
||||
VENDOO_GITHUB_BRANCH: clean(input.github_branch || 'main', 120),
|
||||
VENDOO_GHCR_IMAGE: clean(input.ghcr_image || 'ghcr.io/masterluke77/vendoo', 300),
|
||||
VENDOO_UPDATE_CHANNEL: channel,
|
||||
VENDOO_BACKUP_BEFORE_DEPLOY: input.backup_before_deploy === false ? 'false' : 'true',
|
||||
VENDOO_DEPLOY_BACKUP_UPLOADS: input.backup_uploads === true ? 'true' : 'false',
|
||||
VENDOO_DEPLOY_TIMEOUT_SECONDS: String(Math.max(60, Math.min(7200, Number(input.deployment_timeout_seconds) || 900))),
|
||||
VENDOO_DEPLOY_HEALTH_POLL_SECONDS: String(Math.max(5, Math.min(300, Number(input.health_poll_seconds) || 15))),
|
||||
};
|
||||
let content = readRuntimeConfig();
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
@@ -658,38 +218,27 @@ export function createDeploymentService(adapters = {}) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
writeRuntimeConfig(content);
|
||||
if (input.api_token !== undefined && input.api_token !== '' && input.api_token !== MASKED_SECRET) {
|
||||
if (input.api_token !== undefined && input.api_token !== '' && input.api_token !== '••••••••') {
|
||||
setSecret('COOLIFY_API_TOKEN', clean(input.api_token, 16384), { source: 'deployment-ui' });
|
||||
}
|
||||
if (input.deploy_webhook_url !== undefined && input.deploy_webhook_url !== '' && input.deploy_webhook_url !== MASKED_SECRET) {
|
||||
if (input.deploy_webhook_url !== undefined && input.deploy_webhook_url !== '' && input.deploy_webhook_url !== '••••••••') {
|
||||
setSecret('COOLIFY_DEPLOY_WEBHOOK_URL', normalizeWebhookUrl(input.deploy_webhook_url), { source: 'deployment-ui' });
|
||||
}
|
||||
return publicSettings(repository);
|
||||
return publicSettings();
|
||||
}
|
||||
|
||||
function runProductionCheck(context = {}) {
|
||||
const results = productionChecks({ getPlatformSnapshot });
|
||||
return repository.createProductionCheck({ createdBy: context.userId || null, results });
|
||||
}
|
||||
|
||||
scheduleMonitor(2500);
|
||||
|
||||
return Object.freeze({
|
||||
status,
|
||||
status: publicSettings,
|
||||
update,
|
||||
testConnection,
|
||||
trigger,
|
||||
checkUpdates,
|
||||
refreshRun,
|
||||
confirmExternal,
|
||||
listRuns(limit) { return repository.listRuns(limit).map(run => ({ ...run, events: repository.listEvents(run.id) })); },
|
||||
getRun(id) { const run = repository.getRun(id); return run ? { ...run, events: repository.listEvents(id) } : null; },
|
||||
runProductionCheck,
|
||||
latestProductionCheck: repository.latestProductionCheck,
|
||||
async checkUpdates() {
|
||||
if (typeof checkForUpdates !== 'function') return { configured: false, current_version: APP_VERSION, message: 'Updateprüfung ist nicht verfügbar.' };
|
||||
return checkForUpdates();
|
||||
},
|
||||
health() {
|
||||
const settings = publicSettings(repository);
|
||||
return { ok: true, provider: settings.provider, configured: settings.configured, state: settings.active_run?.state || 'idle', docker_socket_access: false };
|
||||
const status = publicSettings();
|
||||
return { ok: true, provider: status.provider, configured: status.configured, docker_socket_access: false };
|
||||
},
|
||||
stop() { if (monitorTimer) clearTimeout(monitorTimer); monitorTimer = null; },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
export const DEPLOYMENT_STATES = Object.freeze([
|
||||
'checking', 'backup_pending', 'backup_running', 'backup_verified',
|
||||
'deploy_requested', 'deploy_running', 'health_wait',
|
||||
'succeeded', 'failed', 'rollback_required',
|
||||
]);
|
||||
|
||||
export const ACTIVE_DEPLOYMENT_STATES = Object.freeze([
|
||||
'checking', 'backup_pending', 'backup_running', 'backup_verified',
|
||||
'deploy_requested', 'deploy_running', 'health_wait', 'rollback_required',
|
||||
]);
|
||||
|
||||
const TRANSITION_ENTRIES = Object.freeze({
|
||||
checking: ['backup_pending', 'deploy_requested', 'failed'],
|
||||
backup_pending: ['backup_running', 'failed'],
|
||||
backup_running: ['backup_verified', 'failed'],
|
||||
backup_verified: ['deploy_requested', 'failed'],
|
||||
deploy_requested: ['deploy_running', 'health_wait', 'failed', 'rollback_required'],
|
||||
deploy_running: ['health_wait', 'succeeded', 'failed', 'rollback_required'],
|
||||
health_wait: ['succeeded', 'failed', 'rollback_required'],
|
||||
rollback_required: ['failed'],
|
||||
succeeded: [],
|
||||
failed: [],
|
||||
});
|
||||
|
||||
export const DEPLOYMENT_TRANSITIONS = Object.freeze(Object.fromEntries(
|
||||
Object.entries(TRANSITION_ENTRIES).map(([state, targets]) => [state, Object.freeze([...targets])]),
|
||||
));
|
||||
|
||||
const ACTIVE_SET = new Set(ACTIVE_DEPLOYMENT_STATES);
|
||||
|
||||
export function isDeploymentState(state) {
|
||||
return DEPLOYMENT_STATES.includes(state);
|
||||
}
|
||||
|
||||
export function isActiveDeploymentState(state) {
|
||||
return ACTIVE_SET.has(state);
|
||||
}
|
||||
|
||||
export function canTransitionDeployment(from, to) {
|
||||
return from === to || Boolean(DEPLOYMENT_TRANSITIONS[from]?.includes(to));
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import manifest from './module.json' with { type: 'json' };
|
||||
|
||||
export function createProductImageToolsModule() {
|
||||
return {
|
||||
manifest,
|
||||
source: 'builtin',
|
||||
lifecycle: {
|
||||
async start() {},
|
||||
async stop() {},
|
||||
async health() { return { ok: true, native: true, optional: true, contracts: ['ghost-mannequin', 'flatlay'] }; },
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "vendoo.product-image-tools",
|
||||
"name": "Produktbild Studio",
|
||||
"version": "1.0.0",
|
||||
"type": "feature",
|
||||
"status": "native",
|
||||
"defaultEnabled": false,
|
||||
"description": "Optionale, sichere Produktbild-Varianten für Ghost-Mannequin und Flatlay. Das Modul ist standardmäßig deaktiviert und kann im Modulmanager bewusst zugeschaltet werden.",
|
||||
"requires": [
|
||||
"vendoo.ai",
|
||||
"vendoo.media"
|
||||
],
|
||||
"permissions": [
|
||||
"generation.execute"
|
||||
],
|
||||
"provides": [
|
||||
"product-images.variants"
|
||||
],
|
||||
"consumes": [
|
||||
"ai.images",
|
||||
"media.assets"
|
||||
],
|
||||
"owns": [
|
||||
"product-images.jobs",
|
||||
"product-images.variants"
|
||||
]
|
||||
}
|
||||
@@ -48,27 +48,6 @@ export function registerUsersRoutes({ app, routes, deps }) {
|
||||
res.json({ ok: true, emailSent: result.sent, consoleFallback: result.consoleFallback || false });
|
||||
},
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'users.admin.invitations.list', method: 'GET', path: '/api/admin/users/invitations', owner: 'vendoo.users', policy: 'users.manage', csrf: false, stability: 'stable',
|
||||
handler: async (req, res) => res.json({ invitations: getUsersService().invitations(Number(req.query.limit) || 100) }),
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'users.admin.invitations.resend', method: 'POST', path: '/api/admin/users/invitations/:id/resend', owner: 'vendoo.users', policy: 'users.manage', csrf: true, stability: 'stable',
|
||||
handler: async (req, res) => {
|
||||
const prepared = getUsersService().resendPendingInvitation({ invitationId: Number(req.params.id), createdBy: req.user.id });
|
||||
const result = await deps.sendMagicLink(prepared.invitation.email, prepared.invite.token, `${req.protocol}://${req.get('host')}`, 'invite');
|
||||
deps.auditLog(req.user.id, req.user.email, 'user.invitation_resent', 'invitation', String(req.params.id), JSON.stringify({ email: prepared.invitation.email, role: prepared.role, expires_at: prepared.invite.expiresAt }), deps.getClientIp(req));
|
||||
res.json({ ok: true, emailSent: result.sent, consoleFallback: result.consoleFallback || false, expiresAt: prepared.invite.expiresAt });
|
||||
},
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'users.admin.invitations.revoke', method: 'DELETE', path: '/api/admin/users/invitations/:id', owner: 'vendoo.users', policy: 'users.manage', csrf: true, stability: 'stable',
|
||||
handler: async (req, res) => {
|
||||
const invitation = getUsersService().revokePendingInvitation(Number(req.params.id));
|
||||
deps.auditLog(req.user.id, req.user.email, 'user.invitation_revoked', 'invitation', String(req.params.id), JSON.stringify({ email: invitation.email, role: invitation.role }), deps.getClientIp(req));
|
||||
res.json({ ok: true, invitation });
|
||||
},
|
||||
});
|
||||
routes.register(app, {
|
||||
id: 'users.admin.update', method: 'PUT', path: '/api/admin/users/:id', owner: 'vendoo.users', policy: 'users.manage', csrf: true, stability: 'stable',
|
||||
input: objectSchema({ name: { type: 'string', maxLength: 120 }, role: { type: 'string', enum: roles }, active: { type: 'integer', min: 0, max: 1 }, avatar_color: { type: 'string', maxLength: 32 } }),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import {
|
||||
auditLog, createAuthToken, createUser, deleteUser, getLoginHistory, getUser, getUserByEmail,
|
||||
getUsers, setPassword, updateUser, validatePasswordStrength, listInvitations, getInvitation, revokeInvitation,
|
||||
getUsers, setPassword, updateUser, validatePasswordStrength,
|
||||
} from '../../core/identity/identity-store.mjs';
|
||||
import { permissionsForRole, getRoleDefinitions } from '../../../lib/security.mjs';
|
||||
import { PlatformError } from '../../kernel/errors.mjs';
|
||||
@@ -40,7 +40,7 @@ export function createUsersService() {
|
||||
return publicUser(updated);
|
||||
},
|
||||
prepareInvite({ email, name, role, method, createdBy }) {
|
||||
const normalizedRole = ROLES.has(role) ? role : 'viewer';
|
||||
const normalizedRole = ROLES.has(role) ? role : 'editor';
|
||||
if (getUserByEmail(email)) fail('User existiert bereits', 400, 'USER_EXISTS');
|
||||
if (method === 'auto_password') {
|
||||
const password = `Vd!${randomBytes(9).toString('base64url')}9a`;
|
||||
@@ -55,19 +55,6 @@ export function createUsersService() {
|
||||
if (!user) fail('User nicht gefunden', 404, 'USER_NOT_FOUND');
|
||||
return { user: publicUser(user), invite: createAuthToken(user.email, 'invite', user.role, createdBy) };
|
||||
},
|
||||
invitations(limit = 100) { return listInvitations(limit); },
|
||||
resendPendingInvitation({ invitationId, createdBy }) {
|
||||
const invitation = getInvitation(invitationId);
|
||||
if (!invitation) fail('Einladung nicht gefunden', 404, 'INVITATION_NOT_FOUND');
|
||||
if (invitation.status !== 'pending' && invitation.status !== 'expired') fail('Diese Einladung kann nicht erneut versendet werden.', 409, 'INVITATION_NOT_RESENDABLE');
|
||||
const role = ROLES.has(invitation.role) ? invitation.role : 'viewer';
|
||||
return { invitation, invite: createAuthToken(invitation.email, 'invite', role, createdBy), role };
|
||||
},
|
||||
revokePendingInvitation(invitationId) {
|
||||
const invitation = revokeInvitation(invitationId);
|
||||
if (!invitation) fail('Einladung nicht gefunden', 404, 'INVITATION_NOT_FOUND');
|
||||
return invitation;
|
||||
},
|
||||
updateManagedUser({ actor, userId, patch }) {
|
||||
const existing = getUser(userId);
|
||||
if (!existing) fail('User nicht gefunden', 404, 'USER_NOT_FOUND');
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: vendoo:1.42.0
|
||||
image: vendoo:1.41.2
|
||||
container_name: vendoo
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: vendoo:1.42.0
|
||||
image: vendoo:1.41.2
|
||||
container_name: vendoo
|
||||
restart: unless-stopped
|
||||
init: true
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
# Abnahmebericht – Vendoo 1.42.0
|
||||
|
||||
## Ergebnis
|
||||
|
||||
**Technische Paketabnahme bestanden.**
|
||||
|
||||
Version: `1.42.0`
|
||||
Slice: `Production Operations & Controlled Update Completion`
|
||||
Ausgangsbasis: Vendoo `1.41.2`
|
||||
|
||||
Ein produktives Deployment auf `vendoo.flatlined.de` wurde bei dieser Paketabnahme bewusst **nicht** ausgelöst. Der neue `UPDATE_VENDOO.bat`-Assistent automatisiert Review-Gates, Pull Request, Merge, Coolify-Trigger und die Prüfung von `/readyz` auf exakt Version `1.42.0`.
|
||||
|
||||
## Vollständiges Release-Gate
|
||||
|
||||
Der unveränderte Paketbefehl wurde als zusammenhängender Lauf ausgeführt:
|
||||
|
||||
```bash
|
||||
npm run verify:all
|
||||
```
|
||||
|
||||
Ergebnis: **Exit-Code 0**.
|
||||
|
||||
Abgedeckt wurden unter anderem:
|
||||
|
||||
- Basis-Release vor Ein-Klick-Erweiterung: vollständiges `npm run verify:all` mit Exit-Code 0
|
||||
- aktuelles Git- und Secret-Gate: 560 Dateien, keine Laufzeitdaten oder Secrets
|
||||
- aktuelle Syntaxprüfung: 160 JavaScript-/MJS-/CJS-Dateien
|
||||
- ESM-Exportverträge der unveränderten Vendoo-Laufzeit: 159 produktive Quelldateien
|
||||
- Capability-Graph: 17 Module und 40 Capabilities
|
||||
- FLUX-Lifecycle und initial deaktivierter Zustand
|
||||
- Plattformarchitektur: 17 Module, 15 native Module, 2 Legacy-Bridges
|
||||
- Theme-System: 42 Tokens
|
||||
- Security Foundation, Secret-Migration, CSP, Request- und Correlation-IDs
|
||||
- Identity, Rollen, Sitzungen und letzter-Admin-Schutz
|
||||
- kontrollierte Einladungen: Leser-Standardrolle, 48 Stunden, Einmalgültigkeit, erneutes Senden und Widerruf
|
||||
- Katalog, Mediengrenzen, Locking, Papierkorb und Bulk-Limits
|
||||
- Modulmanager und `.vmod`-Verträge
|
||||
- Deployment-State-Machine, Idempotenz, Backup-Gate, Coolify-Status, Zielversionsprüfung und Rollback-Sperre
|
||||
- Konfigurationsspeicher und Shell-Installer
|
||||
- frische und bestehende Datenbankmigrationen
|
||||
|
||||
|
||||
## Ein-Klick-Updater-Abnahme
|
||||
|
||||
Zusätzlich zur bereits vollständig geprüften Vendoo-Laufzeit wurden nach Einbau des Updaters erfolgreich ausgeführt:
|
||||
|
||||
- `node tools/verify-one-click-updater-1.42.0.mjs`
|
||||
- `node tools/check-syntax.mjs`
|
||||
- `node tools/verify-git-safety.mjs`
|
||||
- `node tools/verify-git-safety-regression.mjs`
|
||||
- `node tools/verify-git-staging.mjs`
|
||||
- `node tools/verify-source-boundaries-1.41.2.mjs`
|
||||
- `node tools/verify-git-ignore-boundaries-1.41.2.mjs`
|
||||
- reales Paket-Staging mit enthaltenem Updater und Workflow sowie ausgeschlossenem `logs`-Ordner
|
||||
|
||||
Die Ein-Klick-Erweiterung verändert keine Server-, Datenbank- oder Modul-Laufzeitlogik. Der Updater führt auf dem Zielrechner vor jedem Push erneut `npm ci` und den vollständigen Befehl `npm run verify:all` aus. Schlägt ein Gate fehl, erfolgt weder Merge noch Coolify-Deployment.
|
||||
|
||||
## Datenbankabnahme
|
||||
|
||||
Die neue Migration ist ausschließlich additiv. Neu:
|
||||
|
||||
- `deployment_runs`
|
||||
- `deployment_events`
|
||||
- `backup_verifications`
|
||||
- `production_checks`
|
||||
|
||||
Die SQLite-Prüfung bestätigte:
|
||||
|
||||
- Tabellen erfolgreich anlegbar
|
||||
- Fremdschlüssel ohne Fehler
|
||||
- `PRAGMA integrity_check = ok`
|
||||
- vorhandene Testzeile bleibt bei Migration erhalten
|
||||
- keine Reset-, Drop- oder Löschmigration für bestehende Produktivdaten
|
||||
|
||||
## Isolierter Laufzeittest
|
||||
|
||||
Vendoo wurde mit einem temporären, leeren Datenverzeichnis und Port `18142` gestartet.
|
||||
|
||||
Ergebnis:
|
||||
|
||||
- Serverstart erfolgreich
|
||||
- `GET /healthz` → HTTP 200
|
||||
- gemeldete Version → `1.42.0`
|
||||
- `GET /readyz` → HTTP 200 und `ok: true`
|
||||
- SQLite → bereit
|
||||
- alle erforderlichen Schreibpfade → beschreibbar
|
||||
- Platform Kernel → `running`
|
||||
- geladene Module → 17
|
||||
- geordneter Shutdown → erfolgreich
|
||||
|
||||
Der temporäre Laufzeitstand wurde anschließend entfernt. Er ist nicht Bestandteil des Release-Pakets.
|
||||
|
||||
## Sicherheits- und Datenabgrenzung
|
||||
|
||||
Nicht im Release-Paket enthalten:
|
||||
|
||||
- `.env`
|
||||
- `db/vendoo.db`, `-wal`, `-shm`
|
||||
- Uploads und Backups
|
||||
- Logs und Operationsdaten
|
||||
- `master.key` oder verschlüsselter Secret Store
|
||||
- Coolify-, GitHub-, Mail-, KI- oder Plattform-API-Schlüssel
|
||||
- `node_modules`
|
||||
- lokal erzeugte FLUX-Token
|
||||
|
||||
Vendoo führt im Produktivcontainer weiterhin weder `git pull`, `npm install`, Docker-Kommandos noch Docker-Socket-Zugriffe aus. Build, Containerwechsel und Rollback bleiben Aufgabe von Coolify.
|
||||
|
||||
## Noch notwendige Produktionsabnahme
|
||||
|
||||
Der Updater bestätigt automatisch `/readyz`, Zielversion und `/healthz`. Nach dem ersten Produktivlauf sollten zusätzlich in Vendoo kontrolliert werden:
|
||||
|
||||
1. `/readyz` meldet HTTP 200 und Version `1.42.0`.
|
||||
2. Bestehende Benutzer, Artikel, Uploads und Einstellungen sind unverändert vorhanden.
|
||||
3. Das Volume ist weiterhin exakt nach `/app/data` gemountet.
|
||||
4. FLUX Studio bleibt deaktiviert.
|
||||
5. Produktionscheck zeigt keine Fehler.
|
||||
6. Ein Testbackup wird erstellt und als verifiziert angezeigt.
|
||||
7. Ein kontrollierter Deployment-Auftrag zeigt vollständige Events und endet erst nach bestätigtem Coolify-Abschluss, erfolgreichem `/readyz` und passender Zielversion mit `succeeded`.
|
||||
8. Ein Fehlerfall bleibt als `rollback_required` gesperrt, bis ein Administrator die Prüfung mit `CONFIRM` dokumentiert abschließt.
|
||||
|
||||
## Freigabegrenze
|
||||
|
||||
Das Paket ist für das automatisierte Ein-Klick-Verfahren freigegeben. Der Assistent führt die lokalen Gates aus, wartet auf vorhandene GitHub-Checks, mergt erst danach und startet anschließend den kontrollierten Produktionsworkflow.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Abnahmebericht – Vendoo 1.43.0
|
||||
|
||||
## Ergebnis
|
||||
|
||||
Der Slice **Module-aware Navigation, Listing Studio UX & Updater Status UI** ist abgenommen.
|
||||
|
||||
## Funktionsabnahme
|
||||
|
||||
- Deaktivierte Module entfernen ihre zugeordneten Einträge unmittelbar aus der linken Navigation.
|
||||
- Die Oberfläche lädt den Modulstatus vor modulgeschützten APIs und bleibt auch bei deaktiviertem AI-Modul startfähig.
|
||||
- Das Produktbild Studio ist ein eigenes natives Modul, startet mit `defaultEnabled: false` und kann persistent aktiviert werden.
|
||||
- Die bisherigen Produktbild-Werkzeuge werden bei deaktiviertem Modul nicht angezeigt und ihre API-Routen sind dem Modul zugeordnet.
|
||||
- Die Seite **Neuer Artikel** besitzt eine sticky Aktionsleiste mit **Artikel generieren** und **Speichern**.
|
||||
- Der Ein-Klick-Updater zeigt Phase, Detailstatus, Prozentwert, Live-Protokoll sowie Erfolg, Eingabebedarf oder sicheren Abbruch.
|
||||
|
||||
## Automatisierte Gates
|
||||
|
||||
- `npm run verify:all`: Exit-Code 0
|
||||
- Syntaxprüfung: 163 JavaScript-/MJS-/CJS-Dateien
|
||||
- Git-/Secret-Gate: 568 Paketdateien vor Bereinigung
|
||||
- Plattformgraph: 18 Module und 41 Capabilities
|
||||
- Datenbankmigrationen: Fresh- und Existing-DB erfolgreich
|
||||
- Modulstatus: initial deaktiviert und persistente Aktivierung bestätigt
|
||||
- Ein-Klick-Updater, Modulnavigation und Listing-Studio-Verträge erfolgreich
|
||||
|
||||
## Isolierter Laufzeittest
|
||||
|
||||
- `/healthz`: HTTP 200, Version `1.43.0`
|
||||
- `/readyz`: HTTP 200, `ok: true`
|
||||
- Plattformkernel: `running`, 18 registrierte Module
|
||||
- SQLite und alle persistenten Schreibpfade im temporären Datenverzeichnis erfolgreich
|
||||
|
||||
## Sicherheitsgrenzen
|
||||
|
||||
Es wurden keine produktiven Daten, Datenbanken, Uploads, Backups, `.env`-Dateien, Master-Keys oder API-Schlüssel in das Paket übernommen. Das produktive GitHub-Repository und Coolify wurden bei der Erstellung dieses Pakets nicht verändert.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Abnahmebericht – Vendoo Production Updater 2.1
|
||||
|
||||
## Anlass
|
||||
|
||||
Production Updater 2.0 konnte einen früh beendeten Worker nur als Exit-Code 1 melden. Der Standardfehlerkanal war nicht mit der Oberfläche verbunden und der Protokollpfad wurde erst vom Worker selbst geliefert. Ein Fehler vor dessen erster Statusmeldung führte daher zu einer leeren Diagnose.
|
||||
|
||||
## Korrektur
|
||||
|
||||
- neuer, kleiner `Vendoo.Updater.Host.ps1` als überwachte Bootstrap-Grenze
|
||||
- initialer Status und fester Protokollpfad werden bereits von der GUI angelegt
|
||||
- vollständige Umleitung und Auswertung von Standardausgabe und Standardfehler
|
||||
- Lade-, Parser- und Initialisierungsfehler des Worker-Kerns werden mit Datei, Zeile und vollständiger Ausnahme protokolliert
|
||||
- der Worker-Core beendet den Host nicht mehr mit `exit`
|
||||
- der Host garantiert einen terminalen Status
|
||||
- Diagnoseordner ist direkt aus der Oberfläche erreichbar
|
||||
- `updater.log`, `status.json`, `events.ndjson`, `worker.stdout.log` und `worker.stderr.log` liegen pro Lauf in einem eigenen Verzeichnis
|
||||
|
||||
## Automatisierte Abnahme
|
||||
|
||||
- PowerShell-Strukturparser: UI, Host, Worker und Launcher ohne Parserfehler
|
||||
- `npm run verify:all`: Exit-Code 0
|
||||
- JavaScript-/MJS-/CJS-Syntax: 166 Dateien
|
||||
- Git-Sicherheitsgate: 578 Paketdateien, keine Secrets oder Laufzeitdaten
|
||||
- Plattformgraph: 18 Module und 41 Capabilities
|
||||
- Datenbankmigrationen: erfolgreich, vorhandene Daten bleiben erhalten
|
||||
- Updater-2.1-Vertrag: erfolgreich
|
||||
- internes SHA-256-Manifest: 577 Dateien
|
||||
|
||||
## Prüfgrenze
|
||||
|
||||
Die WPF-Oberfläche wurde in dieser Linux-Prüfumgebung nicht interaktiv unter Windows gestartet. Parser, XAML-/Codevertrag, Bootstrap-Architektur, vollständiger Fehlerkanal, Paketgrenzen und der gesamte Vendoo-Abnahmevertrag wurden automatisiert geprüft. Beim nächsten Windows-Fehler bleibt die Oberfläche geöffnet und zeigt die konkrete PowerShell-Ausnahme samt Protokollpfad; ein diagnostikloser Exit-Code-1-Zustand ist durch den neuen Startvertrag ausgeschlossen.
|
||||
@@ -1,22 +0,0 @@
|
||||
# Abnahmebericht – Vendoo Production Updater 2.2
|
||||
|
||||
## Behobener Defekt
|
||||
|
||||
- `Vendoo.Updater.Worker.ps1`: `"$Label: ..."` zu `"${Label}: ..."` korrigiert.
|
||||
- `Vendoo.Updater.Worker.ps1`: `"$Title: ..."` zu `"${Title}: ..."` korrigiert.
|
||||
|
||||
## Ursachenanalyse
|
||||
|
||||
Der 2.1-Abnahmetest kontrollierte Textverträge, BOM, Fehlerkanal und Paketstruktur, führte aber keinen echten Windows-PowerShell-Parser aus. Die Formulierung im 2.1-Abnahmebericht war daher falsch.
|
||||
|
||||
## Neue Abnahmegrenzen
|
||||
|
||||
- plattformunabhängiger statischer Regressionstest für ungültige `$Variable:`-Verweise
|
||||
- lokale Parserprüfung mit `System.Management.Automation.Language.Parser` vor jedem Updaterstart
|
||||
- zweite Parserprüfung im Worker-Host vor dem Laden des Worker-Kerns
|
||||
- Windows-PowerShell-5.1-Parserjob in GitHub Actions
|
||||
- Release-Workflow von erfolgreichem Windows-Parserjob abhängig
|
||||
|
||||
## Prüfgrenze dieser Paketumgebung
|
||||
|
||||
In der Linux-Paketumgebung kann Windows PowerShell 5.1 nicht ausgeführt werden. Deshalb wird hier nichts als Windows-Parserlauf ausgegeben. Der exakte Windows-PowerShell-5.1-Parser läuft stattdessen zwingend lokal vor dem Start und im Windows-GitHub-Actions-Job.
|
||||
@@ -1,38 +0,0 @@
|
||||
# Abnahmebericht – Vendoo Production Updater 2.4
|
||||
|
||||
## Anlass
|
||||
|
||||
Im Updater 2.3 wurde `gh auth login` im versteckten Worker ausgeführt. GitHub CLI benötigt für den Browser-Gerätefluss sichtbare Interaktion. Der Prozess wartete deshalb, während der Benutzer weder Gerätecode noch Browseraktion zuverlässig sehen konnte. Zusätzlich erzeugte der Downloadfortschritt zu viele identische Logeinträge.
|
||||
|
||||
## Umsetzung
|
||||
|
||||
- interaktiver GitHub-Login aus `Invoke-External` entfernt
|
||||
- eigenes sichtbares Anmeldefenster `github-login.cmd`
|
||||
- expliziter GitHub-Aufruf mit `--hostname github.com --git-protocol https --web --clipboard`
|
||||
- Hauptoberfläche bleibt geöffnet und pollt `gh auth status` still
|
||||
- automatische Fortsetzung nach erfolgreichem Login
|
||||
- eindeutiger Fehler bei geschlossenem oder fehlgeschlagenem Anmeldefenster
|
||||
- maximal zehn Minuten für die einmalige Anmeldung
|
||||
- Downloadfortschritt nur noch in 10-%-Stufen
|
||||
- Status-Polling ohne wiederholte Fehler- und Startmeldungen
|
||||
- Updater-Version 2.4.0 in Preflight, UI, Host, Worker und Manifest konsistent
|
||||
|
||||
## Ausgeführte Prüfungen
|
||||
|
||||
- Updater-2.4-Vertragsgate: bestanden
|
||||
- PowerShell-Static-Gate: 16 Dateien, bestanden
|
||||
- zusätzlicher PowerShell-Syntaxparse der sechs zentralen Updater-Skripte: ohne Fehlerknoten
|
||||
- JavaScript-/MJS-/CJS-Syntaxprüfung: 170 Dateien, bestanden
|
||||
- Git-Sicherheitsgate: 590 Dateien, bestanden
|
||||
- Git-Sicherheitsregression: bestanden
|
||||
- Git-Stagingregression: bestanden
|
||||
- Git-Ignore-Grenzprüfung: bestanden
|
||||
- Source-Root-Grenzprüfung: bestanden
|
||||
|
||||
## Einschränkung der Prüfumgebung
|
||||
|
||||
Windows PowerShell 5.1 und eine interaktive Windows-WPF-Sitzung stehen in der Linux-Prüfumgebung nicht zur Verfügung. Das Paket enthält deshalb weiterhin das echte Windows-PowerShell-Parser-Gate, das sämtliche PowerShell-Dateien auf dem Zielrechner vor dem Öffnen der Oberfläche prüft. Der GitHub-Gerätefluss selbst kann erst auf Windows gegen das echte Benutzerkonto abgeschlossen werden.
|
||||
|
||||
## Sicherheitsgrenze
|
||||
|
||||
Der gemeldete 2.3-Lauf endete während der GitHub-Anmeldung mit `cancelled`. Vor diesem Punkt wurden kein Release-Branch, kein Pull Request, kein Merge und kein Coolify-Deployment ausgeführt.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Abnahmebericht – Vendoo Production Updater 2.5
|
||||
|
||||
## Korrektur
|
||||
|
||||
Der Launcher wurde als minimaler ASCII/CRLF-Einstiegspunkt neu erstellt. Die Prüfung erfolgt bytegenau.
|
||||
|
||||
## Harte Gates
|
||||
|
||||
- keine UTF-8-BOM in `UPDATE_VENDOO.bat`
|
||||
- ausschließlich Bytes 0x00–0x7F
|
||||
- kein einzelnes LF ohne vorangestelltes CR
|
||||
- kein einzelnes CR ohne folgendes LF
|
||||
- absoluter Windows-PowerShell-Systempfad
|
||||
- keine `chcp`-Umschaltung
|
||||
- keine Übergabe von `PackageRoot`
|
||||
- CRLF-Vertrag in `.gitattributes` und `.editorconfig`
|
||||
@@ -1,14 +0,0 @@
|
||||
# Abnahmebericht – Vendoo Production Updater 2.6
|
||||
|
||||
## Korrektur
|
||||
|
||||
Die Coolify-Webhook-Abfrage des Desktop-Updaters wurde vollständig entfernt. Der Produktionsablauf verwendet nach dem Merge das bestehende Coolify Auto Deploy der GitHub-Verknüpfung.
|
||||
|
||||
## Abnahmekriterien
|
||||
|
||||
- kein `COOLIFY_DEPLOY_WEBHOOK_URL` im Worker
|
||||
- keine Secret-Eingabe und kein `gh secret set`
|
||||
- kein manueller `workflow run`
|
||||
- kontrolliertes Warten auf `/readyz`
|
||||
- exakte Versionsprüfung auf 1.43.0
|
||||
- Timeout mit verständlichem Auto-Deploy-Hinweis
|
||||
@@ -1,13 +0,0 @@
|
||||
# Abnahmebericht – Vendoo Production Updater 2.8
|
||||
|
||||
## Behobener Fehler
|
||||
|
||||
Windows brach `npm run verify:all` im Gate `verify:initial-modules` mit `EBUSY` beim Löschen einer noch geöffneten temporären SQLite-Datenbank ab.
|
||||
|
||||
## Korrektur
|
||||
|
||||
- temporäre `better-sqlite3`-Verbindung wird explizit geschlossen
|
||||
- WAL wird vor dem Close kontrolliert ausgecheckt
|
||||
- Dateisystembereinigung besitzt Windows-taugliche Wiederholungen
|
||||
- neuer Regressionstest führt das betroffene Gate dreimal in getrennten Node-Prozessen aus
|
||||
- Updater-Gate verlangt den Cleanup-Vertrag als harte Releasebedingung
|
||||
@@ -1,24 +0,0 @@
|
||||
# Abnahmebericht – Vendoo Production Updater 2.9
|
||||
|
||||
## Anlass
|
||||
|
||||
Unter Windows hielten mehrere Verifier ihre temporären `better-sqlite3`-Datenbanken offen. Dadurch scheiterte das Entfernen der Testordner mit `EBUSY`. Außerdem erzeugten Capability- und Architekturtests durch transitive Modulimporte eine echte `db/vendoo.db` im Quellbaum.
|
||||
|
||||
## Korrektur
|
||||
|
||||
- zentraler Runtime-Cleanup mit WAL-Checkpoint, `db.close()` und Windows-tauglichen Löschwiederholungen
|
||||
- Einladungs-, Initialmodul-, Capability- und Architekturtest laufen in isolierten Datenverzeichnissen
|
||||
- statischer Importgraph-Audit aller aktiven `verify:all`-Verifier bis `lib/db.mjs`
|
||||
- jeder datenbanknahe Verifier muss Testpfade, explizites Schließen und zentralen Temp-Cleanup nachweisen
|
||||
- dynamische Regression führt alle vier datenbanknahen Testklassen jeweils dreimal in getrennten Node-Prozessen aus
|
||||
- ein zweiter Testlauf erzeugt keine `db/vendoo.db` im Quellbaum
|
||||
|
||||
## Abnahme
|
||||
|
||||
- `npm run verify:all`: Exit-Code 0
|
||||
- Syntaxprüfung: 180 JavaScript-/MJS-/CJS-Dateien
|
||||
- SQLite-Cleanup-Audit: 29 aktive Verifier transitiv geprüft
|
||||
- Wiederholungsregression: 12 isolierte Testprozesse ohne offene SQLite-Datei oder Temp-Rest
|
||||
- Plattform: 18 Module und 41 Capabilities
|
||||
- Datenbankmigrationen und Integritätsprüfung: bestanden
|
||||
- Pull Request, Merge und Deployment bleiben bei jedem Testfehler gesperrt
|
||||
@@ -1,48 +0,0 @@
|
||||
# Abnahmebericht – Vendoo Production Updater 3.1
|
||||
|
||||
## Anlass
|
||||
|
||||
Die Updater-2.x-Reihe verwendete eine destruktive Kopierstrategie: Die geklonte Git-Arbeitskopie wurde geleert und anschließend über Ausschlussregeln aus dem Paket neu befüllt. Dadurch konnten Quell- und Laufzeitdaten nicht zuverlässig unterschieden werden. Der konkrete Fehler war das fehlende `db/schema.sql` im Auslieferungspaket beziehungsweise in der vorbereiteten Arbeitskopie.
|
||||
|
||||
## Architekturkorrektur
|
||||
|
||||
Updater 3.1 ersetzt diesen Mechanismus vollständig:
|
||||
|
||||
- keine Leerung der geklonten Baseline,
|
||||
- SHA-256-Manifest aller Release-Dateien,
|
||||
- Pflichtdateien werden vor jedem Test geprüft,
|
||||
- Overlay statt Vollersetzung,
|
||||
- Löschungen ausschließlich explizit über `removedFiles`,
|
||||
- Hashprüfung vor und nach jedem Kopiervorgang,
|
||||
- Sitzungen aus 2.x werden verworfen,
|
||||
- separater v3-Release-Branch,
|
||||
- vollständiger Windows-Releasevertrag in GitHub Actions.
|
||||
|
||||
## Pflichtdateien
|
||||
|
||||
Der Release wird unter anderem blockiert, wenn eine dieser Dateien fehlt:
|
||||
|
||||
- `db/schema.sql`
|
||||
- `lib/db.mjs`
|
||||
- `server.mjs`
|
||||
- `bootstrap.mjs`
|
||||
- `package.json`
|
||||
- `package-lock.json`
|
||||
- Updater Preflight, UI, Host und Worker
|
||||
|
||||
## Prüfungen
|
||||
|
||||
- Release-Manifest-Gate mit Manipulations- und Missing-File-Test
|
||||
- reales Overlay des vollständigen Release-Manifests auf eine Git-Baseline
|
||||
- Baseline-Erhalt ohne nicht deklarierte Löschungen
|
||||
- vollständige Vendoo-Gates in getrennten Blöcken
|
||||
- Datenbankmigrationen
|
||||
- SQLite-Handle-Cleanup
|
||||
- Module, Capabilities, Security, Identity, Katalog und Production Operations
|
||||
- Windows-PowerShell-Parservertrag
|
||||
- Windows-CI mit vollständigem `npm run verify:all`
|
||||
- manifestbasierter GitHub-Release-Builder
|
||||
|
||||
## Sicherheitsgrenze
|
||||
|
||||
Der Updater verändert `main`, Coolify und die Produktionsinstanz erst nach grünen lokalen Tests und grünen GitHub-Checks. `/app/data`, Datenbanken, Uploads, Secrets und Backups sind nicht Bestandteil des Release-Manifests.
|
||||
@@ -1,54 +0,0 @@
|
||||
# Abnahmebericht – Vendoo Production Updater 3.2
|
||||
|
||||
## Anlass
|
||||
|
||||
Die Updater-2.x-Reihe verwendete eine destruktive Kopierstrategie: Die geklonte Git-Arbeitskopie wurde geleert und anschließend über Ausschlussregeln aus dem Paket neu befüllt. Dadurch konnten Quell- und Laufzeitdaten nicht zuverlässig unterschieden werden. Der konkrete Fehler war das fehlende `db/schema.sql` im Auslieferungspaket beziehungsweise in der vorbereiteten Arbeitskopie.
|
||||
|
||||
## Architekturkorrektur
|
||||
|
||||
Updater 3.2 ersetzt diesen Mechanismus vollständig:
|
||||
|
||||
- keine Leerung der geklonten Baseline,
|
||||
- SHA-256-Manifest aller Release-Dateien,
|
||||
- Pflichtdateien werden vor jedem Test geprüft,
|
||||
- Overlay statt Vollersetzung,
|
||||
- Löschungen ausschließlich explizit über `removedFiles`,
|
||||
- Hashprüfung vor und nach jedem Kopiervorgang,
|
||||
- Sitzungen aus 2.x werden verworfen,
|
||||
- separater v3-Release-Branch,
|
||||
- vollständiger Windows-Releasevertrag in GitHub Actions.
|
||||
|
||||
## Pflichtdateien
|
||||
|
||||
Der Release wird unter anderem blockiert, wenn eine dieser Dateien fehlt:
|
||||
|
||||
- `db/schema.sql`
|
||||
- `lib/db.mjs`
|
||||
- `server.mjs`
|
||||
- `bootstrap.mjs`
|
||||
- `package.json`
|
||||
- `package-lock.json`
|
||||
- Updater Preflight, UI, Host und Worker
|
||||
|
||||
## Prüfungen
|
||||
|
||||
- Release-Manifest-Gate mit Manipulations- und Missing-File-Test
|
||||
- reales Overlay des vollständigen Release-Manifests auf eine Git-Baseline
|
||||
- Baseline-Erhalt ohne nicht deklarierte Löschungen
|
||||
- vollständige Vendoo-Gates in getrennten Blöcken
|
||||
- Datenbankmigrationen
|
||||
- SQLite-Handle-Cleanup
|
||||
- Module, Capabilities, Security, Identity, Katalog und Production Operations
|
||||
- Windows-PowerShell-Parservertrag
|
||||
- Windows-CI mit vollständigem `npm run verify:all`
|
||||
- manifestbasierter GitHub-Release-Builder
|
||||
|
||||
## Sicherheitsgrenze
|
||||
|
||||
Der Updater verändert `main`, Coolify und die Produktionsinstanz erst nach grünen lokalen Tests und grünen GitHub-Checks. `/app/data`, Datenbanken, Uploads, Secrets und Backups sind nicht Bestandteil des Release-Manifests.
|
||||
|
||||
## Korrektur 3.2
|
||||
|
||||
- `release-manifest.json` wird nach dem Payload als eigene Steuerdatei kopiert und bytegenau geprüft.
|
||||
- Das Zielrepository darf unveränderte Baseline-Dateien enthalten; manifestierte Dateien bleiben vollständig hashverbindlich.
|
||||
- Die Löschung der Steuerdatei über `removedFiles` ist verboten.
|
||||
@@ -1,9 +0,0 @@
|
||||
# Abnahmebericht – Vendoo Production Updater 3.3
|
||||
|
||||
## Behobener Fehler
|
||||
|
||||
Der Windows-Lauf erreichte das letzte Gate, scheiterte aber im POSIX-Installer-Test. Ursache war eine nicht portable Annahme: Git Bash auf NTFS sollte nach `chmod 600` über `stat -c` exakt Modus `600` melden.
|
||||
|
||||
## Korrektur
|
||||
|
||||
Die POSIX-Rechteprüfung bleibt auf Linux/NAS/VPS strikt. Windows Git Bash prüft die Shell-Syntax; die funktionale Simulation, `.env`, Token, Compose-Aufrufe und Modus `0600` werden verpflichtend unter Linux geprüft. Fehlgeschlagene Assertions liefern vollständige Diagnoseausgaben.
|
||||
@@ -1,108 +0,0 @@
|
||||
# Coolify-Update auf Vendoo 1.42.0
|
||||
|
||||
# Empfohlener automatischer Weg
|
||||
|
||||
Für den normalen Updatefall nur das Release-ZIP entpacken und `UPDATE_VENDOO.bat` doppelklicken. GitHub-Anmeldung und Coolify-Webhook werden nur beim ersten Start abgefragt. Alle nachfolgenden Schritte dieser Datei sind der manuelle Notfall- und Kontrollweg.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Repository: `Masterluke77/vendoo`
|
||||
- Produktionsbranch: `main`
|
||||
- Dockerfile-Build
|
||||
- interner Port: `8124`
|
||||
- Domain: `https://vendoo.flatlined.de`
|
||||
- persistentes Volume unverändert nach `/app/data`
|
||||
- genau eine Instanz
|
||||
- Rolling Updates aus
|
||||
- Auto Deploy aus
|
||||
- FLUX Studio aus
|
||||
|
||||
## Vor dem Merge
|
||||
|
||||
1. Releasebranch `release/1.42.0-production-operations` aus aktuellem `main` erstellen.
|
||||
2. Vollständigen Source-Stand übertragen; keine `.env`, Datenbank, Uploads, Backups, Logs, Master-Keys oder installierten Laufzeitmodule committen.
|
||||
3. `npm ci` und `npm run verify:all` ausführen.
|
||||
4. Draft-PR erstellen und GitHub Actions abwarten.
|
||||
5. Erst nach grünem CI und Review nach `main` mergen.
|
||||
|
||||
## Zusätzliche Coolify-Variablen
|
||||
|
||||
```env
|
||||
COOLIFY_DEPLOYMENT_STATUS_ENDPOINT=/api/v1/deployments/{deployment_id}
|
||||
VENDOO_DEPLOY_TIMEOUT_SECONDS=900
|
||||
VENDOO_DEPLOY_HEALTH_POLL_SECONDS=15
|
||||
VENDOO_BUILD_REVISION=<Git-Commit-SHA>
|
||||
VENDOO_COOLIFY_AUTO_DEPLOY=false
|
||||
```
|
||||
|
||||
Die vorhandenen Produktionswerte müssen erhalten bleiben:
|
||||
|
||||
```env
|
||||
NODE_ENV=production
|
||||
PORT=8124
|
||||
VENDOO_BIND_HOST=0.0.0.0
|
||||
PUBLIC_BASE_URL=https://vendoo.flatlined.de
|
||||
VENDOO_ALLOWED_HOSTS=vendoo.flatlined.de
|
||||
VENDOO_ALLOWED_ORIGINS=https://vendoo.flatlined.de
|
||||
VENDOO_TRUST_PROXY=true
|
||||
VENDOO_COOKIE_SECURE=true
|
||||
VENDOO_COOKIE_SAMESITE=lax
|
||||
VENDOO_INITIAL_DISABLED_MODULES=vendoo.flux-studio
|
||||
LOCAL_IMAGE_ENABLED=false
|
||||
LOCAL_IMAGE_ALLOW_REMOTE=false
|
||||
VENDOO_GITHUB_REPOSITORY=Masterluke77/vendoo
|
||||
VENDOO_GITHUB_BRANCH=main
|
||||
VENDOO_UPDATE_CHANNEL=stable
|
||||
VENDOO_BACKUP_BEFORE_DEPLOY=true
|
||||
VENDOO_DEPLOY_BACKUP_UPLOADS=false
|
||||
```
|
||||
|
||||
## Coolify-Konfiguration prüfen
|
||||
|
||||
1. Coolify öffnen und Projekt **Vendoo → Production → Application** wählen.
|
||||
2. Source muss die private GitHub-App, Repository `Masterluke77/vendoo` und Branch `main` verwenden.
|
||||
3. Build Pack **Dockerfile**, Base Directory `/`, Dockerfile `/Dockerfile`.
|
||||
4. Port `8124`; kein öffentliches Host-Port-Mapping.
|
||||
5. Domain `https://vendoo.flatlined.de`; HTTPS erzwingen.
|
||||
6. Volume `vendoo-data` unverändert nach `/app/data`.
|
||||
7. Replikate `1`, Rolling Updates aus, Auto Deploy aus.
|
||||
8. Healthcheck `/readyz` mit erwartetem HTTP 200.
|
||||
9. Speichern, aber das Volume niemals neu anlegen oder löschen.
|
||||
|
||||
## Erstes kontrolliertes Deployment
|
||||
|
||||
1. Nach dem Merge Coolify noch nicht blind mehrfach starten.
|
||||
2. Bestehendes Vendoo öffnen: **Administration → System → Updates**.
|
||||
3. Produktionscheck ausführen und Fehler vor dem Deployment beheben.
|
||||
4. Coolify-Provider, Trigger-Modus, Resource UUID, Repository und `main` prüfen.
|
||||
5. Webhook oder API-Token nur über die Vendoo-Oberfläche in den verschlüsselten Secret Store eintragen.
|
||||
6. **Version prüfen**. Eine neuere Zielversion wird nur akzeptiert, wenn das freigegebene Manifest SemVer, Kanal, HTTPS-Paket-URL und SHA-256 erfüllt.
|
||||
7. Zielversion `1.42.0` eintragen.
|
||||
8. Vorab-Backup aktiviert lassen. Uploads nur bei ausreichendem Backup-Speicher einschließen.
|
||||
9. **Coolify-Deployment starten** wählen und exakt `DEPLOY` eingeben.
|
||||
10. Der Auftrag muss nacheinander Backup, Backup-Verifikation und Coolify-Trigger protokollieren.
|
||||
11. Im API-Modus wird der Coolify-Status gepollt. Im Webhook-Modus nach dem tatsächlichen Coolify-Abschluss im Verlauf **Externen Abschluss bestätigen** wählen und exakt `CONFIRM` eingeben.
|
||||
12. Erfolg darf erst erscheinen, wenn `/readyz` grün ist und die laufende Version `1.42.0` meldet.
|
||||
|
||||
## Abnahme nach Deployment
|
||||
|
||||
- `/readyz` liefert HTTP 200 und Version `1.42.0`.
|
||||
- Login funktioniert.
|
||||
- Bestehende Benutzer, Artikel und Uploads sind vorhanden.
|
||||
- SQLite-Integrität ist `ok`.
|
||||
- Letztes Vorab-Backup ist als verifiziert sichtbar.
|
||||
- Deployment-Verlauf zeigt `succeeded` und nachvollziehbare Events.
|
||||
- FLUX Studio bleibt deaktiviert.
|
||||
- Produktionscheck hat keine Fehler.
|
||||
|
||||
## Fehler und Rollback
|
||||
|
||||
Bei `rollback_required` nicht erneut deployen, bevor die Ursache geprüft wurde.
|
||||
|
||||
1. Coolify-Build- und Startlogs sichern.
|
||||
2. `/readyz`, Zielversion und Volume-Mount prüfen.
|
||||
3. In Coolify den letzten funktionierenden Commit beziehungsweise das vorige Image redeployen.
|
||||
4. Dasselbe Volume `/app/data` weiterverwenden.
|
||||
5. Volume niemals löschen oder neu initialisieren.
|
||||
6. Nach erfolgreichem Rollback Login, Artikel, Uploads, SQLite und `/readyz` prüfen.
|
||||
7. Den blockierten Vendoo-Auftrag erst danach mit **Prüfung abschließen** und exakt `CONFIRM` dokumentiert schließen.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Vendoo 1.42.0 – Ein-Klick-Update
|
||||
|
||||
## So benutzt du es
|
||||
|
||||
1. ZIP vollständig in einen neuen Ordner entpacken.
|
||||
2. `UPDATE_VENDOO.bat` doppelklicken.
|
||||
3. Nur beim ersten Start GitHub im Browser anmelden.
|
||||
4. Nur beim ersten Start den Coolify-Deploy-Webhook einfügen.
|
||||
5. Danach läuft alles automatisch.
|
||||
|
||||
## Automatischer Ablauf
|
||||
|
||||
- Git, Node.js LTS und GitHub CLI werden bei Bedarf über `winget` installiert.
|
||||
- Das private Repository `Masterluke77/vendoo` wird frisch aus `main` geklont.
|
||||
- Ein isolierter Release-Branch wird angelegt.
|
||||
- Nur sichere Source-Dateien werden übernommen.
|
||||
- `.env`, Datenbanken, Uploads, Backups, Logs und Schlüssel bleiben ausgeschlossen.
|
||||
- `npm ci` und `npm run verify:all` laufen vollständig.
|
||||
- Der Pull Request wird erstellt, geprüft und nach `main` gemergt.
|
||||
- GitHub Actions startet den Coolify-Deploy-Webhook.
|
||||
- Erfolg wird erst gemeldet, wenn `/readyz` HTTP 200 und exakt Version `1.42.0` liefert.
|
||||
- Abschließend wird `/healthz` geprüft.
|
||||
|
||||
## Sicherheit
|
||||
|
||||
Der Coolify-Webhook wird nicht in das Paket oder Repository geschrieben. Er wird nur einmalig als verschlüsseltes GitHub Actions Secret `COOLIFY_DEPLOY_WEBHOOK_URL` gespeichert.
|
||||
|
||||
Das persistente Coolify-Volume `/app/data` wird weder ersetzt noch gelöscht. Parallele Produktionsdeployments werden durch GitHub Actions `concurrency` verhindert.
|
||||
|
||||
Lokale Protokolle liegen unter `logs/update-1.42.0-<Zeitstempel>.log`. Der Ordner `logs` wird vom Git-Staging ausgeschlossen.
|
||||
@@ -1,22 +0,0 @@
|
||||
# Vendoo Production Updater 2.0
|
||||
|
||||
Der bisherige Hotfix-Updater wurde vollständig ersetzt.
|
||||
|
||||
## Eigenschaften
|
||||
|
||||
- moderne, nüchterne WPF-Oberfläche ohne dekorative Spielereien
|
||||
- Fenster bleibt bei Erfolg, Abbruch und Fehler geöffnet
|
||||
- sichtbare zehnstufige Prozessanzeige und technisches Live-Protokoll
|
||||
- verwaltete portable GitHub CLI statt Winget-Abhängigkeit
|
||||
- offizieller GitHub-Download mit SHA-256-Abgleich gegen die Release-Prüfsummen
|
||||
- feste Zeitlimits und kontrollierter Prozessabbruch inklusive Prozessbaum
|
||||
- sichere Coolify-Eingabe ohne Klartext im Log oder in der Kommandozeile
|
||||
- persistenter Wiederaufnahmepunkt unter `%LOCALAPPDATA%\Vendoo\Updater`
|
||||
- idempotenter Ablauf: nach Merge- oder Deploymentfehlern wird nicht blind neu begonnen
|
||||
- kein automatisches Schließen der Oberfläche
|
||||
|
||||
## Start
|
||||
|
||||
`UPDATE_VENDOO.bat` doppelklicken.
|
||||
|
||||
Eine GitHub-Browseranmeldung und die Eingabe des Coolify-Webhooks sind nur erforderlich, wenn sie auf diesem Rechner beziehungsweise im Repository noch fehlen.
|
||||
@@ -1,39 +0,0 @@
|
||||
# Vendoo Production Updater 2.1
|
||||
|
||||
## Ziel
|
||||
|
||||
Production Updater 2.1 ersetzt den direkten, nicht ausreichend überwachten Worker-Start aus 2.0. Die Oberfläche besitzt jetzt bereits vor dem Worker-Start einen festen Laufzeitordner, einen bekannten Protokollpfad und einen initialen Status.
|
||||
|
||||
## Worker-Host
|
||||
|
||||
Die Oberfläche startet ausschließlich `Vendoo.Updater.Host.ps1`. Dieser kleine Bootstrap-Host:
|
||||
|
||||
- prüft alle Übergabepfade,
|
||||
- schreibt vor dem Laden des Update-Kerns einen Status,
|
||||
- fängt Lade-, Parser- und Initialisierungsfehler des Worker-Kerns ab,
|
||||
- schreibt die vollständige PowerShell-Ausnahme inklusive Datei und Zeile,
|
||||
- garantiert einen terminalen Status,
|
||||
- verliert weder Standardausgabe noch Standardfehler.
|
||||
|
||||
Der eigentliche Update-Kern läuft weiter in `Vendoo.Updater.Worker.ps1`, beendet aber nicht mehr eigenmächtig den PowerShell-Host. Erfolg, Abbruch und Fehler werden ausschließlich über den atomar geschriebenen Statusvertrag gemeldet.
|
||||
|
||||
## Diagnose
|
||||
|
||||
Jeder Lauf besitzt einen eigenen Ordner unter:
|
||||
|
||||
```text
|
||||
%LOCALAPPDATA%\Vendoo\Updater\runtime\<Zeitstempel>
|
||||
```
|
||||
|
||||
Darin liegen mindestens:
|
||||
|
||||
- `status.json`
|
||||
- `events.ndjson`
|
||||
- `updater.log`
|
||||
- bei einem Host-Absturz zusätzlich `worker.stdout.log` und `worker.stderr.log`
|
||||
|
||||
Die Oberfläche zeigt den Protokollnamen sofort an. Mit **Diagnoseordner** kann der vollständige Laufzeitordner geöffnet werden. Der Protokollpfad kann nicht mehr leer sein.
|
||||
|
||||
## Sicherheitsgrenzen
|
||||
|
||||
Die Änderungen betreffen ausschließlich den Updater-Unterbau. Vendoo 1.43.0, `/app/data`, Datenbanken, Uploads, Secrets und Coolify-Konfiguration werden dadurch nicht zurückgesetzt oder gelöscht.
|
||||
@@ -1,25 +0,0 @@
|
||||
# Vendoo Production Updater 2.2
|
||||
|
||||
## Anlass
|
||||
|
||||
Updater 2.1 enthielt zwei echte Windows-PowerShell-Parserfehler. In doppelt quotierten Zeichenketten standen `"$Label: ..."` und `"$Title: ..."`. Windows PowerShell interpretiert den Doppelpunkt als Teil eines qualifizierten Variablenverweises. Korrekt sind `"${Label}: ..."` und `"${Title}: ..."`.
|
||||
|
||||
Der bisherige JavaScript-Vertrag prüfte nur das Vorhandensein bestimmter Textbausteine. Er war kein echter PowerShell-Parser und durfte deshalb nicht als solcher bezeichnet werden.
|
||||
|
||||
## Neue harte Gates
|
||||
|
||||
1. `UPDATE_VENDOO.bat` führt vor dem Start der Oberfläche synchron `Vendoo.Updater.Preflight.ps1` aus.
|
||||
2. Die Preflight-Prüfung verwendet den echter Windows-PowerShell-5.1-Parser über `System.Management.Automation.Language.Parser`.
|
||||
3. UI, Host, Worker und Launcher müssen vollständig parserfehlerfrei sein, bevor die Oberfläche startet.
|
||||
4. Der Worker-Host parst den Worker unmittelbar vor dem Laden erneut.
|
||||
5. `verify-powershell-static.mjs` blockiert die konkrete Variablen-Doppelpunkt-Fehlerklasse bereits auf Linux und während des Paketbaus.
|
||||
6. GitHub Actions besitzt einen eigenen `windows-latest`-Job mit echtem Windows-PowerShell-5.1-Parser.
|
||||
7. Release-Pakete hängen vom erfolgreichen Windows-Parser-Job ab.
|
||||
|
||||
## Fehlerverhalten
|
||||
|
||||
Schlägt die lokale Parserprüfung fehl, startet die Oberfläche nicht. Stattdessen bleiben ein sichtbarer Fehlerdialog, ein Konsolenhinweis und `logs/updater-preflight.log` erhalten. Damit kann kein syntaktisch defekter Worker mehr bis zum Worker-Host gelangen.
|
||||
|
||||
## Unveränderte Sicherheitsgrenzen
|
||||
|
||||
Vendoo 1.43.0, `/app/data`, Datenbanken, Uploads, Backups, Secrets und die Coolify-Konfiguration werden durch diese Änderung nicht zurückgesetzt oder gelöscht.
|
||||
@@ -1,19 +0,0 @@
|
||||
# Vendoo Production Updater 2.3
|
||||
|
||||
## Pfadvertrag
|
||||
|
||||
Version 2.3 entfernt die fehleranfällige Übergabe von `%~dp0` als PowerShell-Parameter. Ein mit Backslash endender Batch-Pfad kann vor einem schließenden Anführungszeichen als Bestandteil des Arguments interpretiert werden und dadurch ein illegales `"` in den Pfad einschleusen.
|
||||
|
||||
Der Paketpfad wird nun ausschließlich aus `$PSScriptRoot` des ausgelieferten Preflight-Skripts abgeleitet, mit `Resolve-Path` kanonisiert und vor jeder Dateisystemoperation validiert. Oberfläche und Worker verwenden denselben Vertrag.
|
||||
|
||||
## Startverhalten
|
||||
|
||||
- `UPDATE_VENDOO.bat` verwendet `pushd` und relative Skriptpfade.
|
||||
- Es wird kein Paketroot mehr über die Kommandozeile übergeben.
|
||||
- Preflight und GUI laufen synchron; Exit-Codes bleiben sichtbar.
|
||||
- Bei Fehlern bleibt die Konsole geöffnet.
|
||||
- Das Preflight-Protokoll liegt unter `logs/updater-preflight.log`.
|
||||
|
||||
## Regression
|
||||
|
||||
Das Release-Gate verbietet `-PackageRoot "%~dp0"`, verlangt `param()` im Preflight und prüft die kanonische Root-Ableitung über `$PSScriptRoot`, `Resolve-Path` und `GetFullPath`.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Vendoo Production Updater 2.4
|
||||
|
||||
## Ziel
|
||||
|
||||
Version 2.4 ersetzt den unsichtbaren interaktiven GitHub-Login aus 2.3. Ein interaktiver Gerätefluss darf nicht in einem versteckten Worker laufen.
|
||||
|
||||
## GitHub-Anmeldung
|
||||
|
||||
Ist noch kein GitHub-Konto angemeldet, öffnet der Updater ein sichtbares Anmeldefenster. Dort zeigt GitHub CLI den einmaligen Gerätecode an, kopiert ihn mit `--clipboard` in die Zwischenablage und öffnet den Webbrowser. Die Vendoo-Oberfläche bleibt geöffnet und prüft den Status still im Hintergrund. Nach erfolgreicher Anmeldung wird automatisch fortgefahren.
|
||||
|
||||
Der Ablauf verwendet:
|
||||
|
||||
```text
|
||||
gh auth login --hostname github.com --git-protocol https --web --clipboard
|
||||
```
|
||||
|
||||
Die Anmeldung läuft nicht mehr über den versteckten Worker-Prozess. Ein Abbruch oder Fehler bleibt sichtbar und führt zu einem klaren, wiederholbaren Fehlerzustand.
|
||||
|
||||
## Downloadstatus
|
||||
|
||||
Der Download der portablen GitHub CLI schreibt nur noch sinnvolle 10-%-Fortschrittsstufen. Wiederholte Meldungen für dieselbe Megabyte-Grenze und die bisherige Logflut sind ausgeschlossen.
|
||||
|
||||
## Weiterhin gültige Schutzregeln
|
||||
|
||||
- echter Windows-PowerShell-5.1-Parser vor dem Start
|
||||
- kanonische Root-Pfadermittlung
|
||||
- persistenter Laufzeit- und Diagnoseordner
|
||||
- keine Änderung an `main` vor grünen Tests
|
||||
- kein Produktions-Erfolg ohne exakte Zielversion
|
||||
- kein Zugriff auf oder Löschen von `/app/data`
|
||||
@@ -1,20 +0,0 @@
|
||||
# Vendoo Production Updater 2.5
|
||||
|
||||
## Zweck
|
||||
|
||||
Version 2.5 ersetzt den fehlerhaft ausgelieferten CMD-Launcher aus 2.4. Die PowerShell- und Vendoo-Logik bleibt unverändert; korrigiert wurde der Windows-Einstiegspunkt.
|
||||
|
||||
## Verbindlicher Launcher-Vertrag
|
||||
|
||||
- `UPDATE_VENDOO.bat` ist reines 7-Bit-ASCII.
|
||||
- Die Datei besitzt keine BOM.
|
||||
- Jede Zeile endet mit Windows-CRLF.
|
||||
- Der Launcher enthält keine Umlaute und keinen `chcp`-Wechsel.
|
||||
- Windows PowerShell wird über den absoluten Systempfad gestartet.
|
||||
- Fehlerzustände bleiben mit `pause` sichtbar.
|
||||
- `.gitattributes` und `.editorconfig` erzwingen CRLF für BAT/CMD.
|
||||
- Das Binär-Gate prüft die tatsächlichen Bytes und nicht nur gelesenen Text.
|
||||
|
||||
## Ursache des 2.4-Fehlers
|
||||
|
||||
Die BAT war UTF-8 mit Unix-LF-Zeilenenden. `cmd.exe` interpretierte dadurch mehrere Zeilen beschädigt und verlor sichtbare Anfangszeichen von Befehlen.
|
||||
@@ -1,22 +0,0 @@
|
||||
# Vendoo Production Updater 2.6
|
||||
|
||||
## Ziel
|
||||
|
||||
Updater 2.6 entfernt die unnötige Coolify-Webhook-Abfrage vollständig.
|
||||
|
||||
Nach dem kontrollierten Merge nach `main` nutzt Vendoo die bereits bestehende GitHub-Verknüpfung von Coolify und wartet auf das automatische Deployment. Der Updater prüft anschließend `/readyz` und akzeptiert den Lauf nur, wenn exakt Vendoo 1.43.0 gemeldet wird.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Die Vendoo-Anwendung ist in Coolify mit `Masterluke77/vendoo` und Branch `main` verbunden.
|
||||
- In Coolify ist für die Anwendung **Auto Deploy** aktiviert.
|
||||
- `https://vendoo.flatlined.de/readyz` ist öffentlich erreichbar.
|
||||
|
||||
## Verhalten
|
||||
|
||||
- keine Eingabe eines Coolify-Webhooks
|
||||
- kein GitHub Secret `COOLIFY_DEPLOY_WEBHOOK_URL`
|
||||
- kein manuell gestarteter Deployment-Workflow
|
||||
- Wartezeit maximal 15 Minuten
|
||||
- Statusmeldung höchstens alle 30 Sekunden
|
||||
- eindeutiger Hinweis auf Auto Deploy, falls die Zielversion nicht erscheint
|
||||
@@ -1,18 +0,0 @@
|
||||
# Vendoo Production Updater 2.8
|
||||
|
||||
## Ziel
|
||||
|
||||
Updater 2.8 behebt einen Windows-spezifischen SQLite-Lifecycle-Fehler im vollständigen Release-Test.
|
||||
|
||||
`verify-initial-disabled-modules-1.41.2.mjs` lud den Plattformkernel und öffnete dadurch `better-sqlite3`, löschte den temporären Datenordner anschließend aber ohne die Datenbank explizit zu schließen. Linux tolerierte das; Windows blockierte `vendoo.db` mit `EBUSY`.
|
||||
|
||||
## Neuer Ressourcenvertrag
|
||||
|
||||
- explizites `wal_checkpoint(TRUNCATE)`
|
||||
- explizites `db.close()` vor jeder Temp-Bereinigung
|
||||
- kurze Freigabephase nach dem synchronen Close
|
||||
- `rmSync` mit 20 Wiederholungen und 100 ms Wartezeit für kurzzeitige Windows-Dateisperren
|
||||
- Cleanup-Fehler werden mit Pfad und Fehlercode gemeldet
|
||||
- drei isolierte Wiederholungsläufe prüfen, dass kein Temp-Ordner zurückbleibt
|
||||
|
||||
Der Fehler tritt vor Pull Request, Merge und Coolify-Deployment auf. Produktivdaten waren dadurch nicht betroffen.
|
||||
@@ -1,11 +0,0 @@
|
||||
# Vendoo Production Updater 2.9
|
||||
|
||||
Updater 2.9 behebt die Windows-Dateisperren in allen aktiven Verifiern systematisch.
|
||||
|
||||
## Änderungen
|
||||
|
||||
- Zentraler SQLite-Test-Cleanup mit WAL-Checkpoint, `db.close()` und Windows-tauglichen Löschwiederholungen.
|
||||
- Einladungs-, Initialmodul-, Capability- und Architekturtest verwenden denselben Cleanup-Vertrag.
|
||||
- Statischer Importgraph-Audit prüft alle aktiven `verify:all`-Verifier auf transitive `lib/db.mjs`-Nutzung.
|
||||
- Dynamische Regression führt alle vier datenbanknahen Testklassen jeweils dreimal in getrennten Node-Prozessen aus.
|
||||
- Kein Pull Request, Merge oder Deployment bei fehlgeschlagenem Testlauf.
|
||||
@@ -1,40 +0,0 @@
|
||||
# Vendoo Production Updater 3.1
|
||||
|
||||
## Architekturwechsel
|
||||
|
||||
Updater 3.1 verwendet keine Ausschlusslisten-Kopie und leert die geklonte `main`-Arbeitskopie nicht mehr.
|
||||
|
||||
Der Release enthält `release-manifest.json` mit:
|
||||
|
||||
- jeder auszuliefernden Quelldatei,
|
||||
- Dateigröße,
|
||||
- SHA-256-Prüfsumme,
|
||||
- verbindlichen Pflichtdateien,
|
||||
- ausschließlich expliziten Löschungen in `removedFiles`.
|
||||
|
||||
## Anwendung
|
||||
|
||||
1. `main` wird in einen isolierten Arbeitsordner geklont.
|
||||
2. Das gesamte Paket wird gegen `release-manifest.json` geprüft.
|
||||
3. Sichere Paketdateien, die nicht im Manifest stehen, blockieren das Update.
|
||||
4. Manipulierte oder fehlende Dateien blockieren das Update.
|
||||
5. Der Release wird als Overlay angewendet; bestehende Baseline-Dateien bleiben erhalten.
|
||||
6. Nur Einträge in `removedFiles` werden gelöscht.
|
||||
7. Jede kopierte Datei wird im Ziel erneut per SHA-256 geprüft.
|
||||
8. Erst danach laufen `npm ci` und `npm run verify:all`.
|
||||
|
||||
## Pflichtdateien
|
||||
|
||||
Unter anderem müssen immer vorhanden sein:
|
||||
|
||||
- `db/schema.sql`
|
||||
- `lib/db.mjs`
|
||||
- `package.json`
|
||||
- `package-lock.json`
|
||||
- `server.mjs`
|
||||
- `bootstrap.mjs`
|
||||
- Updater-Preflight, UI, Host und Worker
|
||||
|
||||
## Sitzungen
|
||||
|
||||
Sitzungsstände der Updater-2.x-Reihe werden nicht übernommen. Updater 3.1 beginnt mit einem neuen Release-Branch und einer neuen, versionsgebundenen Sitzung.
|
||||
@@ -1,48 +0,0 @@
|
||||
# Vendoo Production Updater 3.2
|
||||
|
||||
## Architekturwechsel
|
||||
|
||||
Updater 3.2 verwendet keine Ausschlusslisten-Kopie und leert die geklonte `main`-Arbeitskopie nicht mehr.
|
||||
|
||||
Der Release enthält `release-manifest.json` mit:
|
||||
|
||||
- jeder auszuliefernden Quelldatei,
|
||||
- Dateigröße,
|
||||
- SHA-256-Prüfsumme,
|
||||
- verbindlichen Pflichtdateien,
|
||||
- ausschließlich expliziten Löschungen in `removedFiles`.
|
||||
|
||||
## Anwendung
|
||||
|
||||
1. `main` wird in einen isolierten Arbeitsordner geklont.
|
||||
2. Das gesamte Paket wird gegen `release-manifest.json` geprüft.
|
||||
3. Sichere Paketdateien, die nicht im Manifest stehen, blockieren das Update.
|
||||
4. Manipulierte oder fehlende Dateien blockieren das Update.
|
||||
5. Der Release wird als Overlay angewendet; bestehende Baseline-Dateien bleiben erhalten.
|
||||
6. Nur Einträge in `removedFiles` werden gelöscht.
|
||||
7. Jede kopierte Datei wird im Ziel erneut per SHA-256 geprüft.
|
||||
8. Erst danach laufen `npm ci` und `npm run verify:all`.
|
||||
|
||||
## Pflichtdateien
|
||||
|
||||
Unter anderem müssen immer vorhanden sein:
|
||||
|
||||
- `db/schema.sql`
|
||||
- `lib/db.mjs`
|
||||
- `package.json`
|
||||
- `package-lock.json`
|
||||
- `server.mjs`
|
||||
- `bootstrap.mjs`
|
||||
- Updater-Preflight, UI, Host und Worker
|
||||
|
||||
## Sitzungen
|
||||
|
||||
Sitzungsstände der Updater-2.x-Reihe werden nicht übernommen. Updater 3.2 beginnt mit einem neuen Release-Branch und einer neuen, versionsgebundenen Sitzung.
|
||||
|
||||
## Steuerdatei-Vertrag 3.2
|
||||
|
||||
`release-manifest.json` ist kein selbstgehashter Payload-Eintrag. Der Applier kopiert es nach erfolgreicher Payload-Prüfung als separates Steuerartefakt bytegenau in die Git-Arbeitskopie. Verifier in einer Git-Baseline dürfen zusätzliche unveränderte Dateien sehen, müssen aber jede manifestierte Datei und ihre SHA-256-Prüfsumme bestätigen.
|
||||
|
||||
## Fehlerdarstellung
|
||||
|
||||
Bei fehlgeschlagenen Prozessen priorisiert der Worker `stderr` als **Fehlerausgabe** und zeigt `stdout` nur gekürzt als ergänzende **letzte Standardausgabe**. Dadurch bleibt die eigentliche Ursache im Statusfenster sichtbar.
|
||||
@@ -1,17 +0,0 @@
|
||||
# Vendoo Production Updater 3.3
|
||||
|
||||
## Zweck
|
||||
|
||||
Updater 3.3 behält das manifestbasierte Overlay von 3.2 bei und korrigiert das letzte plattformspezifische Release-Gate.
|
||||
|
||||
## Installer-Shell-Portabilität
|
||||
|
||||
- Unter Linux, NAS und VPS muss `.env` nach der Erzeugung weiterhin exakt POSIX-Modus `0600` besitzen.
|
||||
- Unter Windows prüft Git Bash ausschließlich die Syntax von `setup.sh` und dem funktionalen Shell-Test. Die vollständige Docker-/NAS-/VPS-Simulation läuft im verpflichtenden Linux-CI-Gate, wo POSIX-Rechte real abgebildet werden.
|
||||
- Der Shell-Test erkennt MINGW, MSYS und CYGWIN ausdrücklich.
|
||||
- Jede fehlgeschlagene Assertion zeigt die betroffene Ausgabe- oder Konfigurationsdatei vollständig an.
|
||||
- Der Node-Wrapper reicht `stdout` und `stderr` im Fehlertext weiter.
|
||||
|
||||
## Unveränderte Sicherheitsgrenzen
|
||||
|
||||
Das SHA-256-Manifest, die bytegeprüfte Steuerdatei, explizite Löschungen, GitHub-PR-/Merge-Gates, Coolify Auto Deploy und `/readyz` bleiben verpflichtend.
|
||||
@@ -1,50 +0,0 @@
|
||||
# Vendoo 1.42.0 – Production Operations & Controlled Update Completion
|
||||
|
||||
## Ziel
|
||||
|
||||
Version 1.42.0 vervollständigt den sicheren Coolify-Produktionsbetrieb. Ein HTTP-Erfolg des Deploy-Triggers gilt nicht mehr als erfolgreiches Deployment. Ein Auftrag durchläuft persistent gespeicherte Zustände und wird erst nach bestätigtem Deployment, erfolgreicher Readiness und passender Zielversion abgeschlossen.
|
||||
|
||||
## Neue Betriebsfunktionen
|
||||
|
||||
- Produktionsstatus mit Version, Build-Revision, Provider, öffentlicher URL, Readiness, SQLite-Status, Speicherbelegung, letztem verifiziertem Backup, letztem Deployment und FLUX-Status.
|
||||
- Produktions-Selbstprüfung für HTTPS, Cookies, Trust Proxy, Host/Origin, Persistenz, Schreibrechte, SQLite, Backupziel, FLUX, Auto Deploy und Coolify-Secrets.
|
||||
- Persistente Deployment-State-Machine mit Ereignisverlauf.
|
||||
- Idempotency-Key und Sperre gegen parallele Aufträge.
|
||||
- Verifiziertes Vorab-Backup vor dem Coolify-Trigger.
|
||||
- Coolify-API-Statusabfrage oder kontrollierter Webhook-Modus mit exakter Bestätigung `CONFIRM`.
|
||||
- `/readyz`- und Zielversionsprüfung vor dem Status `succeeded`.
|
||||
- `rollback_required` bei Timeout oder Coolify-Fehler; neue Aufträge bleiben bis zur dokumentierten Prüfung gesperrt.
|
||||
- Einladungsübersicht, 48-Stunden-Ablauf, erneutes Senden und Widerruf; Standardrolle ist `viewer`.
|
||||
|
||||
## Additive Datenbankmigration
|
||||
|
||||
Neu angelegt werden ausschließlich:
|
||||
|
||||
- `deployment_runs`
|
||||
- `deployment_events`
|
||||
- `backup_verifications`
|
||||
- `production_checks`
|
||||
|
||||
Bestehende Tabellen, Nutzer, Artikel, Uploads, Einstellungen, Secrets, Backups und Module werden nicht gelöscht oder zurückgesetzt.
|
||||
|
||||
## Neue administrative API-Verträge
|
||||
|
||||
- `GET /api/admin/deployment`
|
||||
- `PUT /api/admin/deployment`
|
||||
- `POST /api/admin/deployment/test`
|
||||
- `POST /api/admin/deployment/updates/check`
|
||||
- `POST /api/admin/deployment/trigger`
|
||||
- `GET /api/admin/deployment/runs`
|
||||
- `GET /api/admin/deployment/runs/:id`
|
||||
- `POST /api/admin/deployment/runs/:id/refresh`
|
||||
- `POST /api/admin/deployment/runs/:id/confirm`
|
||||
- `GET|POST /api/admin/deployment/production-check`
|
||||
- `GET /api/admin/users/invitations`
|
||||
- `POST /api/admin/users/invitations/:id/resend`
|
||||
- `DELETE /api/admin/users/invitations/:id`
|
||||
|
||||
Alle schreibenden Routen bleiben CSRF-geschützt, rollenbasiert und auditierbar. Secretwerte werden nicht zurückgegeben.
|
||||
|
||||
## Sicherheitsgrenzen
|
||||
|
||||
Vendoo 1.42.0 besitzt weiterhin keinen Docker-Socket und führt im Produktivcontainer weder `git pull`, `npm install`, Docker-Kommandos noch eine Selbstüberschreibung aus. Coolify bleibt für Build, Containerwechsel und Rollback verantwortlich.
|
||||
@@ -1,12 +0,0 @@
|
||||
# Vendoo 1.43.0
|
||||
|
||||
## Schwerpunkt
|
||||
|
||||
- Modulstatus und Navigation sind verbindlich gekoppelt.
|
||||
- Das optionale Produktbild Studio ist standardmäßig deaktiviert.
|
||||
- Der Listing-Generator besitzt eine dauerhaft sichtbare Aktionsleiste.
|
||||
- `UPDATE_VENDOO.bat` startet eine grafische Statusanzeige mit Live-Protokoll.
|
||||
|
||||
## Sicherheitsgrenzen
|
||||
|
||||
Der Updater verändert weiterhin keine produktiven Daten unter `/app/data`, überträgt keine `.env`-Dateien und meldet erst nach erfolgreicher `/readyz`-Zielversionsprüfung Erfolg.
|
||||
@@ -1,44 +0,0 @@
|
||||
# Vendoo 1.42.0 – Restore- und Notfall-Runbook
|
||||
|
||||
## Grundsatz
|
||||
|
||||
Ein Restore ist keine normale Weboperation. Das laufende Vendoo darf seine geöffnete SQLite-Datenbank nicht im Hintergrund überschreiben. Die Weboberfläche prüft und staged ein Backup; das tatsächliche Einspielen erfolgt im Wartungsfenster bei gestoppter Anwendung.
|
||||
|
||||
## Restore-Dry-Run
|
||||
|
||||
1. **Administration → System → Backup & Restore** öffnen.
|
||||
2. Gewünschtes Backup zuerst mit **Prüfen** validieren.
|
||||
3. SHA-256, Manifest, Dateigröße und SQLite-Integrität müssen stimmen.
|
||||
4. Für ein extern bereitgestelltes ZIP **Wiederherstellung prüfen und vorbereiten** verwenden.
|
||||
5. Vendoo erstellt vor dem Staging automatisch ein zusätzliches Sicherheitsbackup.
|
||||
6. Fehlerhafte, unvollständige oder verbotene Pakete nicht manuell entpacken.
|
||||
|
||||
## Wartungsrestore
|
||||
|
||||
1. Wartungsfenster ankündigen und Benutzer abmelden.
|
||||
2. Coolify-Anwendung stoppen; keine zweite Instanz starten.
|
||||
3. Vollständiges Volume `/app/data` zusätzlich auf Host-/NAS-Ebene sichern.
|
||||
4. Besonders zusammen sichern:
|
||||
- `/app/data/db/vendoo.db` samt möglicher `-wal`/`-shm`-Dateien
|
||||
- `/app/data/uploads`
|
||||
- `/app/data/config/master.key`
|
||||
- `/app/data/config/secrets.enc.json`
|
||||
- `/app/data/config/vendoo.env`
|
||||
- `/app/data/config/modules-state.json`
|
||||
5. Nur das verifizierte, vorbereitete Restore anwenden.
|
||||
6. Eigentümer und Schreibrechte des nicht privilegierten Containers erhalten.
|
||||
7. Anwendung mit exakt einer Instanz starten.
|
||||
8. `/readyz`, SQLite-Integrität, Login, Benutzer, Artikel, Uploads, Module und Audit prüfen.
|
||||
|
||||
## Reiner Anwendungsrollback
|
||||
|
||||
Bei fehlerhaftem Code-Release normalerweise keinen Datenrestore durchführen. In Coolify die vorherige Revision beziehungsweise das vorherige Image redeployen und dasselbe `/app/data`-Volume verwenden.
|
||||
|
||||
## Verboten
|
||||
|
||||
- `/app/data` löschen
|
||||
- neues leeres Volume anhängen
|
||||
- zwei schreibende Container auf demselben SQLite-Volume starten
|
||||
- die Datenbank bei laufendem Vendoo ersetzen
|
||||
- `master.key` neu erzeugen, solange bestehende verschlüsselte Secrets benötigt werden
|
||||
- ungeprüfte ZIP-Dateien direkt in das Volume kopieren
|
||||
@@ -1,14 +0,0 @@
|
||||
# Vendoo 1.43.0 – Updater-Hotfix H1
|
||||
|
||||
## Behoben
|
||||
|
||||
- Endloses Warten direkt nach „Node.js LTS gefunden“.
|
||||
- Falsche Erkennung des Windows-App-Execution-Alias als echte Node.js-Installation.
|
||||
- Node.js-Versionsprüfung besitzt jetzt ein hartes Timeout von 20 Sekunden.
|
||||
- Eine blockierte oder defekte Node.js-LTS-Installation wird über Winget automatisch repariert.
|
||||
- Der grafische Updater zeigt Laufzeit und Zeit seit der letzten Statusmeldung.
|
||||
- PowerShell- und Prozessausgabe verwenden konsistent UTF-8; deutsche Umlaute werden korrekt dargestellt.
|
||||
|
||||
## Sicherheitsgrenze
|
||||
|
||||
Bis zum Abschluss der lokalen Prüfungen wird weder nach `main` gemergt noch ein Coolify-Deployment gestartet.
|
||||
@@ -1,16 +0,0 @@
|
||||
# Vendoo 1.43.0 – Updater Hotfix H2
|
||||
|
||||
## Behobener Fehler
|
||||
|
||||
Nach einer erfolgreichen Erstinstallation der GitHub CLI brach der Ein-Klick-Updater mit der Aufforderung ab, `UPDATE_VENDOO.bat` erneut zu starten. Ursache war, dass die laufende Windows-PowerShell-Sitzung den neu installierten Pfad noch nicht zuverlässig aufgelöst hatte.
|
||||
|
||||
## Neue Funktionsweise
|
||||
|
||||
- Der Updater aktualisiert den Prozess-PATH nach der Winget-Installation.
|
||||
- Er wartet kontrolliert bis zu 30 Sekunden auf die Registrierung der Programmdatei.
|
||||
- Zusätzlich werden die offiziellen Installationspfade der GitHub CLI direkt geprüft.
|
||||
- Gefundene Programme werden über ihren absoluten Pfad ausgeführt.
|
||||
- Nach der Installation läuft derselbe Updatevorgang ohne zweiten Doppelklick weiter.
|
||||
- Ein echter Abbruch erfolgt nur, wenn die installierte Programmdatei auch über die bekannten Pfade nicht auffindbar ist.
|
||||
|
||||
Produktcode und Datenbankschema bleiben gegenüber Vendoo 1.43.0 unverändert.
|
||||
-81
@@ -1550,84 +1550,3 @@ try {
|
||||
db.prepare(`INSERT OR IGNORE INTO system_migrations (migration_key, app_version, description)
|
||||
VALUES ('operations-center-1.30.0', '1.30.0', 'Backup, Restore, Update Center und Extension-Diagnose')`).run();
|
||||
} catch {}
|
||||
|
||||
|
||||
// --- Production Operations & Controlled Updates 1.42.0 ---
|
||||
try {
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS deployment_runs (
|
||||
id TEXT PRIMARY KEY,
|
||||
idempotency_key TEXT NOT NULL UNIQUE,
|
||||
state TEXT NOT NULL DEFAULT 'checking',
|
||||
provider TEXT NOT NULL DEFAULT 'coolify',
|
||||
trigger_mode TEXT NOT NULL DEFAULT 'webhook',
|
||||
current_version TEXT NOT NULL,
|
||||
target_version TEXT NOT NULL,
|
||||
build_revision_before TEXT,
|
||||
build_revision_after TEXT,
|
||||
deployment_id TEXT,
|
||||
backup_id TEXT,
|
||||
requested_by INTEGER,
|
||||
reason TEXT,
|
||||
github_repository TEXT,
|
||||
github_branch TEXT,
|
||||
request_json TEXT NOT NULL DEFAULT '{}',
|
||||
result_json TEXT NOT NULL DEFAULT '{}',
|
||||
error_code TEXT,
|
||||
error_message TEXT,
|
||||
timeout_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
started_at TEXT,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
completed_at TEXT,
|
||||
FOREIGN KEY(requested_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY(backup_id) REFERENCES operation_backups(id) ON DELETE SET NULL
|
||||
)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_deployment_runs_state ON deployment_runs(state, updated_at DESC)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_deployment_runs_created ON deployment_runs(created_at DESC)`);
|
||||
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS deployment_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
run_id TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
details_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY(run_id) REFERENCES deployment_runs(id) ON DELETE CASCADE
|
||||
)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_deployment_events_run ON deployment_events(run_id, id ASC)`);
|
||||
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS backup_verifications (
|
||||
id TEXT PRIMARY KEY,
|
||||
backup_id TEXT NOT NULL,
|
||||
deployment_run_id TEXT,
|
||||
status TEXT NOT NULL,
|
||||
sha256 TEXT,
|
||||
file_size INTEGER NOT NULL DEFAULT 0,
|
||||
sqlite_integrity TEXT,
|
||||
manifest_json TEXT NOT NULL DEFAULT '{}',
|
||||
errors_json TEXT NOT NULL DEFAULT '[]',
|
||||
warnings_json TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
verified_at TEXT,
|
||||
FOREIGN KEY(backup_id) REFERENCES operation_backups(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(deployment_run_id) REFERENCES deployment_runs(id) ON DELETE SET NULL
|
||||
)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_backup_verifications_backup ON backup_verifications(backup_id, created_at DESC)`);
|
||||
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS production_checks (
|
||||
id TEXT PRIMARY KEY,
|
||||
overall_status TEXT NOT NULL,
|
||||
passed_count INTEGER NOT NULL DEFAULT 0,
|
||||
warning_count INTEGER NOT NULL DEFAULT 0,
|
||||
failed_count INTEGER NOT NULL DEFAULT 0,
|
||||
results_json TEXT NOT NULL DEFAULT '[]',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
created_by INTEGER,
|
||||
FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE SET NULL
|
||||
)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_production_checks_created ON production_checks(created_at DESC)`);
|
||||
|
||||
db.prepare(`INSERT OR IGNORE INTO system_migrations (migration_key, app_version, description)
|
||||
VALUES ('production-operations-1.42.0', '1.42.0', 'Persistente Deployment-State-Machine, Deployment-Events, Backup-Verifikation und Produktionschecks')`).run();
|
||||
} catch {}
|
||||
|
||||
@@ -330,33 +330,20 @@ export function cancelPendingOperation() {
|
||||
return { cancelled: true, pending };
|
||||
}
|
||||
|
||||
export async function checkForUpdates({ channel = 'stable' } = {}) {
|
||||
export async function checkForUpdates() {
|
||||
const settings = getOperationSettings();
|
||||
const rawUrl = String(settings.update_manifest_url || '').trim();
|
||||
if (!rawUrl) return { configured: false, current_version: APP_VERSION, channel, message: 'Keine Update-Manifest-URL konfiguriert.' };
|
||||
let url;
|
||||
try { url = new URL(rawUrl); } catch { throw new Error('Update-Manifest-URL ist ungültig.'); }
|
||||
if (!['https:', 'http:'].includes(url.protocol)) throw new Error('Update-Manifest muss über HTTP oder HTTPS erreichbar sein.');
|
||||
if (url.username || url.password || url.hash) throw new Error('Update-Manifest-URL darf keine Zugangsdaten oder Fragmente enthalten.');
|
||||
if (process.env.NODE_ENV === 'production' && url.protocol !== 'https:') throw new Error('Update-Manifest muss in Produktion HTTPS verwenden.');
|
||||
const url = String(settings.update_manifest_url || '').trim();
|
||||
if (!url) return { configured: false, current_version: APP_VERSION, message: 'Keine Update-Manifest-URL konfiguriert.' };
|
||||
if (!/^https?:\/\//i.test(url)) throw new Error('Update-Manifest muss über HTTP oder HTTPS erreichbar sein.');
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
redirect: 'error',
|
||||
headers: { Accept: 'application/json', 'User-Agent': `Vendoo/${APP_VERSION}` },
|
||||
});
|
||||
const response = await fetch(url, { signal: controller.signal, headers: { Accept: 'application/json' } });
|
||||
if (!response.ok) throw new Error(`Update-Manifest antwortet mit HTTP ${response.status}.`);
|
||||
const contentLength = Number(response.headers.get('content-length') || 0);
|
||||
if (contentLength > 256 * 1024) throw new Error('Update-Manifest ist größer als 256 KB.');
|
||||
const text = await response.text();
|
||||
if (Buffer.byteLength(text, 'utf8') > 256 * 1024) throw new Error('Update-Manifest ist größer als 256 KB.');
|
||||
let manifest;
|
||||
try { manifest = JSON.parse(text); } catch { throw new Error('Update-Manifest enthält kein gültiges JSON.'); }
|
||||
const manifest = await response.json();
|
||||
const latest = String(manifest.version || manifest.latest_version || '');
|
||||
if (!latest) throw new Error('Update-Manifest enthält keine Version.');
|
||||
return { configured: true, current_version: APP_VERSION, latest_version: latest, update_available: compareVersions(latest, APP_VERSION) > 0, channel, manifest };
|
||||
return { configured: true, current_version: APP_VERSION, latest_version: latest, update_available: compareVersions(latest, APP_VERSION) > 0, manifest };
|
||||
} finally { clearTimeout(timeout); }
|
||||
}
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "vendoo",
|
||||
"version": "1.43.0",
|
||||
"version": "1.41.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vendoo",
|
||||
"version": "1.43.0",
|
||||
"version": "1.41.2",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
|
||||
+11
-20
@@ -1,49 +1,40 @@
|
||||
{
|
||||
"name": "vendoo",
|
||||
"version": "1.43.0",
|
||||
"version": "1.41.2",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node bootstrap.mjs",
|
||||
"dev": "node --watch bootstrap.mjs",
|
||||
"check": "node tools/check-syntax.mjs && node --no-warnings tools/verify-identity-db-1.40.0.mjs",
|
||||
"check": "node tools/check-syntax.mjs && python -m py_compile tools/verify-identity-db-1.40.0.py",
|
||||
"verify:slice30": "node tools/verify-slice30-mock.mjs",
|
||||
"verify:db": "node --no-warnings tools/verify-db-migrations.mjs",
|
||||
"verify:db": "python tools/verify-db-migrations.py",
|
||||
"verify:slice31": "node tools/verify-slice31-image-editor.mjs",
|
||||
"verify:hotfix": "node tools/verify-release-1.25.1.mjs",
|
||||
"verify:gallery": "node tools/verify-release-1.25.1-static.mjs",
|
||||
"verify:release": "npm run verify:production-operations",
|
||||
"verify:release": "node tools/verify-deployment-1.41.2.mjs",
|
||||
"verify:ui-contracts": "node tools/verify-ui-contracts-1.26.1.mjs",
|
||||
"verify:deployment": "npm run verify:production-operations",
|
||||
"verify:deployment": "node tools/verify-deployment-1.41.2.mjs",
|
||||
"verify:git": "node tools/verify-git-safety.mjs",
|
||||
"verify:config": "node tools/verify-config-store.mjs",
|
||||
"verify:installer-shell": "node tools/verify-installer-shell.mjs",
|
||||
"verify:installer-shell": "sh tools/verify-installer-shell.sh",
|
||||
"verify:git-regression": "node tools/verify-git-safety-regression.mjs",
|
||||
"verify:git-staging": "node tools/verify-git-staging.mjs",
|
||||
"generate:tokens": "node tools/generate-design-tokens.mjs",
|
||||
"verify:architecture": "node tools/verify-platform-architecture-1.41.2.mjs",
|
||||
"verify:all": "npm run verify:git && npm run verify:git-regression && npm run verify:git-staging && npm run verify:release-manifest && npm run verify:ignore-boundaries && npm run check && npm run verify:powershell-static && npm run verify:source-boundaries && npm run verify:esm-exports && npm run verify:flux-loop && npm run verify:capabilities && npm run verify:coolify && npm run verify:initial-modules && npm run verify:architecture && npm run verify:themes && npm run verify:security && npm run verify:identity && npm run verify:invitations && npm run verify:sqlite-cleanup && npm run verify:catalog && npm run verify:modules && npm run verify:production-operations && npm run verify:one-click-update && npm run verify:module-navigation && npm run verify:updater-ui && npm run verify:updater-hotfix && npm run verify:deployment && npm run verify:config && npm run verify:installer-shell && npm run verify:db",
|
||||
"verify:all": "npm run verify:git && npm run verify:git-regression && npm run verify:git-staging && npm run verify:ignore-boundaries && npm run check && npm run verify:source-boundaries && npm run verify:esm-exports && npm run verify:flux-loop && npm run verify:capabilities && npm run verify:coolify && npm run verify:initial-modules && npm run verify:architecture && npm run verify:themes && npm run verify:security && npm run verify:identity && npm run verify:catalog && npm run verify:modules && npm run verify:deployment && npm run verify:config && npm run verify:installer-shell && npm run verify:db",
|
||||
"verify:themes": "node tools/verify-theme-manager-1.41.2.mjs",
|
||||
"verify:security": "node tools/verify-security-foundation-1.41.2.mjs",
|
||||
"verify:identity": "node tools/verify-identity-settings-1.41.2.mjs && node --no-warnings tools/verify-identity-db-1.40.0.mjs",
|
||||
"verify:identity": "node tools/verify-identity-settings-1.41.2.mjs && python tools/verify-identity-db-1.40.0.py",
|
||||
"verify:catalog": "node tools/verify-catalog-media-1.41.2.mjs && node tools/verify-catalog-services-1.41.2.mjs",
|
||||
"verify:modules": "node tools/verify-module-manager-1.41.2.mjs",
|
||||
"verify:esm-exports": "node tools/verify-esm-export-contracts.mjs",
|
||||
"verify:flux-loop": "node tools/verify-flux-loop-hotfix-1.41.2.mjs",
|
||||
"verify:capabilities": "node tools/verify-capability-graph-1.41.2.mjs",
|
||||
"verify:coolify": "npm run verify:production-operations",
|
||||
"verify:initial-modules": "node tools/verify-initial-disabled-modules-1.41.2.mjs && node tools/verify-initial-module-cleanup-regression-1.43.0.mjs",
|
||||
"verify:coolify": "node tools/verify-coolify-deployment-1.41.2.mjs",
|
||||
"verify:initial-modules": "node tools/verify-initial-disabled-modules-1.41.2.mjs",
|
||||
"verify:source-boundaries": "node tools/verify-source-boundaries-1.41.2.mjs",
|
||||
"verify:ignore-boundaries": "node tools/verify-git-ignore-boundaries-1.41.2.mjs",
|
||||
"verify:production-operations": "node tools/verify-production-operations-1.42.0.mjs && node --no-warnings tools/verify-production-operations-db-1.42.0.mjs",
|
||||
"verify:invitations": "node tools/verify-controlled-invitations-1.42.0.mjs",
|
||||
"verify:one-click-update": "node tools/verify-production-updater-3.3.mjs",
|
||||
"verify:module-navigation": "node tools/verify-module-navigation-generator-1.43.0.mjs",
|
||||
"verify:updater-ui": "node tools/verify-production-updater-3.3.mjs",
|
||||
"verify:updater-hotfix": "node tools/verify-production-updater-3.3.mjs",
|
||||
"verify:powershell-static": "node tools/verify-powershell-static.mjs",
|
||||
"verify:sqlite-cleanup": "node tools/verify-sqlite-cleanup-contract-1.43.0.mjs && node tools/verify-windows-sqlite-cleanup-regression-1.43.0.mjs",
|
||||
"verify:release-manifest": "node tools/verify-release-manifest-staging-3.0.mjs"
|
||||
"verify:ignore-boundaries": "node tools/verify-git-ignore-boundaries-1.41.2.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.39.0",
|
||||
|
||||
+21
-230
@@ -1,4 +1,4 @@
|
||||
const VENDOO_UI_BUILD = '1.43.0';
|
||||
const VENDOO_UI_BUILD = '1.41.2';
|
||||
|
||||
const state = {
|
||||
photos: [],
|
||||
@@ -282,44 +282,6 @@ function setupResponsiveContracts() {
|
||||
}
|
||||
}
|
||||
|
||||
const moduleUiState = new Map();
|
||||
|
||||
function isUiModuleEnabled(id) {
|
||||
return !id || moduleUiState.get(id) !== false;
|
||||
}
|
||||
|
||||
function applyModuleVisibility(root = document) {
|
||||
root.querySelectorAll?.('[data-module], [data-module-panel]').forEach(element => {
|
||||
const moduleId = element.dataset.module || element.dataset.modulePanel;
|
||||
const visible = isUiModuleEnabled(moduleId);
|
||||
element.classList.toggle('hidden', !visible);
|
||||
element.setAttribute('aria-hidden', visible ? 'false' : 'true');
|
||||
if ('disabled' in element && !visible) element.disabled = true;
|
||||
});
|
||||
}
|
||||
|
||||
function applyModuleNavigation(payload = {}) {
|
||||
moduleUiState.clear();
|
||||
for (const module of payload.modules || []) moduleUiState.set(module.id, module.enabled === true);
|
||||
applyModuleVisibility(document);
|
||||
const active = document.querySelector('.tab-content.active');
|
||||
const activeButton = active ? document.querySelector(`.nav-item[data-tab="${active.id}"]`) : null;
|
||||
if (activeButton?.classList.contains('hidden')) switchToTab('dashboard');
|
||||
}
|
||||
|
||||
async function refreshModuleNavigation() {
|
||||
try {
|
||||
const payload = await api('GET', '/api/modules/navigation', null, { retry: false, timeoutMs: 10000 });
|
||||
applyModuleNavigation(payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
console.warn('[Vendoo] Modulnavigation konnte nicht geladen werden:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('vendoo:modules-changed', () => refreshModuleNavigation());
|
||||
|
||||
async function init() {
|
||||
document.documentElement.dataset.vendooBuild = VENDOO_UI_BUILD;
|
||||
console.info(`[Vendoo] UI-Build ${VENDOO_UI_BUILD}`);
|
||||
@@ -333,16 +295,10 @@ async function init() {
|
||||
if (meData.setupMode) { window.location.href = '/login.html'; return; }
|
||||
} catch {}
|
||||
|
||||
// Modulstatus immer zuerst laden: Ein deaktiviertes Modul darf weder seine
|
||||
// Navigation anzeigen noch den App-Start durch eine gesperrte API blockieren.
|
||||
const moduleNavigation = await api('GET', '/api/modules/navigation');
|
||||
applyModuleNavigation(moduleNavigation);
|
||||
const aiEnabled = isUiModuleEnabled('vendoo.ai');
|
||||
|
||||
const [platforms, settings, providers, templates] = await Promise.all([
|
||||
api('GET', '/api/platforms'),
|
||||
api('GET', '/api/settings'),
|
||||
aiEnabled ? api('GET', '/api/providers') : Promise.resolve([]),
|
||||
api('GET', '/api/providers'),
|
||||
api('GET', '/api/templates'),
|
||||
]);
|
||||
|
||||
@@ -1748,12 +1704,6 @@ function closeMobileSidebar({ restoreFocus = false } = {}) {
|
||||
}
|
||||
|
||||
function switchToTab(tabId, adminTab) {
|
||||
const requestedNav = document.querySelector(`.nav-item[data-tab="${tabId}"]`);
|
||||
if (requestedNav?.classList.contains('hidden')) {
|
||||
toast('Dieses Modul ist derzeit deaktiviert.');
|
||||
tabId = 'dashboard';
|
||||
adminTab = null;
|
||||
}
|
||||
if (tabId !== 'history' && listingEditLock.listingId) releaseListingLock({ quiet: true });
|
||||
document.querySelectorAll('.nav-item').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
@@ -2514,8 +2464,7 @@ function showListing(listing, platformHtml) {
|
||||
document.getElementById('result-tags').value = tags;
|
||||
const plat = state.platforms.find(p => p.id === state.platform);
|
||||
document.getElementById('tags-label').textContent = plat?.fields.includes('hashtags') ? 'Hashtags' : 'Tags';
|
||||
const saveButton = document.getElementById('save-btn');
|
||||
if (saveButton) { saveButton.textContent = listing.id ? 'Änderungen speichern' : 'Artikel speichern'; saveButton.disabled = false; }
|
||||
document.getElementById('save-btn').textContent = listing.id ? 'Änderungen speichern' : 'Speichern & Hinzufügen';
|
||||
document.getElementById('generator-preview-empty')?.classList.add('hidden');
|
||||
document.getElementById('result-section').classList.remove('hidden');
|
||||
const previewStatus = document.getElementById('generator-preview-status');
|
||||
@@ -2619,8 +2568,7 @@ function resetGenerator() {
|
||||
document.getElementById('meta-section').classList.add('hidden');
|
||||
document.getElementById('variants-section').classList.add('hidden');
|
||||
document.getElementById('generator-preview-empty')?.classList.remove('hidden');
|
||||
const saveButton = document.getElementById('save-btn');
|
||||
if (saveButton) { saveButton.textContent = 'Speichern'; saveButton.disabled = true; }
|
||||
document.getElementById('save-btn').textContent = 'Speichern & Hinzufügen';
|
||||
document.getElementById('generate-btn').disabled = true;
|
||||
renderAiModelGrid('generator', []);
|
||||
setAiModelStatus('generator-ai-model-status', 'Wartet auf Produktfotos.', 'neutral');
|
||||
@@ -3449,13 +3397,13 @@ async function showDetail(id) {
|
||||
<button id="detail-download-photos" class="vd-button compact" type="button">Fotos herunterladen</button>
|
||||
</div>
|
||||
<p class="detail-photo-help">Vorschaubilder ziehen, um die Reihenfolge zu ändern. Das erste Bild ist das Cover.</p>
|
||||
<section class="ai-model-card detail-ai-model-card hidden" data-module-panel="vendoo.product-image-tools">
|
||||
<section class="ai-model-card detail-ai-model-card">
|
||||
<div class="ai-model-card-head">
|
||||
<div>
|
||||
<span class="generator-panel-index">AI</span>
|
||||
<div>
|
||||
<h4>Produktbild Studio</h4>
|
||||
<p>Optionales Modul für kontrollierte Ghost-Mannequin- und Flatlay-Varianten aus den aktuellen Listing-Fotos.</p>
|
||||
<h4>Produktbild-Werkzeuge</h4>
|
||||
<p>Erzeuge Ghost-Mannequin- oder Flatlay-Varianten aus den aktuellen Listing-Fotos. Freie Prompt-Bilder werden im FLUX Studio erstellt.</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="generator-beta">Safe</span>
|
||||
@@ -3566,8 +3514,6 @@ async function showDetail(id) {
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
applyModuleVisibility(document.getElementById('detail-content'));
|
||||
|
||||
const lockResult = await acquireListingLock(numericId);
|
||||
if (!userCan('listings.edit') || lockResult.readOnly) {
|
||||
document.querySelectorAll('#history-detail input, #history-detail textarea, #history-detail select').forEach(element => { element.disabled = true; });
|
||||
@@ -3599,18 +3545,16 @@ async function showDetail(id) {
|
||||
if (element) { element.disabled = true; element.setAttribute('aria-disabled', 'true'); }
|
||||
});
|
||||
}
|
||||
if (isUiModuleEnabled('vendoo.product-image-tools')) {
|
||||
document.getElementById('detail-ai-model-btn')?.addEventListener('click', runDetailAiModelPhotos);
|
||||
document.getElementById('detail-ai-model-prompt-btn')?.addEventListener('click', () => openAiPromptModal('detail'));
|
||||
document.getElementById('detail-ai-model-mode')?.addEventListener('change', () => updateAiModeControls('detail'));
|
||||
updateAiModeControls('detail');
|
||||
renderAiModelGrid('detail', []);
|
||||
loadDetailAiModelPhotos(numericId);
|
||||
loadDetailAiModelGenerations(numericId);
|
||||
}
|
||||
document.getElementById('detail-ai-model-btn')?.addEventListener('click', runDetailAiModelPhotos);
|
||||
document.getElementById('detail-ai-model-prompt-btn')?.addEventListener('click', () => openAiPromptModal('detail'));
|
||||
document.getElementById('detail-ai-model-mode')?.addEventListener('change', () => updateAiModeControls('detail'));
|
||||
updateAiModeControls('detail');
|
||||
renderDetailPhotos();
|
||||
renderAiModelGrid('detail', []);
|
||||
addQuillTooltips();
|
||||
refreshDetailPlatformPreview();
|
||||
loadDetailAiModelPhotos(numericId);
|
||||
loadDetailAiModelGenerations(numericId);
|
||||
|
||||
if (typeof QRious !== 'undefined') {
|
||||
new QRious({ element: document.getElementById('detail-qr-canvas'), value: JSON.stringify({ sku: skuVal, id: l.id }), size: 118, level: 'M' });
|
||||
@@ -8163,92 +8107,6 @@ async function loadOperationsExtensions() {
|
||||
catch (error) { const box = document.getElementById('operations-extension-clients'); if (box) box.innerHTML = `<div class="empty-state">${escapeHtml(error.message)}</div>`; }
|
||||
}
|
||||
|
||||
function deploymentStateLabel(state) {
|
||||
return ({
|
||||
checking: 'Prüfung', backup_pending: 'Backup geplant', backup_running: 'Backup läuft',
|
||||
backup_verified: 'Backup verifiziert', deploy_requested: 'Deployment angefordert',
|
||||
deploy_running: 'Deployment läuft', health_wait: 'Readiness wird geprüft',
|
||||
succeeded: 'Erfolgreich', failed: 'Fehlgeschlagen', rollback_required: 'Rollback/Prüfung erforderlich',
|
||||
})[state] || state || 'Unbekannt';
|
||||
}
|
||||
|
||||
function deploymentStateClass(state) {
|
||||
if (state === 'succeeded') return 'is-ok';
|
||||
if (state === 'failed' || state === 'rollback_required') return 'is-failed';
|
||||
return 'is-running';
|
||||
}
|
||||
|
||||
function renderProductionOverview(data) {
|
||||
const box = document.getElementById('deployment-production-overview');
|
||||
if (!box) return;
|
||||
const production = data.production || {};
|
||||
const readiness = production.readiness || {};
|
||||
const storage = production.storage || {};
|
||||
const lastBackup = production.last_backup;
|
||||
const flux = production.flux || {};
|
||||
const cards = [
|
||||
['Version', production.version || data.current_version || VENDOO_UI_BUILD, production.build_revision ? `Build ${String(production.build_revision).slice(0, 12)}` : 'Keine Build-Revision gemeldet', true],
|
||||
['Readiness', readiness.ok ? 'Bereit' : 'Nicht bereit', readiness.error || readiness.message || 'Interner Readiness-Check', !!readiness.ok],
|
||||
['Datenbank', production.database?.integrity === 'ok' ? 'Integrität OK' : String(production.database?.integrity || 'Unbekannt'), production.database?.path || 'Kein Pfad gemeldet', production.database?.integrity === 'ok'],
|
||||
['Persistente Daten', operationFormatBytes(storage.data?.bytes || 0), `${Number(storage.data?.files || 0)} Einträge · Uploads ${operationFormatBytes(storage.uploads?.bytes || 0)}`, true],
|
||||
['Letztes Backup', lastBackup ? (lastBackup.status === 'verified' ? 'Verifiziert' : lastBackup.status) : 'Nicht vorhanden', lastBackup ? `${operationFormatDate(lastBackup.created_at)} · ${operationFormatBytes(lastBackup.file_size)}` : 'Vor dem ersten Deployment erforderlich', lastBackup?.status === 'verified'],
|
||||
['FLUX Studio', flux.enabled ? 'Aktiv' : 'Deaktiviert', flux.enabled ? 'Produktionsgrenze prüfen' : 'Wie vorgesehen deaktiviert', !flux.enabled],
|
||||
];
|
||||
box.innerHTML = cards.map(([label, value, detail, ok]) => `<article class="deployment-production-item ${ok ? 'is-ok' : 'is-warning'}"><span data-vd-icon="${ok ? 'check-circle-2' : 'triangle-alert'}"></span><div><small>${escapeHtml(label)}</small><strong>${escapeHtml(String(value))}</strong><p>${escapeHtml(String(detail || ''))}</p></div></article>`).join('');
|
||||
|
||||
const checkBox = document.getElementById('deployment-production-check-result');
|
||||
const check = data.latest_production_check;
|
||||
if (checkBox && check) {
|
||||
checkBox.innerHTML = `<strong>${check.overall_status === 'passed' ? 'Produktionscheck bestanden' : check.overall_status === 'warning' ? 'Produktionscheck mit Hinweisen' : 'Produktionscheck fehlgeschlagen'}</strong><span>${Number(check.passed_count || 0)} bestanden · ${Number(check.warning_count || 0)} Hinweise · ${Number(check.failed_count || 0)} Fehler · ${operationFormatDate(check.created_at || check.verified_at)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDeploymentRuns(data) {
|
||||
const list = document.getElementById('deployment-run-list');
|
||||
if (!list) return;
|
||||
const runs = data.recent_runs || [];
|
||||
if (!runs.length) { list.innerHTML = '<div class="empty-state">Keine Deployment-Aufträge vorhanden.</div>'; return; }
|
||||
list.innerHTML = runs.map(run => {
|
||||
const lastEvent = run.events?.at?.(-1);
|
||||
const active = !['succeeded', 'failed'].includes(run.state);
|
||||
return `<article class="deployment-run ${deploymentStateClass(run.state)}">
|
||||
<div class="deployment-run-head"><span class="deployment-run-state">${escapeHtml(deploymentStateLabel(run.state))}</span><time>${operationFormatDate(run.created_at)}</time></div>
|
||||
<strong>${escapeHtml(run.current_version || '?')} → ${escapeHtml(run.target_version || '?')}</strong>
|
||||
<p>${escapeHtml(lastEvent?.message || run.error_message || run.reason || 'Deployment-Auftrag')}</p>
|
||||
<small>${escapeHtml(run.github_branch || 'main')}${run.backup_id ? ' · Backup verifiziert' : ''}${run.deployment_id ? ` · ID ${escapeHtml(String(run.deployment_id).slice(0, 18))}` : ''}</small>
|
||||
${active ? `<div class="deployment-run-actions">${run.state !== 'rollback_required' ? `<button type="button" class="secondary-btn compact" data-deployment-refresh="${escapeHtml(run.id)}">Status aktualisieren</button>` : ''}${data.trigger_mode === 'webhook' && ['deploy_requested','deploy_running','health_wait'].includes(run.state) ? `<button type="button" class="secondary-btn compact" data-deployment-confirm="${escapeHtml(run.id)}">Externen Abschluss bestätigen</button>` : ''}${run.state === 'rollback_required' ? `<button type="button" class="secondary-btn compact" data-deployment-resolve="${escapeHtml(run.id)}">Prüfung abschließen</button>` : ''}</div>` : ''}
|
||||
</article>`;
|
||||
}).join('');
|
||||
list.querySelectorAll('[data-deployment-refresh]').forEach(button => button.addEventListener('click', async () => {
|
||||
button.disabled = true;
|
||||
try { await api('POST', `/api/admin/deployment/runs/${encodeURIComponent(button.dataset.deploymentRefresh)}/refresh`); await loadDeploymentStatus(); }
|
||||
catch (error) { toast(error.message); }
|
||||
finally { button.disabled = false; }
|
||||
}));
|
||||
list.querySelectorAll('[data-deployment-confirm]').forEach(button => button.addEventListener('click', async () => {
|
||||
const confirmation = prompt('Nur bestätigen, wenn Coolify das Deployment wirklich abgeschlossen hat. Exakt CONFIRM eingeben:');
|
||||
if (confirmation !== 'CONFIRM') return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api('POST', `/api/admin/deployment/runs/${encodeURIComponent(button.dataset.deploymentConfirm)}/confirm`, { confirmation, outcome: 'succeeded', message: 'Im Vendoo Update Center extern bestätigt.' });
|
||||
toast('Externer Abschluss wurde erfasst und Readiness erneut geprüft.');
|
||||
await loadDeploymentStatus();
|
||||
} catch (error) { toast(error.message); }
|
||||
finally { button.disabled = false; }
|
||||
}));
|
||||
list.querySelectorAll('[data-deployment-resolve]').forEach(button => button.addEventListener('click', async () => {
|
||||
const confirmation = prompt('Der Auftrag bleibt als fehlgeschlagen dokumentiert und die Deployment-Sperre wird aufgehoben. Exakt CONFIRM eingeben:');
|
||||
if (confirmation !== 'CONFIRM') return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api('POST', `/api/admin/deployment/runs/${encodeURIComponent(button.dataset.deploymentResolve)}/confirm`, { confirmation, outcome: 'failed', message: 'Rollback-/Fehlerprüfung manuell abgeschlossen.' });
|
||||
toast('Prüfung abgeschlossen. Ein neuer Deployment-Auftrag ist wieder möglich.');
|
||||
await loadDeploymentStatus();
|
||||
} catch (error) { toast(error.message); }
|
||||
finally { button.disabled = false; }
|
||||
}));
|
||||
}
|
||||
|
||||
function renderDeploymentStatus() {
|
||||
const data = operationsState.deployment || {};
|
||||
const setValue = (id, value) => { const field = document.getElementById(id); if (field && document.activeElement !== field) field.value = value ?? ''; };
|
||||
@@ -8256,14 +8114,9 @@ function renderDeploymentStatus() {
|
||||
setValue('deployment-coolify-url', data.coolify_url || '');
|
||||
setValue('deployment-resource-uuid', data.resource_uuid || '');
|
||||
setValue('deployment-trigger-mode', data.trigger_mode || 'webhook');
|
||||
setValue('deployment-status-endpoint', data.deployment_status_endpoint || '/api/v1/deployments/{deployment_id}');
|
||||
setValue('deployment-timeout-seconds', data.deployment_timeout_seconds || 900);
|
||||
setValue('deployment-poll-seconds', data.health_poll_seconds || 15);
|
||||
setValue('deployment-github-repository', data.github_repository || 'Masterluke77/vendoo');
|
||||
setValue('deployment-github-branch', data.github_branch || 'main');
|
||||
setValue('deployment-update-channel', data.update_channel || 'stable');
|
||||
const target = document.getElementById('deployment-target-version');
|
||||
if (target && document.activeElement !== target && !target.value) target.value = data.active_run?.target_version || data.current_version || '';
|
||||
const backup = document.getElementById('deployment-backup-before'); if (backup) backup.checked = data.backup_before_deploy !== false;
|
||||
const uploads = document.getElementById('deployment-backup-uploads'); if (uploads) uploads.checked = data.backup_uploads === true;
|
||||
const webhook = document.getElementById('deployment-webhook-url'); if (webhook && document.activeElement !== webhook) { webhook.value = ''; webhook.placeholder = data.deploy_webhook?.configured ? `Gespeichert: ${data.deploy_webhook.masked || '••••••••'}` : 'Coolify Deploy-Webhook eintragen'; }
|
||||
@@ -8271,11 +8124,7 @@ function renderDeploymentStatus() {
|
||||
const status = document.getElementById('deployment-status');
|
||||
if (status) status.innerHTML = `<strong>${data.configured ? 'Coolify ist konfiguriert' : 'Coolify noch nicht vollständig konfiguriert'}</strong><span>Provider: ${escapeHtml(data.provider || 'none')} · Trigger: ${escapeHtml(data.trigger_mode || 'webhook')} · Docker-Socket: ${data.docker_socket_access ? 'vorhanden' : 'nicht freigegeben'}</span>`;
|
||||
const action = document.getElementById('deployment-action-result');
|
||||
const last = data.recent_runs?.[0] || data.production?.last_deployment;
|
||||
if (action && last) action.innerHTML = `<strong>${escapeHtml(deploymentStateLabel(last.state))}</strong><span>${operationFormatDate(last.created_at || last.requested_at)} · ${escapeHtml(last.current_version || data.current_version || '?')} → ${escapeHtml(last.target_version || '?')} · Branch ${escapeHtml(last.github_branch || 'main')}</span>`;
|
||||
renderProductionOverview(data);
|
||||
renderDeploymentRuns(data);
|
||||
window.VendooIcons?.render(document.getElementById('operations-updates-panel'));
|
||||
if (action && data.last_deployment) action.innerHTML = `<strong>${data.last_deployment.accepted ? 'Letzter Deployment-Auftrag angenommen' : 'Letzter Deployment-Auftrag fehlgeschlagen'}</strong><span>${operationFormatDate(data.last_deployment.requested_at)} · Branch ${escapeHtml(data.last_deployment.github_branch || 'main')} · HTTP ${escapeHtml(String(data.last_deployment.http_status || '—'))}</span>`;
|
||||
}
|
||||
|
||||
async function loadDeploymentStatus() {
|
||||
@@ -8291,9 +8140,6 @@ function deploymentSettingsPayload() {
|
||||
trigger_mode: document.getElementById('deployment-trigger-mode')?.value || 'webhook',
|
||||
deploy_method: 'GET',
|
||||
deploy_endpoint: '/api/v1/deploy?uuid={uuid}&force={force}',
|
||||
deployment_status_endpoint: document.getElementById('deployment-status-endpoint')?.value || '/api/v1/deployments/{deployment_id}',
|
||||
deployment_timeout_seconds: Number(document.getElementById('deployment-timeout-seconds')?.value || 900),
|
||||
health_poll_seconds: Number(document.getElementById('deployment-poll-seconds')?.value || 15),
|
||||
github_repository: document.getElementById('deployment-github-repository')?.value || 'Masterluke77/vendoo',
|
||||
github_branch: document.getElementById('deployment-github-branch')?.value || 'main',
|
||||
ghcr_image: 'ghcr.io/masterluke77/vendoo',
|
||||
@@ -8392,18 +8238,6 @@ function setupOperationsCenter() {
|
||||
finally { button.disabled = !operationsState.updateFile; button.textContent = 'Update prüfen und vorbereiten'; }
|
||||
});
|
||||
|
||||
document.getElementById('deployment-production-check')?.addEventListener('click', async event => {
|
||||
const button = event.currentTarget; const resultBox = document.getElementById('deployment-production-check-result');
|
||||
button.disabled = true; if (resultBox) resultBox.textContent = 'Produktionsgrenzen werden geprüft …';
|
||||
try {
|
||||
const result = await api('POST', '/api/admin/deployment/production-check');
|
||||
const details = (result.results || []).filter(item => item.status !== 'pass').map(item => `${item.title || item.name || item.id}: ${item.message || item.status}`).slice(0, 4).join(' · ');
|
||||
if (resultBox) resultBox.innerHTML = `<strong>${result.overall_status === 'passed' ? 'Produktionscheck bestanden' : result.overall_status === 'warning' ? 'Produktionscheck mit Hinweisen' : 'Produktionscheck fehlgeschlagen'}</strong><span>${Number(result.passed_count || 0)} bestanden · ${Number(result.warning_count || 0)} Hinweise · ${Number(result.failed_count || 0)} Fehler${details ? ` · ${escapeHtml(details)}` : ''}</span>`;
|
||||
await loadDeploymentStatus();
|
||||
} catch (error) { if (resultBox) resultBox.innerHTML = `<strong>Produktionscheck konnte nicht ausgeführt werden</strong><span>${escapeHtml(error.message)}</span>`; }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
|
||||
document.getElementById('deployment-save')?.addEventListener('click', async event => {
|
||||
const button = event.currentTarget; button.disabled = true;
|
||||
try { operationsState.deployment = await api('PUT', '/api/admin/deployment', deploymentSettingsPayload()); renderDeploymentStatus(); toast('Coolify- und GitHub-Konfiguration gespeichert.'); }
|
||||
@@ -8418,12 +8252,7 @@ function setupOperationsCenter() {
|
||||
});
|
||||
document.getElementById('deployment-update-check')?.addEventListener('click', async event => {
|
||||
const button = event.currentTarget; const resultBox = document.getElementById('deployment-action-result'); button.disabled = true;
|
||||
try {
|
||||
const result = await api('POST', '/api/admin/deployment/updates/check');
|
||||
const target = document.getElementById('deployment-target-version');
|
||||
if (target && result.latest_version) target.value = result.latest_version;
|
||||
if (resultBox) resultBox.innerHTML = `<strong>${result.update_available ? `Version ${escapeHtml(result.latest_version)} verfügbar` : result.configured ? 'Vendoo ist aktuell' : 'Keine Manifest-URL konfiguriert'}</strong><span>Installiert: ${escapeHtml(result.current_version || VENDOO_UI_BUILD)}${result.latest_version ? ` · Freigegeben: ${escapeHtml(result.latest_version)}` : ''}${result.message ? ` · ${escapeHtml(result.message)}` : ''}</span>`;
|
||||
}
|
||||
try { const result = await api('POST', '/api/admin/deployment/updates/check'); if (resultBox) resultBox.innerHTML = `<strong>${result.update_available ? `Version ${escapeHtml(result.latest_version)} verfügbar` : result.configured ? 'Vendoo ist aktuell' : 'Keine Manifest-URL konfiguriert'}</strong><span>Installiert: ${escapeHtml(result.current_version || VENDOO_UI_BUILD)}${result.message ? ` · ${escapeHtml(result.message)}` : ''}</span>`; }
|
||||
catch (error) { if (resultBox) resultBox.innerHTML = `<strong>Updateprüfung fehlgeschlagen</strong><span>${escapeHtml(error.message)}</span>`; }
|
||||
finally { button.disabled = false; }
|
||||
});
|
||||
@@ -8432,11 +8261,9 @@ function setupOperationsCenter() {
|
||||
if (confirmation !== 'DEPLOY') return;
|
||||
const button = event.currentTarget; const resultBox = document.getElementById('deployment-action-result'); button.disabled = true; button.textContent = 'Deployment wird angefordert …';
|
||||
try {
|
||||
const targetVersion = document.getElementById('deployment-target-version')?.value || operationsState.deployment?.current_version || VENDOO_UI_BUILD;
|
||||
const idempotencyKey = globalThis.crypto?.randomUUID?.() || `vendoo-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
const result = await api('POST', '/api/admin/deployment/trigger', { confirmation, force: false, reason: 'update-center', target_version: targetVersion, idempotency_key: idempotencyKey });
|
||||
if (resultBox) resultBox.innerHTML = `<strong>${result.reused ? 'Vorhandener Deployment-Auftrag erkannt' : 'Coolify-Deployment angenommen'}</strong><span>${escapeHtml(result.message || deploymentStateLabel(result.state))}${result.backup_id ? ` · Backup ${escapeHtml(String(result.backup_id).slice(0, 12))}` : ''}</span>`;
|
||||
toast(result.reused ? 'Der gleiche Deployment-Auftrag war bereits vorhanden.' : 'Coolify-Deployment wurde angefordert.');
|
||||
const result = await api('POST', '/api/admin/deployment/trigger', { confirmation, force: false, reason: 'update-center' });
|
||||
if (resultBox) resultBox.innerHTML = `<strong>Coolify-Deployment angenommen</strong><span>${escapeHtml(result.message || '')}${result.backup_file ? ` · Backup: ${escapeHtml(result.backup_file)}` : ''}</span>`;
|
||||
toast('Coolify-Deployment wurde angefordert.');
|
||||
await loadDeploymentStatus();
|
||||
} catch (error) { if (resultBox) resultBox.innerHTML = `<strong>Deployment fehlgeschlagen</strong><span>${escapeHtml(error.message)}</span>`; toast(error.message); }
|
||||
finally { button.disabled = false; button.textContent = 'Coolify-Deployment starten'; }
|
||||
@@ -8464,7 +8291,7 @@ function setupAdmin() {
|
||||
document.getElementById('invite-user-btn')?.addEventListener('click', () => {
|
||||
document.getElementById('invite-email').value = '';
|
||||
document.getElementById('invite-name').value = '';
|
||||
document.getElementById('invite-role').value = 'viewer';
|
||||
document.getElementById('invite-role').value = 'editor';
|
||||
document.getElementById('invite-result-wrap').classList.add('hidden');
|
||||
document.getElementById('invite-result-password').value = '';
|
||||
const mlRadio = document.querySelector('input[name="invite-method"][value="magic_link"]');
|
||||
@@ -8615,48 +8442,13 @@ function exportAdminAudit() {
|
||||
window.location.href = `/api/admin/audit/export.csv?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function loadAdminInvitations() {
|
||||
const container = document.getElementById('invitations-list');
|
||||
if (!container) return;
|
||||
try {
|
||||
const result = await api('GET', '/api/admin/users/invitations');
|
||||
const invitations = result.invitations || [];
|
||||
if (!invitations.length) { container.innerHTML = '<div class="empty-state">Noch keine Einladungen vorhanden.</div>'; return; }
|
||||
const statusLabel = status => ({ pending: 'Offen', expired: 'Abgelaufen', revoked_or_used: 'Verwendet oder widerrufen', revoked: 'Widerrufen' })[status] || status;
|
||||
container.innerHTML = invitations.map(invitation => {
|
||||
const actionable = ['pending', 'expired'].includes(invitation.status);
|
||||
return `<article class="invitation-admin-row ${invitation.status === 'pending' ? 'is-pending' : ''}">
|
||||
<div><strong>${escapeHtml(invitation.email)}</strong><span>${escapeHtml(ROLE_LABELS[invitation.role] || invitation.role || 'Leser')} · ${escapeHtml(statusLabel(invitation.status))}</span><small>Erstellt ${operationFormatDate(invitation.created_at)} · Ablauf ${operationFormatDate(invitation.expires_at)}</small></div>
|
||||
${actionable ? `<div class="invitation-admin-actions"><button type="button" class="small-btn" data-invitation-resend="${invitation.id}" title="Einladung erneut senden"><span data-vd-icon="mail"></span></button>${invitation.status === 'pending' ? `<button type="button" class="small-btn danger-btn" data-invitation-revoke="${invitation.id}" title="Einladung widerrufen"><span data-vd-icon="ban"></span></button>` : ''}</div>` : ''}
|
||||
</article>`;
|
||||
}).join('');
|
||||
container.querySelectorAll('[data-invitation-resend]').forEach(button => button.addEventListener('click', async () => {
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await api('POST', `/api/admin/users/invitations/${encodeURIComponent(button.dataset.invitationResend)}/resend`);
|
||||
toast(result.consoleFallback ? 'Neue Einladung erstellt; Link wurde in der Server-Konsole ausgegeben.' : 'Einladung erneut gesendet.');
|
||||
await loadAdminInvitations();
|
||||
} catch (error) { toast(error.message); }
|
||||
finally { button.disabled = false; }
|
||||
}));
|
||||
container.querySelectorAll('[data-invitation-revoke]').forEach(button => button.addEventListener('click', async () => {
|
||||
if (!confirm('Diese Einladung sofort widerrufen? Der Link kann danach nicht mehr verwendet werden.')) return;
|
||||
button.disabled = true;
|
||||
try { await api('DELETE', `/api/admin/users/invitations/${encodeURIComponent(button.dataset.invitationRevoke)}`); toast('Einladung widerrufen.'); await loadAdminInvitations(); }
|
||||
catch (error) { toast(error.message); }
|
||||
finally { button.disabled = false; }
|
||||
}));
|
||||
globalThis.VendooIcons?.render?.(container);
|
||||
} catch (error) { container.innerHTML = `<div class="empty-state">${escapeHtml(error.message)}</div>`; }
|
||||
}
|
||||
|
||||
async function loadAdminUsers() {
|
||||
try {
|
||||
const users = await api('GET', '/api/admin/users');
|
||||
adminUsersCache = Array.isArray(users) ? users : [];
|
||||
const container = document.getElementById('users-list');
|
||||
if (!container) return;
|
||||
if (!adminUsersCache.length) { container.innerHTML = '<div class="empty-state">Keine User vorhanden</div>'; await loadAdminInvitations(); return; }
|
||||
if (!adminUsersCache.length) { container.innerHTML = '<div class="empty-state">Keine User vorhanden</div>'; return; }
|
||||
|
||||
container.innerHTML = adminUsersCache.map(u => {
|
||||
const initials = getInitials(u.name, u.email);
|
||||
@@ -8688,7 +8480,6 @@ async function loadAdminUsers() {
|
||||
if (user) confirmDeleteUser(user.id, user.email);
|
||||
}));
|
||||
globalThis.VendooIcons?.render?.(container);
|
||||
await loadAdminInvitations();
|
||||
} catch (err) { toast('Fehler: ' + err.message); }
|
||||
}
|
||||
|
||||
|
||||
+54
-85
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="vendoo-build" content="1.43.0">
|
||||
<meta name="vendoo-build" content="1.41.2">
|
||||
<title>Vendoo</title>
|
||||
<link rel="icon" href="/brand/favicons/favicon.ico" sizes="any">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/brand/favicons/favicon-16x16.png">
|
||||
@@ -13,9 +13,9 @@
|
||||
<link rel="manifest" href="/brand/favicons/site.webmanifest">
|
||||
<meta name="msapplication-config" content="/brand/favicons/browserconfig.xml">
|
||||
<meta name="theme-color" content="#FAF6F2">
|
||||
<link rel="stylesheet" href="style.css?v=1.43.0">
|
||||
<link rel="stylesheet" href="modules/theme-manager/theme-manager.css?v=1.43.0">
|
||||
<link rel="stylesheet" href="modules/module-manager/module-manager.css?v=1.43.0">
|
||||
<link rel="stylesheet" href="style.css?v=1.41.2">
|
||||
<link rel="stylesheet" href="modules/theme-manager/theme-manager.css?v=1.41.2">
|
||||
<link rel="stylesheet" href="modules/module-manager/module-manager.css?v=1.41.2">
|
||||
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.snow.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
@@ -36,55 +36,55 @@
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
||||
<span>Dashboard</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="generator" data-module="vendoo.ai">
|
||||
<button class="nav-item" data-tab="generator">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/><rect x="3" y="3" width="18" height="18" rx="3"/></svg>
|
||||
<span>Neuer Artikel</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="today" data-module="vendoo.listings">
|
||||
<button class="nav-item" data-tab="today">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10.5V20h13v-9.5"/><path d="M9.5 20v-6h5v6"/></svg>
|
||||
<span>Heute</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="history" data-module="vendoo.listings">
|
||||
<button class="nav-item" data-tab="history">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 7h8M8 11h8M8 15h5"/></svg>
|
||||
<span>Artikel</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="quality-center" data-module="vendoo.quality">
|
||||
<button class="nav-item" data-tab="quality-center">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 20 7v5c0 5-3.4 8.3-8 10-4.6-1.7-8-5-8-10V7Z"/><path d="m8.5 12 2.2 2.2 4.8-5"/></svg>
|
||||
<span>Qualitätscenter</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="publish" data-module="vendoo.publishing">
|
||||
<button class="nav-item" data-tab="publish">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>
|
||||
<span>Publish</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="inventory" data-module="vendoo.inventory">
|
||||
<button class="nav-item" data-tab="inventory">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m4 7 8-4 8 4-8 4Z"/><path d="m4 7 8 4 8-4v10l-8 4-8-4Z"/><path d="M12 11v10"/></svg>
|
||||
<span>Lager</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="templates" data-module="vendoo.listings">
|
||||
<button class="nav-item" data-tab="templates">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 8h8M8 12h8M8 16h5"/></svg>
|
||||
<span>Vorlagen</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="flux-studio" data-module="vendoo.flux-studio">
|
||||
<button class="nav-item" data-tab="flux-studio">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 9.8 8.2 4 10l5 3.2L8 19l4-3 4 3-1-5.8 5-3.2-5.8-1.8Z"/><path d="M18 3v4M16 5h4"/></svg>
|
||||
<span>FLUX Studio</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="media-library" data-module="vendoo.media">
|
||||
<button class="nav-item" data-tab="media-library">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9" r="1.5"/><path d="m4 17 5-5 4 4 2-2 5 5"/></svg>
|
||||
<span>Bilder</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="image-factory" data-module="vendoo.ai">
|
||||
<button class="nav-item" data-tab="image-factory">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4z"/><path d="M14 17h6M17 14v6"/></svg>
|
||||
<span>Bildproduktion</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="extensions" data-module="vendoo.module-manager">
|
||||
<button class="nav-item" data-tab="extensions">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.5 3v4M15.5 3v4M6 7h12v4a6 6 0 0 1-12 0Z"/><path d="M12 17v4M9 21h6"/></svg>
|
||||
<span>Extensions</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="settings" data-module="vendoo.settings">
|
||||
<button class="nav-item" data-tab="settings">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.05.05-2.83 2.83-.05-.05a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.04 1.57V21h-4v-.07a1.7 1.7 0 0 0-1.04-1.57 1.7 1.7 0 0 0-1.87.34l-.05.05-2.83-2.83.05-.05A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.57-1.04H3v-4h.07A1.7 1.7 0 0 0 4.64 8.9a1.7 1.7 0 0 0-.34-1.87l-.05-.05 2.83-2.83.05.05A1.7 1.7 0 0 0 9 4.54 1.7 1.7 0 0 0 10.04 3H10V3h4v.07A1.7 1.7 0 0 0 15.04 4.6a1.7 1.7 0 0 0 1.87-.34l.05-.05 2.83 2.83-.05.05A1.7 1.7 0 0 0 19.4 9c.24.63.85 1.04 1.53 1.04H21v4h-.07c-.68 0-1.29.41-1.53 1.04Z"/></svg>
|
||||
<span>Einstellungen</span>
|
||||
</button>
|
||||
<button class="nav-item" data-tab="admin" data-module="vendoo.users" data-admin-tab="users" id="nav-admin-main">
|
||||
<button class="nav-item" data-tab="admin" data-admin-tab="users" id="nav-admin-main">
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/><path d="M9 12h6M12 9v6"/></svg>
|
||||
<span>Admin</span>
|
||||
</button>
|
||||
@@ -92,11 +92,11 @@
|
||||
|
||||
<div class="nav-group nav-group-tools" id="nav-system-group">
|
||||
<div class="nav-divider"></div>
|
||||
<button class="nav-item nav-item-quiet" data-tab="fees" data-module="vendoo.publishing">
|
||||
<button class="nav-item nav-item-quiet" data-tab="fees">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 7h8M8 11h3M8 15h3M15 11v5M13 13.5h4"/></svg>
|
||||
<span>Gebühren</span>
|
||||
</button>
|
||||
<button class="nav-item nav-item-quiet" data-tab="trash" data-module="vendoo.listings">
|
||||
<button class="nav-item nav-item-quiet" data-tab="trash">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M9 7V4h6v3M7 7l1 14h8l1-14"/></svg>
|
||||
<span>Papierkorb</span>
|
||||
<span id="trash-badge" class="nav-badge hidden">0</span>
|
||||
@@ -145,7 +145,7 @@
|
||||
</button>
|
||||
|
||||
<div class="topbar-actions">
|
||||
<button class="topbar-btn primary" id="topbar-new-btn" data-module="vendoo.ai">
|
||||
<button class="topbar-btn primary" id="topbar-new-btn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>
|
||||
<span>Neuer Artikel</span>
|
||||
</button>
|
||||
@@ -314,39 +314,18 @@
|
||||
<p>Fotos erfassen, AI gezielt steuern und das Ergebnis ohne Kontextwechsel fertigstellen.</p>
|
||||
</div>
|
||||
<div class="generator-heading-actions">
|
||||
<div class="generator-stepper" aria-label="Arbeitsfortschritt">
|
||||
<span class="generator-step is-active" id="generator-step-photos"><b>1</b> Fotos</span>
|
||||
<span class="generator-step" id="generator-step-ai"><b>2</b> AI</span>
|
||||
<span class="generator-step" id="generator-step-result"><b>3</b> Prüfen</span>
|
||||
</div>
|
||||
<button id="reset-generator-btn" class="vd-button secondary" type="button">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>
|
||||
Formular leeren
|
||||
Zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="generator-commandbar" id="generator-commandbar">
|
||||
<div class="generator-command-progress">
|
||||
<div class="generator-stepper" aria-label="Arbeitsfortschritt">
|
||||
<span class="generator-step is-active" id="generator-step-photos"><b>1</b> Fotos</span>
|
||||
<span class="generator-step" id="generator-step-ai"><b>2</b> Generieren</span>
|
||||
<span class="generator-step" id="generator-step-result"><b>3</b> Prüfen</span>
|
||||
</div>
|
||||
<div class="generator-readiness">
|
||||
<span id="generator-readiness-dot"></span>
|
||||
<div>
|
||||
<strong id="generator-readiness-title">Fotos fehlen</strong>
|
||||
<small id="generator-readiness-copy">Lade mindestens ein Produktfoto hoch.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="generator-command-actions">
|
||||
<button id="generate-btn" class="vd-button primary generator-primary-action" disabled>
|
||||
<span aria-hidden="true">✦</span>
|
||||
Artikel generieren
|
||||
</button>
|
||||
<button id="save-btn" class="vd-button generator-save-action" disabled>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="generator-studio-layout">
|
||||
<section class="generator-panel media-workbench" aria-labelledby="generator-media-title">
|
||||
<div class="generator-panel-header">
|
||||
@@ -398,13 +377,13 @@
|
||||
</div>
|
||||
|
||||
|
||||
<section id="generator-ai-model-card" class="ai-model-card generator-ai-model-card hidden" data-module-panel="vendoo.product-image-tools">
|
||||
<section id="generator-ai-model-card" class="ai-model-card generator-ai-model-card">
|
||||
<div class="ai-model-card-head">
|
||||
<div>
|
||||
<span class="generator-panel-index">AI</span>
|
||||
<div>
|
||||
<h4>Produktbild Studio</h4>
|
||||
<p>Optionales Modul für kontrollierte Ghost-Mannequin- und Flatlay-Varianten. Aktivierung und Deaktivierung erfolgen im Modulmanager.</p>
|
||||
<h4>Produktbild-Werkzeuge</h4>
|
||||
<p>Erzeuge Ghost-Mannequin- oder Flatlay-Varianten aus dem vorhandenen Produktfoto. Freie Prompt-Bilder findest du im separaten FLUX Studio.</p>
|
||||
</div>
|
||||
</div>
|
||||
<span id="generator-ai-model-badge" class="generator-beta">Safe</span>
|
||||
@@ -554,7 +533,19 @@
|
||||
<p><strong>AI analysiert deine Fotos</strong><br><span id="generator-loading-copy">Produkt, Zustand und Marktplatzdaten werden vorbereitet.</span></p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="generator-action-zone">
|
||||
<div class="generator-readiness">
|
||||
<span id="generator-readiness-dot"></span>
|
||||
<div>
|
||||
<strong id="generator-readiness-title">Fotos fehlen</strong>
|
||||
<small id="generator-readiness-copy">Lade mindestens ein Produktfoto hoch.</small>
|
||||
</div>
|
||||
</div>
|
||||
<button id="generate-btn" class="primary-btn generator-primary-action" disabled>
|
||||
<span>✦</span>
|
||||
Artikel generieren
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="generator-panel generator-preview-panel" aria-labelledby="generator-preview-title">
|
||||
@@ -658,7 +649,7 @@
|
||||
|
||||
<div class="result-actions generator-result-actions">
|
||||
<button id="copy-all-btn" class="secondary-btn">Alles kopieren</button>
|
||||
<span>Speichern ist dauerhaft oben erreichbar.</span>
|
||||
<button id="save-btn" class="primary-btn">Speichern & Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -1131,10 +1122,6 @@
|
||||
<!-- Users Tab -->
|
||||
<div class="admin-panel active" id="admin-users-panel">
|
||||
<div id="users-list" class="users-list"></div>
|
||||
<section class="invitation-admin-section">
|
||||
<div class="security-panel-heading"><div><p class="eyebrow">Testzugänge</p><h3>Einladungen</h3><p>Offene, abgelaufene und widerrufene Einladungen. Standardrolle ist Leser; höhere Rechte müssen bewusst gewählt werden.</p></div></div>
|
||||
<div id="invitations-list" class="invitation-admin-list"><div class="empty-state">Einladungen werden geladen …</div></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Roles Tab -->
|
||||
@@ -1269,45 +1256,27 @@
|
||||
<div class="operation-card-actions"><button type="button" class="secondary-btn" id="operations-update-url-save">URL speichern</button><button type="button" class="secondary-btn" id="operations-update-check">Jetzt prüfen</button></div>
|
||||
<div id="operations-update-check-result" class="operation-result">Noch nicht geprüft.</div>
|
||||
</article>
|
||||
<article class="operations-card operations-production-card">
|
||||
<header><span data-vd-icon="activity"></span><div><h4>Produktionsstatus</h4><p>Live-Überblick über Version, Readiness, Datenbank, Speicher und FLUX-Grenze.</p></div></header>
|
||||
<div id="deployment-production-overview" class="deployment-production-overview"><div class="empty-state">Produktionsstatus wird geladen …</div></div>
|
||||
<div class="operation-card-actions"><button type="button" class="secondary-btn" id="deployment-production-check">Produktionscheck ausführen</button></div>
|
||||
<div id="deployment-production-check-result" class="operation-result">Noch kein Produktionscheck ausgeführt.</div>
|
||||
</article>
|
||||
<article class="operations-card operations-deployment-card">
|
||||
<header><span data-vd-icon="cloud-cog"></span><div><h4>Coolify & GitHub Deployment</h4><p>Kontrolliertes Deployment ohne Docker-Socket und ohne selbständernden Produktivcontainer.</p></div></header>
|
||||
<header><span data-vd-icon="cloud-cog"></span><div><h4>Coolify & GitHub Deployment</h4><p>Neue GitHub-Versionen kontrolliert über Coolify ausrollen. Vendoo erhält keinen Docker-Socket.</p></div></header>
|
||||
<label class="operation-field"><span>Provider</span><select id="deployment-provider"><option value="none">Nicht konfiguriert</option><option value="coolify">Coolify</option></select></label>
|
||||
<label class="operation-field"><span>Coolify URL</span><input id="deployment-coolify-url" type="url" placeholder="https://coolify.example.de"></label>
|
||||
<label class="operation-field"><span>Resource UUID</span><input id="deployment-resource-uuid" type="text" placeholder="Coolify Application UUID"></label>
|
||||
<label class="operation-field"><span>Trigger</span><select id="deployment-trigger-mode"><option value="webhook">Deploy-Webhook</option><option value="api">Coolify API mit Statusabfrage</option></select></label>
|
||||
<label class="operation-field"><span>Deployment-Status-Endpunkt</span><input id="deployment-status-endpoint" type="text" value="/api/v1/deployments/{deployment_id}"></label>
|
||||
<div class="operation-inline-fields">
|
||||
<label class="operation-field"><span>Zeitlimit (Sek.)</span><input id="deployment-timeout-seconds" type="number" min="60" max="7200" value="900"></label>
|
||||
<label class="operation-field"><span>Prüfintervall (Sek.)</span><input id="deployment-poll-seconds" type="number" min="5" max="300" value="15"></label>
|
||||
</div>
|
||||
<label class="operation-field"><span>Trigger</span><select id="deployment-trigger-mode"><option value="webhook">Deploy-Webhook (empfohlen)</option><option value="api">Coolify API</option></select></label>
|
||||
<label class="operation-field"><span>Deploy-Webhook</span><input id="deployment-webhook-url" type="password" autocomplete="new-password" placeholder="Nur neu eingeben, wenn er geändert werden soll"></label>
|
||||
<label class="operation-field"><span>API-Token</span><input id="deployment-api-token" type="password" autocomplete="new-password" placeholder="Nur für API-Modus"></label>
|
||||
<div class="operation-card-actions"><button type="button" class="secondary-btn" id="deployment-save">Konfiguration speichern</button><button type="button" class="secondary-btn" id="deployment-test">Verbindung prüfen</button></div>
|
||||
<div id="deployment-status" class="operation-result">Deployment-Konfiguration wird geladen …</div>
|
||||
</article>
|
||||
<article class="operations-card operations-deploy-action-card">
|
||||
<header><span data-vd-icon="rocket"></span><div><h4>GitHub-Stand kontrolliert ausrollen</h4><p>Vorab-Backup, Doppelstartschutz, Statusverfolgung, Readiness und Zielversion sind Bestandteil eines Auftrags.</p></div></header>
|
||||
<header><span data-vd-icon="rocket"></span><div><h4>GitHub-Stand ausrollen</h4><p>Coolify holt den konfigurierten Branch und ersetzt den laufenden Container erst nach erfolgreichem Build und Healthcheck.</p></div></header>
|
||||
<label class="operation-field"><span>GitHub Repository</span><input id="deployment-github-repository" type="text" value="Masterluke77/vendoo"></label>
|
||||
<label class="operation-field"><span>Produktionsbranch</span><input id="deployment-github-branch" type="text" value="main"></label>
|
||||
<div class="operation-inline-fields">
|
||||
<label class="operation-field"><span>Update-Kanal</span><select id="deployment-update-channel"><option value="stable">Stable</option><option value="beta">Beta</option></select></label>
|
||||
<label class="operation-field"><span>Zielversion</span><input id="deployment-target-version" type="text" placeholder="z. B. 1.43.0"></label>
|
||||
</div>
|
||||
<label class="operation-field"><span>Update-Kanal</span><select id="deployment-update-channel"><option value="stable">Stable</option><option value="beta">Beta</option></select></label>
|
||||
<label class="operation-check"><input id="deployment-backup-before" type="checkbox" checked><span>Vor dem Deployment ein verifiziertes Backup mit Konfiguration erstellen</span></label>
|
||||
<label class="operation-check"><input id="deployment-backup-uploads" type="checkbox"><span>Uploads zusätzlich in das Vorab-Backup aufnehmen</span></label>
|
||||
<div class="operation-card-actions"><button type="button" class="secondary-btn" id="deployment-update-check">Version prüfen</button><button type="button" class="primary-btn" id="deployment-trigger">Coolify-Deployment starten</button></div>
|
||||
<div id="deployment-action-result" class="operation-result">Noch kein Deployment aus dem Webinterface angefordert.</div>
|
||||
</article>
|
||||
<article class="operations-card operations-deployment-history-card">
|
||||
<header><span data-vd-icon="history"></span><div><h4>Deployment-Verlauf</h4><p>Persistente Aufträge und Ereignisse bleiben auch nach einem Neustart nachvollziehbar.</p></div></header>
|
||||
<div id="deployment-run-list" class="deployment-run-list"><div class="empty-state">Keine Deployment-Aufträge vorhanden.</div></div>
|
||||
</article>
|
||||
|
||||
<article class="operations-card operations-update-upload">
|
||||
<header><span data-vd-icon="package-open"></span><div><h4>Update-Paket vorbereiten</h4><p>Prüft Version, Pflichtdateien und verbotene Laufzeitdaten. Vorher wird automatisch gesichert.</p></div></header>
|
||||
@@ -1409,9 +1378,9 @@ PUBLIC_BASE_URL=https://vendoo.example.de</pre>
|
||||
<div class="form-group">
|
||||
<label>Rolle</label>
|
||||
<select id="invite-role" class="model-select">
|
||||
<option value="viewer" selected>Leser</option>
|
||||
<option value="viewer">Leser</option>
|
||||
<option value="publisher">Publisher</option>
|
||||
<option value="editor">Editor</option>
|
||||
<option value="editor" selected>Editor</option>
|
||||
<option value="manager">Manager</option>
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
@@ -2392,11 +2361,11 @@ PUBLIC_BASE_URL=https://vendoo.example.de</pre>
|
||||
<div id="toast" class="toast"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.3/dist/quill.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js"></script>
|
||||
<script src="design-system/theme-runtime.js?v=1.43.0"></script>
|
||||
<script src="vendoo-icons.js?v=1.43.0"></script>
|
||||
<script src="app.js?v=1.43.0"></script>
|
||||
<script type="module" src="modules/theme-manager/theme-manager.js?v=1.43.0"></script>
|
||||
<script type="module" src="modules/module-manager/module-manager.js?v=1.43.0"></script>
|
||||
<script src="design-system/theme-runtime.js?v=1.41.2"></script>
|
||||
<script src="vendoo-icons.js?v=1.41.2"></script>
|
||||
<script src="app.js?v=1.41.2"></script>
|
||||
<script type="module" src="modules/theme-manager/theme-manager.js?v=1.41.2"></script>
|
||||
<script type="module" src="modules/module-manager/module-manager.js?v=1.41.2"></script>
|
||||
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -106,6 +106,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/login.js?v=1.43.0" defer></script>
|
||||
<script src="/login.js?v=1.41.2" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -150,7 +150,6 @@ async function perform(action, id) {
|
||||
await request('DELETE', `/api/admin/modules/${encodeURIComponent(id)}`);
|
||||
}
|
||||
await load();
|
||||
window.dispatchEvent(new CustomEvent('vendoo:modules-changed', { detail: { action, id } }));
|
||||
globalThis.toast?.(action === 'enable' ? 'Modul aktiviert.' : action === 'disable' ? 'Modul deaktiviert.' : 'Modul gelöscht.');
|
||||
} catch (error) {
|
||||
globalThis.toast?.(error.message);
|
||||
@@ -169,7 +168,6 @@ async function install() {
|
||||
await request('POST', '/api/admin/modules/install', form, { multipart: true });
|
||||
input.value = '';
|
||||
await load();
|
||||
window.dispatchEvent(new CustomEvent('vendoo:modules-changed', { detail: { action: 'install' } }));
|
||||
globalThis.toast?.('Modulpaket geprüft und installiert.');
|
||||
} catch (error) {
|
||||
globalThis.toast?.(error.message);
|
||||
|
||||
+1
-153
@@ -7165,29 +7165,6 @@ button,input,select,textarea { max-width:100%; }
|
||||
.operation-card-actions { display: flex; gap: 7px; flex-wrap: wrap; }
|
||||
.operation-result { display: grid; gap: 3px; min-height: 52px; margin-top: 10px; padding: 10px; border-radius: 9px; background: var(--bg-page); color: var(--text-muted); font-size: 10px; }
|
||||
.operation-result strong { color: var(--text); font-size: 12px; }
|
||||
|
||||
.deployment-production-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
|
||||
.deployment-production-item { display: grid; grid-template-columns: 28px minmax(0, 1fr); gap: 8px; align-items: start; padding: 9px; border: 1px solid var(--border); border-radius: 9px; background: var(--bg-page); min-width: 0; }
|
||||
.deployment-production-item > span { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 8px; color: var(--warning, #d97706); background: color-mix(in srgb, var(--warning, #d97706) 10%, transparent); }
|
||||
.deployment-production-item.is-ok > span { color: var(--success, #20875d); background: color-mix(in srgb, var(--success, #20875d) 10%, transparent); }
|
||||
.deployment-production-item small, .deployment-production-item strong, .deployment-production-item p { display: block; min-width: 0; overflow-wrap: anywhere; }
|
||||
.deployment-production-item small { color: var(--text-muted); font-size: 8px; text-transform: uppercase; letter-spacing: .04em; }
|
||||
.deployment-production-item strong { margin-top: 2px; font-size: 11px; }
|
||||
.deployment-production-item p { margin-top: 2px; font-size: 9px; line-height: 1.35; }
|
||||
.deployment-run-list { display: grid; gap: 8px; max-height: 430px; overflow: auto; }
|
||||
.deployment-run { padding: 10px; border: 1px solid var(--border); border-left-width: 3px; border-radius: 10px; background: var(--bg-page); }
|
||||
.deployment-run.is-ok { border-left-color: var(--success, #20875d); }
|
||||
.deployment-run.is-failed { border-left-color: var(--danger, #c43f4b); }
|
||||
.deployment-run.is-running { border-left-color: var(--accent); }
|
||||
.deployment-run-head { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 5px; }
|
||||
.deployment-run-state { font-size: 9px; font-weight: 800; text-transform: uppercase; letter-spacing: .035em; color: var(--accent); }
|
||||
.deployment-run.is-ok .deployment-run-state { color: var(--success, #20875d); }
|
||||
.deployment-run.is-failed .deployment-run-state { color: var(--danger, #c43f4b); }
|
||||
.deployment-run time, .deployment-run small { color: var(--text-muted); font-size: 8px; }
|
||||
.deployment-run > strong { display: block; font-size: 12px; }
|
||||
.deployment-run > p { margin: 3px 0; font-size: 10px; line-height: 1.4; }
|
||||
.deployment-run-actions { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
|
||||
.secondary-btn.compact { min-height: 29px; padding: 0 9px; font-size: 9px; }
|
||||
.operations-instruction { display: flex; gap: 11px; padding: 13px; margin-top: 14px; border-radius: 12px; background: color-mix(in srgb, var(--accent) 7%, var(--bg-card)); }
|
||||
.operations-instruction > span { color: var(--accent); }
|
||||
.operations-instruction strong, .operations-instruction p { display: block; margin: 0; }
|
||||
@@ -7230,7 +7207,7 @@ button,input,select,textarea { max-width:100%; }
|
||||
.operations-pending .secondary-btn { width: 100%; }
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.operation-inline-fields, .operations-extension-packages, .deployment-production-overview { grid-template-columns: minmax(0, 1fr); }
|
||||
.operation-inline-fields, .operations-extension-packages { grid-template-columns: minmax(0, 1fr); }
|
||||
.operations-backup-row { grid-template-columns: 32px minmax(0, 1fr); }
|
||||
.operation-backup-actions { grid-column: 1 / -1; justify-content: flex-end; }
|
||||
.operations-extension-client { grid-template-columns: 10px minmax(0, 1fr); }
|
||||
@@ -7402,132 +7379,3 @@ body[data-user-role="publisher"] .danger-zone { display: none !important; }
|
||||
/* 1.37.0 – Security Dashboard Foundation */
|
||||
.security-secret-list{display:flex;flex-wrap:wrap;gap:var(--vd-space-2,8px);margin-top:var(--vd-space-3,12px)}
|
||||
.security-secret-list .status-badge{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
|
||||
/* Vendoo 1.42.0 — kontrollierte Testeinladungen */
|
||||
.invitation-admin-section { margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--border); }
|
||||
.invitation-admin-list { display: grid; gap: 8px; }
|
||||
.invitation-admin-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: center; padding: 11px 12px; border: 1px solid var(--border); border-left: 3px solid var(--text-muted); border-radius: 11px; background: var(--bg-card); }
|
||||
.invitation-admin-row.is-pending { border-left-color: var(--accent); }
|
||||
.invitation-admin-row strong, .invitation-admin-row span, .invitation-admin-row small { display: block; min-width: 0; overflow-wrap: anywhere; }
|
||||
.invitation-admin-row strong { font-size: 12px; }
|
||||
.invitation-admin-row span { margin-top: 2px; color: var(--text-muted); font-size: 10px; }
|
||||
.invitation-admin-row small { margin-top: 2px; color: var(--text-muted); font-size: 9px; }
|
||||
.invitation-admin-actions { display: flex; gap: 6px; }
|
||||
@media (max-width: 620px) {
|
||||
.invitation-admin-row { grid-template-columns: minmax(0, 1fr); }
|
||||
.invitation-admin-actions { justify-content: flex-end; }
|
||||
}
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
Vendoo 1.43.0 – Module-aware Listing Studio
|
||||
========================================================================== */
|
||||
.generator-studio-page {
|
||||
--generator-gap: clamp(14px, 1.4vw, 22px);
|
||||
padding-bottom: 36px;
|
||||
}
|
||||
.generator-heading {
|
||||
margin-bottom: 12px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
.generator-heading h2 { font-size: clamp(24px, 2.2vw, 34px); }
|
||||
.generator-heading > div:first-child > p:last-child { max-width: 720px; font-size: 13px; line-height: 1.55; }
|
||||
.generator-commandbar {
|
||||
position: sticky;
|
||||
top: calc(var(--vd-topbar-height) + 8px);
|
||||
z-index: 34;
|
||||
min-height: 72px;
|
||||
margin-bottom: var(--generator-gap);
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
border: 1px solid var(--vd-line);
|
||||
border-radius: var(--vd-radius-md, 14px);
|
||||
background: color-mix(in srgb, var(--vd-surface-raised) 92%, transparent);
|
||||
box-shadow: 0 12px 34px color-mix(in srgb, var(--vd-ink) 9%, transparent);
|
||||
backdrop-filter: blur(18px) saturate(1.15);
|
||||
}
|
||||
.generator-command-progress { min-width: 0; display: flex; align-items: center; gap: 18px; }
|
||||
.generator-command-actions { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
|
||||
.generator-command-actions .vd-button { min-height: 44px; padding-inline: 18px; font-weight: 760; }
|
||||
.generator-save-action { min-width: 148px; border-color: var(--vd-brand); color: var(--vd-brand); background: var(--vd-surface-raised); }
|
||||
.generator-save-action:not(:disabled):hover { background: var(--vd-brand-soft); }
|
||||
.generator-commandbar .generator-readiness { min-width: 230px; grid-template-columns: 11px minmax(0, 1fr); }
|
||||
.generator-commandbar .generator-readiness > span { width: 9px; height: 9px; box-shadow: 0 0 0 5px color-mix(in srgb, var(--vd-warning) 14%, transparent); }
|
||||
.generator-commandbar .generator-readiness.is-ready > span { box-shadow: 0 0 0 5px color-mix(in srgb, var(--vd-success) 14%, transparent); }
|
||||
.generator-commandbar .generator-readiness strong { font-size: 12px; }
|
||||
.generator-commandbar .generator-readiness small { margin-top: 3px; font-size: 10px; }
|
||||
.generator-studio-layout {
|
||||
grid-template-columns: minmax(320px, .88fr) minmax(420px, 1.12fr);
|
||||
gap: var(--generator-gap);
|
||||
}
|
||||
.generator-panel {
|
||||
border-radius: var(--vd-radius-md, 14px);
|
||||
box-shadow: 0 8px 26px color-mix(in srgb, var(--vd-ink) 5%, transparent);
|
||||
}
|
||||
.generator-panel-header { min-height: 76px; padding: 16px 18px; }
|
||||
.generator-panel-header h3 { font-size: 15px; }
|
||||
.generator-panel-header p { margin-top: 4px; font-size: 11px; line-height: 1.45; }
|
||||
.generator-panel-index { width: 34px; height: 34px; font-size: 10px; }
|
||||
.generator-count, .generator-beta, .generator-preview-status { min-height: 26px; padding: 5px 10px; font-size: 10px; }
|
||||
.generator-photo-stage { margin: 16px 16px 12px; border-radius: 12px; }
|
||||
.generator-dropzone { margin-inline: 16px; min-height: 108px; padding: 16px; border-radius: 12px; }
|
||||
.generator-dropzone p { font-size: 11px; }
|
||||
.generator-dropzone p strong { font-size: 13px; }
|
||||
.generator-dropzone small { font-size: 10px; }
|
||||
.generator-photo-strip, .generator-image-tools { margin-inline: 16px; }
|
||||
.generator-photo-quality { margin: 14px 16px 4px; padding: 14px; border-radius: 12px; }
|
||||
.generator-photo-quality-head strong { font-size: 12px; }
|
||||
.generator-photo-quality li { font-size: 10px; }
|
||||
.generator-config-stack { padding: 14px; gap: 11px; }
|
||||
.generator-config-card, .generator-notes-card { padding: 14px; border-radius: 12px; }
|
||||
.generator-config-copy label, .generator-notes-head label { font-size: 12px; }
|
||||
.generator-config-copy span, .generator-notes-head span { font-size: 10px; }
|
||||
.generator-choice-group .toggle-btn { min-height: 34px; padding-inline: 12px; font-size: 10px; }
|
||||
.generator-config-card select { min-height: 40px; font-size: 11px; }
|
||||
.generator-notes-card textarea { min-height: 118px; font-size: 11px; }
|
||||
.generator-tone-row span { font-size: 9px; }
|
||||
.generator-loading { margin: 0 14px 14px; }
|
||||
.generator-action-zone { display: none; }
|
||||
.generator-preview-panel {
|
||||
grid-column: 1 / -1;
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.generator-preview-empty { min-height: 300px; }
|
||||
.generator-result { padding: 18px; }
|
||||
.generator-result .result-input { min-height: 42px; font-size: 13px; }
|
||||
.generator-result .ql-container.ql-snow { min-height: 280px; font-size: 13px; }
|
||||
.generator-meta-section .meta-input { min-height: 38px; font-size: 11px; }
|
||||
.generator-result-actions {
|
||||
position: static;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-top: 14px;
|
||||
color: var(--vd-ink-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
.generator-result-actions button { min-width: 140px; }
|
||||
[data-module].hidden, [data-module-panel].hidden { display: none !important; }
|
||||
@media (max-width: 1100px) {
|
||||
.generator-commandbar { align-items: stretch; flex-direction: column; }
|
||||
.generator-command-progress, .generator-command-actions { width: 100%; justify-content: space-between; }
|
||||
.generator-studio-layout { grid-template-columns: 1fr; }
|
||||
.generator-preview-panel { grid-column: auto; }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.generator-commandbar { top: calc(var(--vd-topbar-height) + 4px); padding: 10px; }
|
||||
.generator-command-progress { align-items: stretch; flex-direction: column; gap: 10px; }
|
||||
.generator-commandbar .generator-readiness { min-width: 0; }
|
||||
.generator-command-actions { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
.generator-command-actions .vd-button { min-width: 0; padding-inline: 10px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.generator-command-actions { grid-template-columns: 1fr; }
|
||||
.generator-step { font-size: 9px; }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+210
-3
@@ -1,3 +1,210 @@
|
||||
# Kompatibilitaetsstarter fuer alte Verknuepfungen.
|
||||
& (Join-Path $PSScriptRoot 'update-vendoo.ps1')
|
||||
exit $LASTEXITCODE
|
||||
# Vendoo 1.41.2 – kontrollierter GitHub-Erstimport
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||
$RepoUrl = 'https://github.com/Masterluke77/vendoo.git'
|
||||
$PullRequestUrl = 'https://github.com/Masterluke77/vendoo/pull/18'
|
||||
$Branch = 'release/1.41.2-git-ignore-boundary-hotfix'
|
||||
$WorkRoot = Join-Path $env:TEMP ('vendoo-github-deploy-' + [Guid]::NewGuid().ToString('N'))
|
||||
$CloneDir = Join-Path $WorkRoot 'repo'
|
||||
|
||||
function Info($Text) { Write-Host "[INFO] $Text" -ForegroundColor Cyan }
|
||||
function Ok($Text) { Write-Host "[OK] $Text" -ForegroundColor Green }
|
||||
function Warn($Text) { Write-Host "[WARN] $Text" -ForegroundColor Yellow }
|
||||
function Fail($Text) { Write-Host "[FEHLER] $Text" -ForegroundColor Red }
|
||||
|
||||
function Test-Command([string]$Name) {
|
||||
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
function Ensure-Git {
|
||||
if (Test-Command 'git.exe') { return }
|
||||
Warn 'Git für Windows wurde nicht gefunden.'
|
||||
if (Test-Command 'winget.exe') {
|
||||
$answer = Read-Host 'Git jetzt automatisch über winget installieren? (ja/nein)'
|
||||
if ($answer -eq 'ja') {
|
||||
& winget.exe install --id Git.Git --exact --source winget --accept-package-agreements --accept-source-agreements
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Git-Installation über winget ist fehlgeschlagen.' }
|
||||
$env:Path = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User')
|
||||
}
|
||||
}
|
||||
if (-not (Test-Command 'git.exe')) { throw 'Git für Windows wird benötigt. Starte den Assistenten nach der Git-Installation erneut.' }
|
||||
}
|
||||
|
||||
function Invoke-Git([string[]]$Arguments, [string]$WorkingDirectory = $CloneDir) {
|
||||
Push-Location $WorkingDirectory
|
||||
try {
|
||||
& git.exe @Arguments
|
||||
if ($LASTEXITCODE -ne 0) { throw "git $($Arguments -join ' ') fehlgeschlagen (Exit-Code $LASTEXITCODE)." }
|
||||
} finally { Pop-Location }
|
||||
}
|
||||
|
||||
function Copy-ProjectClean {
|
||||
if (-not (Test-Command 'node.exe')) {
|
||||
throw 'Node.js ist für den sicheren GitHub-Import erforderlich. Bitte zuerst über setup.bat installieren oder aktualisieren.'
|
||||
}
|
||||
Info 'Bereite den bereinigten Git-Arbeitsordner mit dem plattformübergreifenden Staging-Builder vor ...'
|
||||
$builder = Join-Path $Root 'tools\prepare-git-staging.mjs'
|
||||
$result = Invoke-NodeProcess -ScriptPath $builder -Arguments @($Root, $CloneDir) -WorkingDirectory $Root -Prefix 'Staging'
|
||||
if ($result.ExitCode -ne 0) {
|
||||
$result.Lines | ForEach-Object { Write-Host $_ -ForegroundColor Red }
|
||||
throw 'Der sichere Git-Staging-Aufbau ist fehlgeschlagen.'
|
||||
}
|
||||
$result.Lines | ForEach-Object { Write-Host $_ }
|
||||
}
|
||||
|
||||
function Invoke-NodeProcess {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ScriptPath,
|
||||
[string[]]$Arguments = @(),
|
||||
[string]$WorkingDirectory = $CloneDir,
|
||||
[string]$Prefix = 'Node'
|
||||
)
|
||||
$id = [Guid]::NewGuid().ToString('N')
|
||||
$stdoutPath = Join-Path $WorkRoot ("$Prefix-$id-stdout.txt")
|
||||
$stderrPath = Join-Path $WorkRoot ("$Prefix-$id-stderr.txt")
|
||||
$nodePath = (Get-Command node.exe -ErrorAction Stop).Source
|
||||
$allArguments = @($ScriptPath) + $Arguments
|
||||
$argumentLine = ($allArguments | ForEach-Object { '"' + ([string]$_).Replace('"','\"') + '"' }) -join ' '
|
||||
$process = Start-Process -FilePath $nodePath `
|
||||
-ArgumentList $argumentLine `
|
||||
-WorkingDirectory $WorkingDirectory `
|
||||
-NoNewWindow `
|
||||
-Wait `
|
||||
-PassThru `
|
||||
-RedirectStandardOutput $stdoutPath `
|
||||
-RedirectStandardError $stderrPath
|
||||
|
||||
$lines = @()
|
||||
if (Test-Path -LiteralPath $stdoutPath) { $lines += @(Get-Content -LiteralPath $stdoutPath -Encoding UTF8 -ErrorAction SilentlyContinue) }
|
||||
if (Test-Path -LiteralPath $stderrPath) { $lines += @(Get-Content -LiteralPath $stderrPath -Encoding UTF8 -ErrorAction SilentlyContinue) }
|
||||
return [pscustomobject]@{ ExitCode = [int]$process.ExitCode; Lines = $lines }
|
||||
}
|
||||
|
||||
function Run-SafetyGate {
|
||||
$ReportDir = Join-Path $Root 'logs'
|
||||
$ReportPath = Join-Path $ReportDir 'github-deploy-safety-report.txt'
|
||||
if (-not (Test-Command 'node.exe')) {
|
||||
throw 'Node.js ist für den sicheren GitHub-Import erforderlich. Bitte zuerst über setup.bat installieren oder aktualisieren.'
|
||||
}
|
||||
|
||||
Info 'Führe das Vendoo Git-Sicherheitsgate über den vollständigen Staging-Arbeitsbaum aus ...'
|
||||
$result = Invoke-NodeProcess -ScriptPath 'tools/verify-git-safety.mjs' -WorkingDirectory $CloneDir -Prefix 'SafetyGate'
|
||||
foreach ($line in $result.Lines) {
|
||||
if ($line -match '^Git-Sicherheitsgate fehlgeschlagen|^- ') { Write-Host $line -ForegroundColor Red }
|
||||
elseif ($line -match '^\[WARN\]') { Write-Host $line -ForegroundColor Yellow }
|
||||
else { Write-Host $line }
|
||||
}
|
||||
|
||||
if ($result.ExitCode -ne 0) {
|
||||
New-Item -ItemType Directory -Path $ReportDir -Force | Out-Null
|
||||
@(
|
||||
'Vendoo Git-Sicherheitsgate – vollständiger Fehlerbericht',
|
||||
('Zeitpunkt: ' + (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')),
|
||||
('Projekt: ' + $Root),
|
||||
('Staging: ' + $CloneDir),
|
||||
'',
|
||||
$result.Lines
|
||||
) | Set-Content -LiteralPath $ReportPath -Encoding UTF8
|
||||
Fail "Das Gate hat $($result.Lines.Count) Diagnosezeilen erzeugt. Bericht: $ReportPath"
|
||||
try { Start-Process notepad.exe -ArgumentList ('"' + $ReportPath + '"') | Out-Null } catch {}
|
||||
throw 'Git-Sicherheitsgate ist fehlgeschlagen. Die konkreten Treffer wurden oben angezeigt und im Bericht gespeichert.'
|
||||
}
|
||||
|
||||
Remove-Item -LiteralPath $ReportPath -Force -ErrorAction SilentlyContinue
|
||||
$forbidden = Get-ChildItem -LiteralPath $CloneDir -Recurse -Force -File -ErrorAction SilentlyContinue | Where-Object {
|
||||
($_.Name -match '^\.env(?:\..+)?$' -and $_.Name -notin @('.env.example','.env.docker.example')) -or
|
||||
$_.Extension -in @('.db','.sqlite','.sqlite3','.log','.key','.pem','.pfx','.p12')
|
||||
}
|
||||
if ($forbidden) {
|
||||
throw ('Verbotene Dateien im Git-Staging: ' + (($forbidden | Select-Object -First 20 -ExpandProperty FullName) -join ', '))
|
||||
}
|
||||
Ok 'Git-Sicherheitsgate erfolgreich: keine lokalen Secrets oder Laufzeitdaten im Import.'
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Host ''
|
||||
Write-Host 'Vendoo 1.41.2 – GitHub Deploy Assistent' -ForegroundColor Green
|
||||
Write-Host 'Repository: Masterluke77/vendoo (privat)' -ForegroundColor DarkGray
|
||||
Write-Host ''
|
||||
|
||||
Ensure-Git
|
||||
New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null
|
||||
|
||||
Info 'Klone die vorhandene GitHub-Baseline ...'
|
||||
& git.exe clone $RepoUrl $CloneDir
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Repository konnte nicht geklont werden. Die Git-Anmeldung öffnet sich normalerweise automatisch im Browser.' }
|
||||
|
||||
Push-Location $CloneDir
|
||||
try {
|
||||
& git.exe ls-remote --exit-code --heads origin $Branch 1>$null 2>$null
|
||||
$branchExists = ($LASTEXITCODE -eq 0)
|
||||
} finally { Pop-Location }
|
||||
if ($branchExists) {
|
||||
Invoke-Git @('fetch','origin',$Branch)
|
||||
Invoke-Git @('switch','--track','-c',$Branch,("origin/" + $Branch))
|
||||
} else {
|
||||
Invoke-Git @('switch','-c',$Branch)
|
||||
}
|
||||
|
||||
Copy-ProjectClean
|
||||
Run-SafetyGate
|
||||
|
||||
Info 'Prüfe produktive Source-Dateien und Git-Ignore-Grenzen im Staging ...'
|
||||
$sourceGate = Invoke-NodeProcess -ScriptPath 'tools/verify-source-boundaries-1.41.2.mjs' -WorkingDirectory $CloneDir -Prefix 'SourceGate'
|
||||
$sourceGate.Lines | ForEach-Object { Write-Host $_ }
|
||||
if ($sourceGate.ExitCode -ne 0) { throw 'Produktive Source-Root-Prüfung ist fehlgeschlagen.' }
|
||||
$ignoreGate = Invoke-NodeProcess -ScriptPath 'tools/verify-git-ignore-boundaries-1.41.2.mjs' -WorkingDirectory $CloneDir -Prefix 'IgnoreGate'
|
||||
$ignoreGate.Lines | ForEach-Object { Write-Host $_ }
|
||||
if ($ignoreGate.ExitCode -ne 0) { throw 'Git-Ignore-Grenzprüfung ist fehlgeschlagen.' }
|
||||
|
||||
Invoke-Git @('config','user.name','Markus Müller')
|
||||
Invoke-Git @('config','user.email','163446896+Masterluke77@users.noreply.github.com')
|
||||
Invoke-Git @('add','--all')
|
||||
|
||||
$requiredTracked = @(
|
||||
'app/modules/listings/routes.mjs',
|
||||
'app/modules/inventory/index.mjs',
|
||||
'app/modules/inventory/repository.mjs',
|
||||
'app/modules/inventory/routes.mjs',
|
||||
'app/modules/sessions/index.mjs'
|
||||
)
|
||||
Push-Location $CloneDir
|
||||
try {
|
||||
$ignoredModules = @(& git.exe ls-files --others --ignored --exclude-standard -- app/modules)
|
||||
if ($ignoredModules.Count -gt 0) { throw ('Produktive Moduldateien werden von .gitignore ausgeschlossen: ' + ($ignoredModules -join ', ')) }
|
||||
foreach ($rel in $requiredTracked) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $CloneDir $rel))) { throw "Pflichtdatei fehlt im Git-Staging: $rel" }
|
||||
& git.exe ls-files --error-unmatch -- $rel 1>$null 2>$null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Pflichtdatei wurde von Git nicht getrackt: $rel" }
|
||||
}
|
||||
} finally { Pop-Location }
|
||||
Ok 'Produktive Moduldateien sind vorhanden und werden von Git getrackt.'
|
||||
|
||||
Push-Location $CloneDir
|
||||
try {
|
||||
$status = & git.exe status --short
|
||||
if (-not $status) { Warn 'Es gibt keine Änderungen gegenüber GitHub.'; exit 0 }
|
||||
Write-Host ''
|
||||
Write-Host $status
|
||||
Write-Host ''
|
||||
} finally { Pop-Location }
|
||||
|
||||
$confirm = Read-Host 'Geprüften Vendoo-Stand als Release-Branch zu GitHub übertragen? Zum Fortfahren PUSH eingeben'
|
||||
if ($confirm -cne 'PUSH') { Warn 'Deployment abgebrochen. Auf GitHub wurde nichts verändert.'; exit 0 }
|
||||
|
||||
Invoke-Git @('commit','-m','fix: track native source modules with root-anchored runtime ignores')
|
||||
Invoke-Git @('push','--set-upstream','origin',$Branch)
|
||||
Ok "Branch $Branch wurde erfolgreich übertragen."
|
||||
|
||||
Info 'Öffne jetzt Draft Pull Request #18 im Browser ...'
|
||||
Start-Process $PullRequestUrl
|
||||
Write-Host ''
|
||||
Write-Host 'Nächster Schritt im Browser: Pull Request #18 und CI prüfen. Erst nach grüner CI nach main mergen.' -ForegroundColor Yellow
|
||||
} catch {
|
||||
Fail $_.Exception.Message
|
||||
exit 1
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $WorkRoot) {
|
||||
Remove-Item -LiteralPath $WorkRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param([switch]$SkipProductionDeploy)
|
||||
|
||||
Set-StrictMode -Version 2.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ui = Join-Path $PSScriptRoot 'updater\Vendoo.Updater.UI.ps1'
|
||||
try {
|
||||
& $ui @PSBoundParameters
|
||||
} catch {
|
||||
try {
|
||||
Add-Type -AssemblyName PresentationFramework
|
||||
[System.Windows.MessageBox]::Show(
|
||||
"Der Vendoo-Updater konnte nicht gestartet werden.`n`n$($_.Exception.Message)`n`nBitte die Datei logs\updater-bootstrap.log mitsenden.",
|
||||
'Vendoo Updater – Startfehler',
|
||||
[System.Windows.MessageBoxButton]::OK,
|
||||
[System.Windows.MessageBoxImage]::Error
|
||||
) | Out-Null
|
||||
} catch {}
|
||||
$packageRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
|
||||
$fallback = Join-Path $packageRoot 'logs\updater-bootstrap.log'
|
||||
New-Item -ItemType Directory -Path (Split-Path -Parent $fallback) -Force | Out-Null
|
||||
"$(Get-Date -Format o) $($_.Exception.ToString())" | Add-Content -LiteralPath $fallback -Encoding UTF8
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$StatusFile = '',
|
||||
[string]$EventFile = '',
|
||||
[string]$InputFile = '',
|
||||
[string]$CancelFile = '',
|
||||
[string]$SessionFile = '',
|
||||
[switch]$SkipProductionDeploy,
|
||||
[switch]$SelfTest,
|
||||
[string]$SelfTestFailAt = ''
|
||||
)
|
||||
$worker = Join-Path $PSScriptRoot 'updater\Vendoo.Updater.Worker.ps1'
|
||||
& $worker @PSBoundParameters
|
||||
exit $LASTEXITCODE
|
||||
@@ -1,140 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$CoreScript,
|
||||
[Parameter(Mandatory=$true)][string]$StatusFile,
|
||||
[Parameter(Mandatory=$true)][string]$EventFile,
|
||||
[Parameter(Mandatory=$true)][string]$InputFile,
|
||||
[Parameter(Mandatory=$true)][string]$CancelFile,
|
||||
[Parameter(Mandatory=$true)][string]$SessionFile,
|
||||
[Parameter(Mandatory=$true)][string]$LogFile,
|
||||
[switch]$SkipProductionDeploy,
|
||||
[switch]$SelfTest,
|
||||
[string]$SelfTestFailAt = ''
|
||||
)
|
||||
|
||||
Set-StrictMode -Version 2.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$TargetVersion = '1.43.0'
|
||||
$UpdaterVersion = '3.3.0'
|
||||
|
||||
function Get-DefaultSteps {
|
||||
return @(
|
||||
[ordered]@{id='package';title='Paket prüfen';state='pending'},
|
||||
[ordered]@{id='tools';title='System prüfen';state='pending'},
|
||||
[ordered]@{id='github';title='GitHub verbinden';state='pending'},
|
||||
[ordered]@{id='production';title='Produktion vorbereiten';state='pending'},
|
||||
[ordered]@{id='workspace';title='Arbeitskopie erstellen';state='pending'},
|
||||
[ordered]@{id='tests';title='Vollständig testen';state='pending'},
|
||||
[ordered]@{id='pullrequest';title='Pull Request erstellen';state='pending'},
|
||||
[ordered]@{id='merge';title='Sicher mergen';state='pending'},
|
||||
[ordered]@{id='deploy';title='Coolify deployen';state='pending'},
|
||||
[ordered]@{id='verify';title='Produktion bestätigen';state='pending'}
|
||||
)
|
||||
}
|
||||
|
||||
function Append-HostLog([string]$Level,[string]$Message) {
|
||||
try {
|
||||
$parent = Split-Path -Parent $LogFile
|
||||
if ($parent) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
|
||||
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') [$Level] $Message$([Environment]::NewLine)"
|
||||
[IO.File]::AppendAllText($LogFile,$line,(New-Object Text.UTF8Encoding($false)))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function Write-HostStatus([string]$State,[string]$Phase,[string]$Detail,[string]$ErrorText='',[string]$Remediation='') {
|
||||
try {
|
||||
$percent = 1
|
||||
if ($State -in @('failed','cancelled','success')) { $percent = 100 }
|
||||
$payload = [ordered]@{
|
||||
version=$TargetVersion
|
||||
updaterVersion=$UpdaterVersion
|
||||
percent=$percent
|
||||
phase=$Phase
|
||||
detail=$Detail
|
||||
state=$State
|
||||
error=$ErrorText
|
||||
remediation=$Remediation
|
||||
updatedAt=(Get-Date).ToString('o')
|
||||
logFile=$LogFile
|
||||
sessionFile=$SessionFile
|
||||
steps=(Get-DefaultSteps)
|
||||
request=$null
|
||||
} | ConvertTo-Json -Depth 8
|
||||
$parent = Split-Path -Parent $StatusFile
|
||||
if ($parent) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
|
||||
$tmp = "$StatusFile.$PID.host.tmp"
|
||||
[IO.File]::WriteAllText($tmp,$payload,(New-Object Text.UTF8Encoding($false)))
|
||||
Move-Item -LiteralPath $tmp -Destination $StatusFile -Force
|
||||
} catch {
|
||||
Append-HostLog 'HOST-ERROR' "Status konnte nicht geschrieben werden: $($_.Exception.ToString())"
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
foreach($path in @($StatusFile,$EventFile,$InputFile,$CancelFile,$SessionFile,$LogFile)) {
|
||||
$parent = Split-Path -Parent $path
|
||||
if ($parent) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $CoreScript -PathType Leaf)) {
|
||||
throw "Worker-Core fehlt: $CoreScript"
|
||||
}
|
||||
|
||||
Append-HostLog 'HOST' "Worker-Host $UpdaterVersion gestartet. Core: $CoreScript"
|
||||
Write-HostStatus 'running' 'PowerShell-Parserprüfung' 'Der Worker-Kern wird vor der Ausführung mit dem echten PowerShell-Parser geprüft.'
|
||||
|
||||
$parserTokens = $null
|
||||
$parserErrors = $null
|
||||
[void][System.Management.Automation.Language.Parser]::ParseFile($CoreScript,[ref]$parserTokens,[ref]$parserErrors)
|
||||
if ($parserErrors -and $parserErrors.Count -gt 0) {
|
||||
$details = @($parserErrors | ForEach-Object {
|
||||
"{0}:{1}:{2} {3}" -f $_.Extent.File,$_.Extent.StartLineNumber,$_.Extent.StartColumnNumber,$_.Message
|
||||
}) -join [Environment]::NewLine
|
||||
throw "PowerShell-Parserprüfung des Worker-Kerns fehlgeschlagen.$([Environment]::NewLine)$details"
|
||||
}
|
||||
Append-HostLog 'HOST' 'PowerShell-Parserprüfung des Worker-Kerns erfolgreich.'
|
||||
Write-HostStatus 'running' 'Worker wird gestartet' 'Der geprüfte Worker-Kern wird geladen.'
|
||||
|
||||
$invokeParams = @{
|
||||
StatusFile=$StatusFile
|
||||
EventFile=$EventFile
|
||||
InputFile=$InputFile
|
||||
CancelFile=$CancelFile
|
||||
SessionFile=$SessionFile
|
||||
LogFile=$LogFile
|
||||
}
|
||||
if ($SkipProductionDeploy) { $invokeParams.SkipProductionDeploy = $true }
|
||||
if ($SelfTest) { $invokeParams.SelfTest = $true }
|
||||
if ($SelfTestFailAt) { $invokeParams.SelfTestFailAt = $SelfTestFailAt }
|
||||
|
||||
& $CoreScript @invokeParams
|
||||
|
||||
$terminal = $false
|
||||
$stateName = ''
|
||||
if (Test-Path -LiteralPath $StatusFile) {
|
||||
try {
|
||||
$final = Get-Content -LiteralPath $StatusFile -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$stateName = [string]$final.state
|
||||
$terminal = $stateName -in @('success','failed','cancelled')
|
||||
} catch {
|
||||
Append-HostLog 'HOST-ERROR' "Abschlussstatus ist nicht lesbar: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $terminal) {
|
||||
$message = 'Der Update-Kern wurde beendet, ohne einen terminalen Status zu schreiben.'
|
||||
Append-HostLog 'HOST-ERROR' $message
|
||||
Write-HostStatus 'failed' 'Update-Kern unvollständig beendet' $message $message 'Das Protokoll enthält jetzt die vollständige Start- und Worker-Diagnose. Es wurden keine stillen Fehler verworfen.'
|
||||
} else {
|
||||
Append-HostLog 'HOST' "Worker-Core regulär beendet. Status: $stateName"
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
$full = $_.Exception.ToString()
|
||||
$message = $_.Exception.Message
|
||||
Append-HostLog 'HOST-FATAL' $full
|
||||
Write-HostStatus 'failed' 'Worker konnte nicht initialisiert werden' $message $full 'Der Fehler entstand vor oder beim Laden des Worker-Kerns. Protokoll öffnen zeigt die vollständige PowerShell-Ausnahme einschließlich Datei und Zeile.'
|
||||
try { [Console]::Error.WriteLine($full) } catch {}
|
||||
Start-Sleep -Milliseconds 350
|
||||
exit 1
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
Set-StrictMode -Version 2.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$UpdaterVersion = '3.3.0'
|
||||
|
||||
function Get-CanonicalPackageRoot {
|
||||
$candidate = Join-Path -Path $PSScriptRoot -ChildPath '..\..'
|
||||
$resolved = Resolve-Path -LiteralPath $candidate -ErrorAction Stop
|
||||
$fullPath = [System.IO.Path]::GetFullPath($resolved.ProviderPath)
|
||||
if ([string]::IsNullOrWhiteSpace($fullPath)) {
|
||||
throw 'Der kanonische Vendoo-Paketpfad ist leer.'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $fullPath -PathType Container)) {
|
||||
throw "Der kanonische Vendoo-Paketpfad existiert nicht: $fullPath"
|
||||
}
|
||||
return $fullPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
|
||||
}
|
||||
|
||||
$PackageRoot = Get-CanonicalPackageRoot
|
||||
$logDirectory = Join-Path -Path $PackageRoot -ChildPath 'logs'
|
||||
$logFile = Join-Path -Path $logDirectory -ChildPath 'updater-preflight.log'
|
||||
|
||||
function Write-PreflightLog([string]$Level,[string]$Message) {
|
||||
[System.IO.Directory]::CreateDirectory($logDirectory) | Out-Null
|
||||
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') [$Level] $Message$([Environment]::NewLine)"
|
||||
[System.IO.File]::AppendAllText($logFile,$line,(New-Object System.Text.UTF8Encoding($false)))
|
||||
}
|
||||
|
||||
function Show-PreflightError([string]$Message) {
|
||||
try {
|
||||
Add-Type -AssemblyName PresentationFramework
|
||||
[System.Windows.MessageBox]::Show(
|
||||
"Der Vendoo Production Updater konnte die PowerShell-Startprüfung nicht bestehen.`n`n$Message`n`nProtokoll:`n$logFile",
|
||||
'Vendoo Updater – Startprüfung fehlgeschlagen',
|
||||
[System.Windows.MessageBoxButton]::OK,
|
||||
[System.Windows.MessageBoxImage]::Error
|
||||
) | Out-Null
|
||||
} catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
Write-PreflightLog 'INFO' "Production Updater $UpdaterVersion – kanonischer Paketpfad: $PackageRoot"
|
||||
if ($PackageRoot.IndexOfAny([System.IO.Path]::GetInvalidPathChars()) -ge 0) {
|
||||
throw "Der kanonische Paketpfad enthält unzulässige Zeichen: $PackageRoot"
|
||||
}
|
||||
|
||||
$scripts = @(
|
||||
(Join-Path $PackageRoot 'scripts\updater\Vendoo.Updater.Preflight.ps1'),
|
||||
(Join-Path $PackageRoot 'scripts\update-vendoo-gui.ps1'),
|
||||
(Join-Path $PackageRoot 'scripts\updater\Vendoo.Updater.UI.ps1'),
|
||||
(Join-Path $PackageRoot 'scripts\updater\Vendoo.Updater.Host.ps1'),
|
||||
(Join-Path $PackageRoot 'scripts\updater\Vendoo.Updater.Worker.ps1')
|
||||
)
|
||||
|
||||
Write-PreflightLog 'INFO' 'Echte Windows-PowerShell-Parserprüfung gestartet.'
|
||||
$allErrors = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($scriptPath in $scripts) {
|
||||
if (-not (Test-Path -LiteralPath $scriptPath -PathType Leaf)) {
|
||||
$allErrors.Add("Pflichtskript fehlt: $scriptPath")
|
||||
continue
|
||||
}
|
||||
$tokens = $null
|
||||
$errors = $null
|
||||
[void][System.Management.Automation.Language.Parser]::ParseFile($scriptPath,[ref]$tokens,[ref]$errors)
|
||||
if ($errors -and $errors.Count -gt 0) {
|
||||
foreach ($parseError in $errors) {
|
||||
$allErrors.Add(("{0}:{1}:{2} {3}" -f $parseError.Extent.File,$parseError.Extent.StartLineNumber,$parseError.Extent.StartColumnNumber,$parseError.Message))
|
||||
}
|
||||
} else {
|
||||
Write-PreflightLog 'OK' "Parserprüfung erfolgreich: $scriptPath"
|
||||
}
|
||||
}
|
||||
|
||||
if ($allErrors.Count -gt 0) {
|
||||
$message = @($allErrors) -join [Environment]::NewLine
|
||||
Write-PreflightLog 'FATAL' $message
|
||||
Show-PreflightError $message
|
||||
exit 21
|
||||
}
|
||||
|
||||
Write-PreflightLog 'OK' 'Alle Updater-Skripte wurden vom echten Windows-PowerShell-Parser akzeptiert.'
|
||||
exit 0
|
||||
} catch {
|
||||
$message = $_.Exception.ToString()
|
||||
try { Write-PreflightLog 'FATAL' $message } catch {}
|
||||
Show-PreflightError $message
|
||||
exit 22
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param([switch]$SkipProductionDeploy)
|
||||
|
||||
Set-StrictMode -Version 2.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName PresentationFramework
|
||||
Add-Type -AssemblyName PresentationCore
|
||||
Add-Type -AssemblyName WindowsBase
|
||||
Add-Type -AssemblyName System.Xaml
|
||||
|
||||
$PackageRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
|
||||
$TargetVersion = '1.43.0'
|
||||
$UpdaterVersion = '3.3.0'
|
||||
$WorkerScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Worker.ps1'
|
||||
$WorkerHostScript = Join-Path $PSScriptRoot 'Vendoo.Updater.Host.ps1'
|
||||
$LocalRoot = Join-Path $env:LOCALAPPDATA 'Vendoo\Updater'
|
||||
$RuntimeRoot = Join-Path $LocalRoot 'runtime'
|
||||
$SessionRoot = Join-Path $LocalRoot 'sessions'
|
||||
New-Item -ItemType Directory -Path $RuntimeRoot,$SessionRoot -Force | Out-Null
|
||||
|
||||
$createdNew = $false
|
||||
$mutex = [System.Threading.Mutex]::new($true, 'Local\VendooProductionUpdater', [ref]$createdNew)
|
||||
if (-not $createdNew) {
|
||||
[System.Windows.MessageBox]::Show('Der Vendoo-Updater läuft bereits. Bitte das vorhandene Fenster verwenden.','Vendoo Updater',[System.Windows.MessageBoxButton]::OK,[System.Windows.MessageBoxImage]::Information) | Out-Null
|
||||
return
|
||||
}
|
||||
|
||||
$xaml = @'
|
||||
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Vendoo Production Updater" Width="1080" Height="720" MinWidth="960" MinHeight="650"
|
||||
WindowStartupLocation="CenterScreen" Background="#0F1115" Foreground="#EEF1F5"
|
||||
FontFamily="Segoe UI" ResizeMode="CanResize">
|
||||
<Window.Resources>
|
||||
<SolidColorBrush x:Key="Panel" Color="#171A20"/>
|
||||
<SolidColorBrush x:Key="Panel2" Color="#1D2129"/>
|
||||
<SolidColorBrush x:Key="Border" Color="#2B313C"/>
|
||||
<SolidColorBrush x:Key="Muted" Color="#9AA4B2"/>
|
||||
<SolidColorBrush x:Key="Accent" Color="#4F8CFF"/>
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Foreground" Value="#EEF1F5"/><Setter Property="Background" Value="#252B35"/>
|
||||
<Setter Property="BorderBrush" Value="#343C49"/><Setter Property="Padding" Value="16,9"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/><Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template"><Setter.Value><ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1" CornerRadius="7" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border><ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True"><Setter Property="Background" Value="#303744"/></Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False"><Setter Property="Opacity" Value="0.42"/></Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate></Setter.Value></Setter>
|
||||
</Style>
|
||||
<Style x:Key="PrimaryButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Background" Value="#2E6FE8"/><Setter Property="BorderBrush" Value="#4F8CFF"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions><RowDefinition Height="76"/><RowDefinition Height="*"/><RowDefinition Height="68"/></Grid.RowDefinitions>
|
||||
<Border Grid.Row="0" Background="#14171C" BorderBrush="#252B35" BorderThickness="0,0,0,1">
|
||||
<Grid Margin="28,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock FontSize="23" FontWeight="SemiBold" Text="Vendoo Production Updater"/>
|
||||
<TextBlock Foreground="{StaticResource Muted}" FontSize="12" Margin="0,4,0,0" Text="Sicherer Release-, GitHub- und Coolify-Ablauf"/>
|
||||
</StackPanel>
|
||||
<Border Grid.Column="1" Background="#202631" BorderBrush="#343C49" BorderThickness="1" CornerRadius="8" Padding="14,8" VerticalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal"><TextBlock Text="ZIEL "/><TextBlock FontWeight="Bold" Text="1.43.0"/></StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="1" Margin="24,22,24,16">
|
||||
<Grid.ColumnDefinitions><ColumnDefinition Width="300"/><ColumnDefinition Width="18"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="0" Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="10" Padding="18">
|
||||
<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="*"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||||
<StackPanel><TextBlock FontSize="15" FontWeight="SemiBold" Text="Update-Ablauf"/><TextBlock Foreground="{StaticResource Muted}" FontSize="11" Margin="0,4,0,14" Text="Jeder Schritt wird einzeln geprüft."/></StackPanel>
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"><StackPanel x:Name="StepsPanel"/></ScrollViewer>
|
||||
<Border Grid.Row="2" Background="#12151A" BorderBrush="#272D37" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,14,0,0">
|
||||
<StackPanel><TextBlock Foreground="{StaticResource Muted}" FontSize="10" Text="SICHERHEIT"/>
|
||||
<TextBlock FontSize="11" Margin="0,5,0,0" TextWrapping="Wrap" Text="Kein Datenbank-Reset. Keine Uploads, Secrets oder /app/data-Dateien werden ersetzt."/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Column="2"><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/></Grid.RowDefinitions>
|
||||
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="10" Padding="22">
|
||||
<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||||
<Grid><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||
<StackPanel><TextBlock x:Name="PhaseText" FontSize="20" FontWeight="SemiBold" Text="Updater wird vorbereitet"/>
|
||||
<TextBlock x:Name="DetailText" Foreground="{StaticResource Muted}" Margin="0,7,20,0" TextWrapping="Wrap" Text="Die sichere Update-Umgebung wird initialisiert."/>
|
||||
</StackPanel>
|
||||
<Border x:Name="StatusBadge" Grid.Column="1" Background="#22314A" BorderBrush="#36598A" BorderThickness="1" CornerRadius="7" Padding="12,7" VerticalAlignment="Top">
|
||||
<TextBlock x:Name="StatusText" FontWeight="Bold" FontSize="11" Text="START"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" Margin="0,24,0,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="64"/></Grid.ColumnDefinitions>
|
||||
<ProgressBar x:Name="ProgressBar" Height="10" Minimum="0" Maximum="100" Value="0" Foreground="{StaticResource Accent}" Background="#262C35" BorderThickness="0"/>
|
||||
<TextBlock x:Name="PercentText" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" FontWeight="SemiBold" Text="0 %"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="2" Margin="0,13,0,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="ActivityText" Foreground="{StaticResource Muted}" FontSize="11" Text="Laufzeit 00:00"/>
|
||||
<TextBlock x:Name="HeartbeatText" Grid.Column="1" Foreground="{StaticResource Muted}" FontSize="11" Text="Status wird aufgebaut"/>
|
||||
</Grid>
|
||||
<Border x:Name="InputPanel" Grid.Row="3" Visibility="Collapsed" Background="#171E2A" BorderBrush="#35547E" BorderThickness="1" CornerRadius="8" Padding="15" Margin="0,18,0,0">
|
||||
<Grid><Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||||
<TextBlock x:Name="InputTitle" FontWeight="SemiBold" Text="Eingabe erforderlich"/>
|
||||
<TextBlock x:Name="InputPrompt" Grid.Row="1" Foreground="{StaticResource Muted}" Margin="0,6,0,10" TextWrapping="Wrap"/>
|
||||
<Grid Grid.Row="2"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||
<PasswordBox x:Name="SecretInput" Height="34" Padding="8" Background="#0F1217" Foreground="#EEF1F5" BorderBrush="#3A4657"/>
|
||||
<Button x:Name="SubmitInputButton" Grid.Column="1" Style="{StaticResource PrimaryButton}" Margin="10,0,0,0" Content="Sicher übernehmen"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1" x:Name="ErrorPanel" Visibility="Collapsed" Background="#24181B" BorderBrush="#7A3640" BorderThickness="1" CornerRadius="10" Padding="18" Margin="0,14,0,0">
|
||||
<StackPanel><TextBlock x:Name="ErrorTitle" FontSize="15" FontWeight="SemiBold" Foreground="#FFB6BE" Text="Update gestoppt"/>
|
||||
<TextBlock x:Name="ErrorText" Margin="0,7,0,0" TextWrapping="Wrap"/>
|
||||
<TextBlock x:Name="RemediationText" Foreground="#D7A9AF" Margin="0,8,0,0" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="2" Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="10" Margin="0,14,0,0">
|
||||
<Grid><Grid.RowDefinitions><RowDefinition Height="42"/><RowDefinition Height="*"/></Grid.RowDefinitions>
|
||||
<Grid Margin="16,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" FontWeight="SemiBold" Text="Technisches Protokoll"/>
|
||||
<TextBlock x:Name="LogPathText" Grid.Column="1" VerticalAlignment="Center" Foreground="{StaticResource Muted}" FontSize="10" Text="wird vorbereitet"/>
|
||||
</Grid>
|
||||
<TextBox x:Name="LogBox" Grid.Row="1" Margin="12,0,12,12" IsReadOnly="True" AcceptsReturn="True" TextWrapping="NoWrap" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Background="#0D0F13" Foreground="#CDD4DE" BorderBrush="#292F38" FontFamily="Consolas" FontSize="11" Padding="10"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="2" Background="#14171C" BorderBrush="#252B35" BorderThickness="0,1,0,0">
|
||||
<Grid Margin="24,0"><Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button x:Name="OpenLogButton" Content="Protokoll öffnen" IsEnabled="False"/>
|
||||
<Button x:Name="OpenRuntimeButton" Content="Diagnoseordner" Margin="10,0,0,0" IsEnabled="False"/>
|
||||
<Button x:Name="CopyErrorButton" Content="Fehler kopieren" Margin="10,0,0,0" Visibility="Collapsed"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Button x:Name="CancelButton" Content="Update abbrechen"/>
|
||||
<Button x:Name="RetryButton" Content="Erneut versuchen" Margin="10,0,0,0" Visibility="Collapsed"/>
|
||||
<Button x:Name="CloseButton" Style="{StaticResource PrimaryButton}" Content="Schließen" Margin="10,0,0,0" IsEnabled="False"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
'@
|
||||
|
||||
$reader = [System.Xml.XmlNodeReader]::new([xml]$xaml)
|
||||
$window = [System.Windows.Markup.XamlReader]::Load($reader)
|
||||
$names = @('StepsPanel','PhaseText','DetailText','StatusBadge','StatusText','ProgressBar','PercentText','ActivityText','HeartbeatText','InputPanel','InputTitle','InputPrompt','SecretInput','SubmitInputButton','ErrorPanel','ErrorTitle','ErrorText','RemediationText','LogBox','LogPathText','OpenLogButton','OpenRuntimeButton','CopyErrorButton','CancelButton','RetryButton','CloseButton')
|
||||
foreach ($name in $names) { Set-Variable -Name $name -Value $window.FindName($name) -Scope Script }
|
||||
|
||||
$steps = @(
|
||||
@{id='package'; title='Paket prüfen'}, @{id='tools'; title='System prüfen'}, @{id='github'; title='GitHub verbinden'},
|
||||
@{id='production'; title='Produktion vorbereiten'}, @{id='workspace'; title='Arbeitskopie erstellen'}, @{id='tests'; title='Vollständig testen'},
|
||||
@{id='pullrequest'; title='Pull Request erstellen'}, @{id='merge'; title='Sicher mergen'}, @{id='deploy'; title='Coolify deployen'}, @{id='verify'; title='Produktion bestätigen'}
|
||||
)
|
||||
$script:StepControls = @{}
|
||||
foreach ($step in $steps) {
|
||||
$border = New-Object System.Windows.Controls.Border
|
||||
$border.CornerRadius = [System.Windows.CornerRadius]::new(6); $border.Padding = [System.Windows.Thickness]::new(9,8,9,8); $border.Margin = [System.Windows.Thickness]::new(0,0,0,4); $border.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString('#171A20')
|
||||
$grid = New-Object System.Windows.Controls.Grid
|
||||
$c1=New-Object System.Windows.Controls.ColumnDefinition; $c1.Width=[System.Windows.GridLength]::new(28); $grid.ColumnDefinitions.Add($c1)
|
||||
$c2=New-Object System.Windows.Controls.ColumnDefinition; $c2.Width=[System.Windows.GridLength]::new(1,[System.Windows.GridUnitType]::Star); $grid.ColumnDefinitions.Add($c2)
|
||||
$icon=New-Object System.Windows.Controls.TextBlock; $icon.Text='○'; $icon.Foreground='#6E7886'; $icon.FontSize=16; $icon.VerticalAlignment='Center'
|
||||
$label=New-Object System.Windows.Controls.TextBlock; $label.Text=$step.title; $label.Foreground='#AAB3BF'; $label.VerticalAlignment='Center'; [System.Windows.Controls.Grid]::SetColumn($label,1)
|
||||
$grid.Children.Add($icon)|Out-Null; $grid.Children.Add($label)|Out-Null; $border.Child=$grid; $StepsPanel.Children.Add($border)|Out-Null
|
||||
$script:StepControls[$step.id]=@{Border=$border;Icon=$icon;Label=$label}
|
||||
}
|
||||
|
||||
$script:Worker=$null; $script:Timer=$null; $script:Terminal=$false; $script:ClosingAllowed=$false; $script:LastStatusAt=Get-Date; $script:StartedAt=Get-Date
|
||||
$script:StatusFile=''; $script:EventFile=''; $script:InputFile=''; $script:CancelFile=''; $script:SessionFile=Join-Path $SessionRoot "$TargetVersion.json"; $script:LogFile=''; $script:StdoutFile=''; $script:StderrFile=''; $script:EventOffset=0; $script:LastRequestId=''; $script:LastError=''
|
||||
|
||||
function Set-Badge([string]$state) {
|
||||
switch ($state) {
|
||||
'success' { $StatusText.Text='ERFOLGREICH'; $StatusBadge.Background='#173322'; $StatusBadge.BorderBrush='#2C7A4B'; $StatusText.Foreground='#8FE0AC' }
|
||||
'failed' { $StatusText.Text='GESTOPPT'; $StatusBadge.Background='#351B20'; $StatusBadge.BorderBrush='#8B3B48'; $StatusText.Foreground='#FFB6BE' }
|
||||
'input' { $StatusText.Text='EINGABE'; $StatusBadge.Background='#392E17'; $StatusBadge.BorderBrush='#8C6D27'; $StatusText.Foreground='#F4D47B' }
|
||||
'waiting' { $StatusText.Text='WARTE'; $StatusBadge.Background='#1B2D3B'; $StatusBadge.BorderBrush='#35627D'; $StatusText.Foreground='#91CBE9' }
|
||||
'cancelling' { $StatusText.Text='ABBRUCH'; $StatusBadge.Background='#392E17'; $StatusBadge.BorderBrush='#8C6D27'; $StatusText.Foreground='#F4D47B' }
|
||||
default { $StatusText.Text='LÄUFT'; $StatusBadge.Background='#1C2D49'; $StatusBadge.BorderBrush='#36598A'; $StatusText.Foreground='#9FC0FF' }
|
||||
}
|
||||
}
|
||||
|
||||
function Update-Steps($status) {
|
||||
if (-not $status.steps) { return }
|
||||
foreach ($s in @($status.steps)) {
|
||||
if (-not $script:StepControls.ContainsKey([string]$s.id)) { continue }
|
||||
$c=$script:StepControls[[string]$s.id]
|
||||
switch ([string]$s.state) {
|
||||
'done' { $c.Icon.Text='●'; $c.Icon.Foreground='#62D28B'; $c.Label.Foreground='#EEF1F5'; $c.Border.Background='#18251E' }
|
||||
'active' { $c.Icon.Text='●'; $c.Icon.Foreground='#6EA1FF'; $c.Label.Foreground='#FFFFFF'; $c.Label.FontWeight='SemiBold'; $c.Border.Background='#1A2435' }
|
||||
'failed' { $c.Icon.Text='×'; $c.Icon.Foreground='#FF7A88'; $c.Label.Foreground='#FFD2D7'; $c.Border.Background='#2A191D' }
|
||||
'skipped' { $c.Icon.Text='–'; $c.Icon.Foreground='#77808D'; $c.Label.Foreground='#77808D'; $c.Border.Background='#171A20' }
|
||||
default { $c.Icon.Text='○'; $c.Icon.Foreground='#6E7886'; $c.Label.Foreground='#AAB3BF'; $c.Label.FontWeight='Normal'; $c.Border.Background='#171A20' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Write-InitialStatus {
|
||||
$steps=@(
|
||||
@{id='package';title='Paket prüfen';state='pending'},@{id='tools';title='System prüfen';state='pending'},@{id='github';title='GitHub verbinden';state='pending'},
|
||||
@{id='production';title='Produktion vorbereiten';state='pending'},@{id='workspace';title='Arbeitskopie erstellen';state='pending'},@{id='tests';title='Vollständig testen';state='pending'},
|
||||
@{id='pullrequest';title='Pull Request erstellen';state='pending'},@{id='merge';title='Sicher mergen';state='pending'},@{id='deploy';title='Coolify deployen';state='pending'},@{id='verify';title='Produktion bestätigen';state='pending'}
|
||||
)
|
||||
$payload=[ordered]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;percent=0;phase='Updater wird vorbereitet';detail='Die überwachte Update-Umgebung wird initialisiert.';state='running';error='';remediation='';updatedAt=(Get-Date).ToString('o');logFile=$script:LogFile;sessionFile=$script:SessionFile;steps=$steps;request=$null}|ConvertTo-Json -Depth 8
|
||||
[IO.File]::WriteAllText($script:StatusFile,$payload,(New-Object Text.UTF8Encoding($false)))
|
||||
}
|
||||
|
||||
function Get-WorkerExitDiagnostics {
|
||||
$parts=New-Object Collections.Generic.List[string]
|
||||
try {
|
||||
$stderr=[string]$script:Worker.StandardError.ReadToEnd()
|
||||
if($stderr){$stderr|Set-Content -LiteralPath $script:StderrFile -Encoding UTF8;$parts.Add("PowerShell-Fehlerkanal:`n$stderr")}
|
||||
} catch {}
|
||||
try {
|
||||
$stdout=[string]$script:Worker.StandardOutput.ReadToEnd()
|
||||
if($stdout){$stdout|Set-Content -LiteralPath $script:StdoutFile -Encoding UTF8;$parts.Add("PowerShell-Ausgabe:`n$stdout")}
|
||||
} catch {}
|
||||
if(Test-Path -LiteralPath $script:LogFile){
|
||||
try{
|
||||
$tail=@(Get-Content -LiteralPath $script:LogFile -Encoding UTF8 -Tail 35) -join [Environment]::NewLine
|
||||
if($tail){$parts.Add("Letzte Protokollzeilen:`n$tail")}
|
||||
}catch{}
|
||||
}
|
||||
if($parts.Count -eq 0){$parts.Add('Der Prozess lieferte keinen Fehlertext. Die Bootstrap-Dateien im Laufzeitordner wurden dennoch erhalten.')}
|
||||
return ($parts -join ([Environment]::NewLine+[Environment]::NewLine))
|
||||
}
|
||||
|
||||
function Start-Worker {
|
||||
$stamp=Get-Date -Format 'yyyyMMdd-HHmmss-fff'; $run=Join-Path $RuntimeRoot $stamp; New-Item -ItemType Directory -Path $run -Force|Out-Null
|
||||
$script:StatusFile=Join-Path $run 'status.json'; $script:EventFile=Join-Path $run 'events.ndjson'; $script:InputFile=Join-Path $run 'input.json'; $script:CancelFile=Join-Path $run 'cancel.flag'
|
||||
$script:LogFile=Join-Path $run 'updater.log'; $script:StdoutFile=Join-Path $run 'worker.stdout.log'; $script:StderrFile=Join-Path $run 'worker.stderr.log'
|
||||
$script:EventOffset=0; $script:Terminal=$false; $script:ClosingAllowed=$false; $script:StartedAt=Get-Date; $script:LastStatusAt=Get-Date; $script:LastRequestId=''; $script:LastError=''
|
||||
$ErrorPanel.Visibility='Collapsed'; $InputPanel.Visibility='Collapsed'; $CopyErrorButton.Visibility='Collapsed'; $RetryButton.Visibility='Collapsed'; $CloseButton.IsEnabled=$false; $CancelButton.IsEnabled=$true; $OpenLogButton.IsEnabled=$true; $LogBox.Clear()
|
||||
$LogPathText.Text=[IO.Path]::GetFileName($script:LogFile)
|
||||
[IO.File]::WriteAllText($script:LogFile,"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff') [UI] Laufzeitordner angelegt: $run$([Environment]::NewLine)",(New-Object Text.UTF8Encoding($false)))
|
||||
$OpenRuntimeButton.IsEnabled=$true
|
||||
foreach($entry in $script:StepControls.Values){$entry.Icon.Text='○';$entry.Icon.Foreground='#6E7886';$entry.Label.Foreground='#AAB3BF';$entry.Label.FontWeight='Normal';$entry.Border.Background='#171A20'}
|
||||
Set-Badge 'running'; $PhaseText.Text='Updater wird vorbereitet'; $DetailText.Text='Die überwachte Update-Umgebung wird initialisiert.'; $ProgressBar.Value=0; $PercentText.Text='0 %'
|
||||
Write-InitialStatus
|
||||
|
||||
$args=@('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"{0}"' -f $WorkerHostScript),'-CoreScript',('"{0}"' -f $WorkerScript),'-StatusFile',('"{0}"' -f $script:StatusFile),'-EventFile',('"{0}"' -f $script:EventFile),'-InputFile',('"{0}"' -f $script:InputFile),'-CancelFile',('"{0}"' -f $script:CancelFile),'-SessionFile',('"{0}"' -f $script:SessionFile),'-LogFile',('"{0}"' -f $script:LogFile))
|
||||
if($SkipProductionDeploy){$args+='-SkipProductionDeploy'}
|
||||
$psi=New-Object Diagnostics.ProcessStartInfo
|
||||
$psi.FileName='powershell.exe'; $psi.Arguments=($args -join ' '); $psi.UseShellExecute=$false; $psi.CreateNoWindow=$true; $psi.WorkingDirectory=$PackageRoot
|
||||
$psi.RedirectStandardOutput=$true; $psi.RedirectStandardError=$true
|
||||
$script:Worker=New-Object Diagnostics.Process; $script:Worker.StartInfo=$psi
|
||||
if(-not $script:Worker.Start()){throw 'Der überwachte Update-Prozess konnte nicht gestartet werden.'}
|
||||
}
|
||||
|
||||
function Read-NewEvents {
|
||||
if(-not (Test-Path -LiteralPath $script:EventFile)){return}
|
||||
$stream=[IO.File]::Open($script:EventFile,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
if($script:EventOffset -gt $stream.Length){$script:EventOffset=0}
|
||||
$stream.Seek($script:EventOffset,[IO.SeekOrigin]::Begin)|Out-Null
|
||||
$reader=New-Object IO.StreamReader($stream,[Text.Encoding]::UTF8,$true,4096,$true)
|
||||
while(-not $reader.EndOfStream){
|
||||
$line=$reader.ReadLine(); if(-not $line){continue}
|
||||
try{$e=$line|ConvertFrom-Json; $display=('{0} {1,-7} {2}' -f ([DateTime]::Parse($e.at).ToString('HH:mm:ss')),[string]$e.level,[string]$e.message)}catch{$display=$line}
|
||||
$LogBox.AppendText($display+[Environment]::NewLine); $LogBox.ScrollToEnd()
|
||||
}
|
||||
$script:EventOffset=$stream.Position; $reader.Dispose()
|
||||
} finally {$stream.Dispose()}
|
||||
}
|
||||
|
||||
function Apply-Status($s) {
|
||||
$script:LastStatusAt=Get-Date
|
||||
$PhaseText.Text=[string]$s.phase; $DetailText.Text=[string]$s.detail
|
||||
$p=[Math]::Max(0,[Math]::Min(100,[int]$s.percent)); $ProgressBar.Value=$p; $PercentText.Text="$p %"
|
||||
Set-Badge ([string]$s.state); Update-Steps $s
|
||||
if($s.logFile){$script:LogFile=[string]$s.logFile;$LogPathText.Text=[IO.Path]::GetFileName($script:LogFile);$OpenLogButton.IsEnabled=$true}
|
||||
if([string]$s.state -eq 'input' -and $s.request){
|
||||
$requestId=[string]$s.request.id
|
||||
if($requestId -ne $script:LastRequestId){$script:LastRequestId=$requestId;$SecretInput.Clear();$InputTitle.Text=[string]$s.request.title;$InputPrompt.Text=[string]$s.request.prompt;$InputPanel.Visibility='Visible';$SecretInput.Focus()}
|
||||
} else {$InputPanel.Visibility='Collapsed'}
|
||||
if([string]$s.state -in @('success','failed','cancelled')){
|
||||
$script:Terminal=$true;$script:ClosingAllowed=$true;$CancelButton.IsEnabled=$false;$CloseButton.IsEnabled=$true
|
||||
if([string]$s.state -eq 'failed'){
|
||||
$script:LastError=[string]$s.error; $ErrorTitle.Text='Update sicher gestoppt';$ErrorText.Text=[string]$s.error;$RemediationText.Text=[string]$s.remediation;$ErrorPanel.Visibility='Visible';$CopyErrorButton.Visibility='Visible';$RetryButton.Visibility='Visible'
|
||||
} elseif([string]$s.state -eq 'cancelled'){
|
||||
$script:LastError='Das Update wurde durch den Benutzer abgebrochen.';$ErrorTitle.Text='Update abgebrochen';$ErrorText.Text=$script:LastError;$RemediationText.Text='Ein erneuter Start setzt den kontrollierten Ablauf fort.';$ErrorPanel.Visibility='Visible';$CopyErrorButton.Visibility='Visible';$RetryButton.Visibility='Visible'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$SubmitInputButton.Add_Click({
|
||||
$value=$SecretInput.Password.Trim(); if(-not $value){[System.Windows.MessageBox]::Show('Bitte einen Wert eintragen.','Vendoo Updater',[System.Windows.MessageBoxButton]::OK,[System.Windows.MessageBoxImage]::Warning)|Out-Null;return}
|
||||
@{requestId=$script:LastRequestId;value=$value;submittedAt=(Get-Date).ToString('o')}|ConvertTo-Json|Set-Content -LiteralPath $script:InputFile -Encoding UTF8
|
||||
$SecretInput.Clear();$InputPanel.Visibility='Collapsed';$DetailText.Text='Eingabe wurde sicher übernommen und wird geprüft.'
|
||||
})
|
||||
$CancelButton.Add_Click({if($script:Worker -and -not $script:Worker.HasExited){New-Item -ItemType File -Path $script:CancelFile -Force|Out-Null;Set-Badge 'cancelling';$DetailText.Text='Der aktuelle Prozess wird kontrolliert beendet. Bitte kurz geöffnet lassen.';$CancelButton.IsEnabled=$false}})
|
||||
$RetryButton.Add_Click({ try { Start-Worker } catch { $script:LastError=$_.Exception.Message; $ErrorText.Text=$script:LastError; $ErrorPanel.Visibility='Visible'; Set-Badge 'failed' } })
|
||||
$CloseButton.Add_Click({$script:ClosingAllowed=$true;$window.Close()})
|
||||
$OpenLogButton.Add_Click({if($script:LogFile -and (Test-Path -LiteralPath $script:LogFile)){Start-Process notepad.exe -ArgumentList ('"{0}"' -f $script:LogFile)}elseif($script:StatusFile){$folder=Split-Path -Parent $script:StatusFile;if(Test-Path $folder){Start-Process explorer.exe -ArgumentList ('"{0}"' -f $folder)}}})
|
||||
$OpenRuntimeButton.Add_Click({if($script:StatusFile){$folder=Split-Path -Parent $script:StatusFile;if(Test-Path $folder){Start-Process explorer.exe -ArgumentList ('"{0}"' -f $folder)}}})
|
||||
$CopyErrorButton.Add_Click({if($script:LastError){[System.Windows.Clipboard]::SetText($script:LastError+[Environment]::NewLine+[Environment]::NewLine+'Protokoll: '+$script:LogFile)}})
|
||||
$window.Add_Closing({param($sender,$e) if(-not $script:ClosingAllowed){if($script:Worker -and -not $script:Worker.HasExited){$choice=[System.Windows.MessageBox]::Show('Das Update läuft noch. Soll es kontrolliert abgebrochen werden?','Vendoo Updater',[System.Windows.MessageBoxButton]::YesNo,[System.Windows.MessageBoxImage]::Warning);if($choice -eq [System.Windows.MessageBoxResult]::Yes){New-Item -ItemType File -Path $script:CancelFile -Force|Out-Null;$e.Cancel=$true;$CancelButton.IsEnabled=$false;$DetailText.Text='Abbruch wird ausgeführt …'}else{$e.Cancel=$true}}else{$e.Cancel=$true}}})
|
||||
|
||||
$script:Timer=New-Object System.Windows.Threading.DispatcherTimer
|
||||
$script:Timer.Interval=[TimeSpan]::FromMilliseconds(350)
|
||||
$script:Timer.Add_Tick({
|
||||
try {
|
||||
Read-NewEvents
|
||||
if(Test-Path -LiteralPath $script:StatusFile){try{$s=Get-Content -LiteralPath $script:StatusFile -Raw -Encoding UTF8|ConvertFrom-Json;Apply-Status $s}catch{}}
|
||||
$elapsed=(Get-Date)-$script:StartedAt;$idle=[int]((Get-Date)-$script:LastStatusAt).TotalSeconds
|
||||
$ActivityText.Text=('Laufzeit {0:00}:{1:00}' -f [int]$elapsed.TotalMinutes,$elapsed.Seconds);$HeartbeatText.Text=if($idle -lt 5){'Status aktuell'}else{"Letzte Meldung vor $idle Sek."}
|
||||
if($script:Worker -and $script:Worker.HasExited -and -not $script:Terminal){
|
||||
if(Test-Path -LiteralPath $script:StatusFile){
|
||||
try{$final=Get-Content -LiteralPath $script:StatusFile -Raw -Encoding UTF8|ConvertFrom-Json;Apply-Status $final}catch{}
|
||||
}
|
||||
if(-not $script:Terminal){
|
||||
$diagnostics=Get-WorkerExitDiagnostics
|
||||
$script:Terminal=$true;$script:ClosingAllowed=$true;$CancelButton.IsEnabled=$false;$CloseButton.IsEnabled=$true;$RetryButton.Visibility='Visible';$CopyErrorButton.Visibility='Visible';Set-Badge 'failed'
|
||||
$PhaseText.Text='Updater-Prozess unerwartet beendet';$DetailText.Text='Die vollständige Startdiagnose wurde aufgezeichnet. Das Fenster bleibt geöffnet.'
|
||||
$script:LastError="Der überwachte Worker-Host wurde mit Exit-Code $($script:Worker.ExitCode) beendet.`n`n$diagnostics"
|
||||
$ErrorText.Text=$script:LastError;$RemediationText.Text='Protokoll öffnen zeigt die vollständige Ausnahme mit Datei und Zeile. Danach kann derselbe sichere Lauf erneut gestartet werden.';$ErrorPanel.Visibility='Visible'
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
$script:LastError=$_.Exception.Message
|
||||
}
|
||||
})
|
||||
|
||||
try { Start-Worker; $script:Timer.Start(); $window.ShowDialog()|Out-Null }
|
||||
finally { try{$script:Timer.Stop()}catch{};if($script:Worker -and -not $script:Worker.HasExited){try{New-Item -ItemType File -Path $script:CancelFile -Force|Out-Null}catch{}};try{$mutex.ReleaseMutex()}catch{};$mutex.Dispose() }
|
||||
@@ -1,334 +0,0 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$StatusFile,
|
||||
[Parameter(Mandatory=$true)][string]$EventFile,
|
||||
[Parameter(Mandatory=$true)][string]$InputFile,
|
||||
[Parameter(Mandatory=$true)][string]$CancelFile,
|
||||
[Parameter(Mandatory=$true)][string]$SessionFile,
|
||||
[Parameter(Mandatory=$true)][string]$LogFile,
|
||||
[switch]$SkipProductionDeploy,
|
||||
[switch]$SelfTest,
|
||||
[string]$SelfTestFailAt = ''
|
||||
)
|
||||
|
||||
Set-StrictMode -Version 2.0
|
||||
$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue'
|
||||
Add-Type -AssemblyName System.Net.Http
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$PackageRoot=(Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..\..') -ErrorAction Stop).ProviderPath.TrimEnd([System.IO.Path]::DirectorySeparatorChar,[System.IO.Path]::AltDirectorySeparatorChar)
|
||||
$Repository='Masterluke77/vendoo';$BaseBranch='main';$TargetVersion='1.43.0';$UpdaterVersion='3.3.0';$PublicBaseUrl='https://vendoo.flatlined.de'
|
||||
$LocalRoot=Join-Path $env:LOCALAPPDATA 'Vendoo\Updater';$ToolsRoot=Join-Path $LocalRoot 'tools';$LogsRoot=Join-Path $LocalRoot 'logs';$WorkRoot=Join-Path $LocalRoot "work\$TargetVersion";New-Item -ItemType Directory -Path $ToolsRoot,$LogsRoot,(Split-Path -Parent $SessionFile) -Force|Out-Null
|
||||
$Stamp=Get-Date -Format 'yyyyMMdd-HHmmss';$Branch="release/$TargetVersion-production-update-v3";$CloneDir=Join-Path $WorkRoot 'repository'
|
||||
$script:Tools=@{};$script:CurrentStep='package';$script:LastHeartbeat=Get-Date
|
||||
$script:Steps=@(
|
||||
[ordered]@{id='package';title='Paket prüfen';state='pending'},[ordered]@{id='tools';title='System prüfen';state='pending'},[ordered]@{id='github';title='GitHub verbinden';state='pending'},
|
||||
[ordered]@{id='production';title='Produktion vorbereiten';state='pending'},[ordered]@{id='workspace';title='Arbeitskopie erstellen';state='pending'},[ordered]@{id='tests';title='Vollständig testen';state='pending'},
|
||||
[ordered]@{id='pullrequest';title='Pull Request erstellen';state='pending'},[ordered]@{id='merge';title='Sicher mergen';state='pending'},[ordered]@{id='deploy';title='Coolify deployen';state='pending'},[ordered]@{id='verify';title='Produktion bestätigen';state='pending'}
|
||||
)
|
||||
|
||||
function Write-Event([string]$Level,[string]$Message){
|
||||
$safe=$Message -replace '(?i)(https://[^\s]*(?:token|webhook)[^\s]*)','[VERDECKT]'
|
||||
$entry=[ordered]@{at=(Get-Date).ToString('o');level=$Level.ToUpperInvariant();message=$safe}|ConvertTo-Json -Compress
|
||||
[IO.File]::AppendAllText($EventFile,$entry+[Environment]::NewLine,(New-Object Text.UTF8Encoding($false)))
|
||||
[IO.File]::AppendAllText($LogFile,"$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$($Level.ToUpperInvariant())] $safe$([Environment]::NewLine)",(New-Object Text.UTF8Encoding($false)))
|
||||
}
|
||||
function Write-State([int]$Percent,[string]$Phase,[string]$Detail,[string]$State='running',[string]$Error='',[string]$Remediation='', $Request=$null){
|
||||
$payload=[ordered]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;percent=[Math]::Max(0,[Math]::Min(100,$Percent));phase=$Phase;detail=$Detail;state=$State;error=$Error;remediation=$Remediation;updatedAt=(Get-Date).ToString('o');logFile=$LogFile;sessionFile=$SessionFile;steps=$script:Steps;request=$Request}|ConvertTo-Json -Depth 8
|
||||
$tmp="$StatusFile.$PID.tmp";[IO.File]::WriteAllText($tmp,$payload,(New-Object Text.UTF8Encoding($false)));Move-Item -LiteralPath $tmp -Destination $StatusFile -Force
|
||||
}
|
||||
function Set-Step([string]$Id,[string]$State){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state=$State}elseif($State -eq 'active' -and $s.state -eq 'active'){$s.state='pending'}};$script:CurrentStep=$Id}
|
||||
function Complete-Step([string]$Id){foreach($s in $script:Steps){if($s.id -eq $Id){$s.state='done'}}}
|
||||
function Fail-Step{foreach($s in $script:Steps){if($s.id -eq $script:CurrentStep){$s.state='failed'}}}
|
||||
function Assert-NotCancelled{if(Test-Path -LiteralPath $CancelFile){throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')}}
|
||||
function Save-Session($Data){if(-not($Data.PSObject.Properties.Name -contains 'updaterVersion')){$Data|Add-Member -NotePropertyName updaterVersion -NotePropertyValue $UpdaterVersion};$Data.updaterVersion=$UpdaterVersion;$Data.updatedAt=(Get-Date).ToString('o');$tmp="$SessionFile.tmp";$Data|ConvertTo-Json -Depth 8|Set-Content -LiteralPath $tmp -Encoding UTF8;Move-Item $tmp $SessionFile -Force}
|
||||
function Load-Session{if(Test-Path -LiteralPath $SessionFile){try{$saved=Get-Content $SessionFile -Raw -Encoding UTF8|ConvertFrom-Json;if([string]$saved.version-eq$TargetVersion -and [string]$saved.updaterVersion-eq$UpdaterVersion){return $saved};Write-Event 'INFO' 'Veralteter oder anderer Updater-Sitzungsstand wird nicht übernommen.'}catch{Write-Event 'WARN' 'Gespeicherter Sitzungsstand ist ungültig und wird verworfen.'}};return [pscustomobject]@{version=$TargetVersion;updaterVersion=$UpdaterVersion;branch=$Branch;prUrl='';merged=$false;runId='';stage='new';updatedAt=''}}
|
||||
function Quote-Arg([string]$Value){if($null -eq $Value){return '""'};if($Value -notmatch '[\s"]'){return $Value};return '"'+($Value -replace '(\\*)"','$1$1\"' -replace '(\\+)$','$1$1')+'"'}
|
||||
function Stop-Tree([int]$ProcessId){
|
||||
try { & taskkill.exe /PID $ProcessId /T /F 2>$null | Out-Null }
|
||||
catch { try { Stop-Process -Id $ProcessId -Force -ErrorAction SilentlyContinue } catch {} }
|
||||
}
|
||||
function Get-TailText([string]$Text,[int]$MaxLength){
|
||||
if([string]::IsNullOrWhiteSpace($Text)){return ''}
|
||||
$value=$Text.Trim()
|
||||
if($value.Length -gt $MaxLength){return $value.Substring($value.Length-$MaxLength)}
|
||||
return $value
|
||||
}
|
||||
function Get-ProcessFailureSummary([string]$StdErr,[string]$StdOut){
|
||||
$parts=New-Object Collections.Generic.List[string]
|
||||
$errorTail=Get-TailText $StdErr 1800
|
||||
$outputTail=Get-TailText $StdOut 900
|
||||
if($errorTail){$parts.Add("Fehlerausgabe:`n$errorTail")}
|
||||
if($outputTail){$parts.Add("Letzte Standardausgabe:`n$outputTail")}
|
||||
if($parts.Count -eq 0){return 'Der Prozess lieferte keine Diagnoseausgabe.'}
|
||||
return ($parts -join "`n`n")
|
||||
}
|
||||
function Invoke-External{
|
||||
param([string]$File,[string[]]$Arguments,[string]$WorkingDirectory=$PackageRoot,[int]$TimeoutSeconds=120,[switch]$AllowFailure,[string]$InputText='',[string]$Label='Prozess',[switch]$Quiet)
|
||||
Assert-NotCancelled
|
||||
$psi=New-Object Diagnostics.ProcessStartInfo;$psi.FileName=$File;$psi.Arguments=(@($Arguments|ForEach-Object{Quote-Arg ([string]$_)}) -join ' ');$psi.WorkingDirectory=$WorkingDirectory;$psi.UseShellExecute=$false;$psi.CreateNoWindow=$true;$psi.RedirectStandardOutput=$true;$psi.RedirectStandardError=$true;$psi.RedirectStandardInput=$true
|
||||
# Node.js, npm and Git emit UTF-8 when redirected. Windows PowerShell otherwise decodes
|
||||
# redirected streams with the active OEM code page, which corrupts German umlauts.
|
||||
$utf8NoBom=New-Object Text.UTF8Encoding($false)
|
||||
if($psi.PSObject.Properties.Name -contains 'StandardOutputEncoding'){$psi.StandardOutputEncoding=$utf8NoBom}
|
||||
if($psi.PSObject.Properties.Name -contains 'StandardErrorEncoding'){$psi.StandardErrorEncoding=$utf8NoBom}
|
||||
$psi.EnvironmentVariables['NO_COLOR']='1'
|
||||
$psi.EnvironmentVariables['FORCE_COLOR']='0'
|
||||
$psi.EnvironmentVariables['NPM_CONFIG_COLOR']='false'
|
||||
$psi.EnvironmentVariables['NPM_CONFIG_UNICODE']='true'
|
||||
$p=New-Object Diagnostics.Process;$p.StartInfo=$psi
|
||||
if(-not $Quiet){Write-Event 'INFO' "$Label gestartet."}
|
||||
if(-not $p.Start()){throw "$Label konnte nicht gestartet werden."}
|
||||
$outTask = $p.StandardOutput.ReadToEndAsync()
|
||||
$errTask = $p.StandardError.ReadToEndAsync()
|
||||
if ($InputText) { $p.StandardInput.Write($InputText) }
|
||||
$p.StandardInput.Close()
|
||||
$sw=[Diagnostics.Stopwatch]::StartNew()
|
||||
while(-not $p.WaitForExit(500)){
|
||||
if(Test-Path -LiteralPath $CancelFile){Stop-Tree $p.Id;throw [System.OperationCanceledException]::new('Das Update wurde durch den Benutzer abgebrochen.')}
|
||||
if($sw.Elapsed.TotalSeconds -gt $TimeoutSeconds){Stop-Tree $p.Id;throw "$Label hat das Zeitlimit von $TimeoutSeconds Sekunden überschritten."}
|
||||
if(-not $Quiet -and ((Get-Date)-$script:LastHeartbeat).TotalSeconds -ge 8){Write-Event 'INFO' ("$Label läuft seit {0:mm\:ss}." -f $sw.Elapsed);$script:LastHeartbeat=Get-Date}
|
||||
}
|
||||
$p.WaitForExit();$stdout=[string]$outTask.Result;$stderr=[string]$errTask.Result;$code=$p.ExitCode;$p.Dispose()
|
||||
if(-not $Quiet){
|
||||
foreach($line in @($stdout -split "`r?`n")){if($line.Trim()){Write-Event 'OUTPUT' $line.Trim()}}
|
||||
foreach($line in @($stderr -split "`r?`n")){
|
||||
$clean=$line.Trim()
|
||||
if(-not $clean){continue}
|
||||
if($code -ne 0){Write-Event 'ERROR' $clean}
|
||||
elseif($clean -match '(?i)\b(warn|warning|deprecated)\b'){Write-Event 'WARN' $clean}
|
||||
else{Write-Event 'OUTPUT' $clean}
|
||||
}
|
||||
}
|
||||
if(-not $AllowFailure -and $code -ne 0){$summary=Get-ProcessFailureSummary $stderr $stdout;throw "$Label ist mit Exit-Code $code fehlgeschlagen.`n$summary"}
|
||||
return [pscustomobject]@{Code=$code;Output=$stdout;Error=$stderr}
|
||||
}
|
||||
function Resolve-Tool([string]$Name,[string[]]$Candidates=@()){
|
||||
if($script:Tools.ContainsKey($Name) -and (Test-Path -LiteralPath $script:Tools[$Name])){return $script:Tools[$Name]}
|
||||
foreach($c in @($Candidates)){if($c -and (Test-Path -LiteralPath $c -PathType Leaf)){$script:Tools[$Name]=(Resolve-Path $c).Path;return $script:Tools[$Name]}}
|
||||
$cmd=Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue|Select-Object -First 1;if($cmd -and $cmd.Source -and (Test-Path $cmd.Source)){$script:Tools[$Name]=$cmd.Source;return $cmd.Source};return $null
|
||||
}
|
||||
function Get-GitPath{return Resolve-Tool 'git.exe' @("$env:ProgramFiles\Git\cmd\git.exe","$env:LOCALAPPDATA\Programs\Git\cmd\git.exe")}
|
||||
function Get-NodePath{return Resolve-Tool 'node.exe' @("$env:ProgramFiles\nodejs\node.exe","$env:LOCALAPPDATA\Programs\nodejs\node.exe")}
|
||||
function Get-NpmPath{return Resolve-Tool 'npm.cmd' @("$env:ProgramFiles\nodejs\npm.cmd","$env:LOCALAPPDATA\Programs\nodejs\npm.cmd")}
|
||||
function Invoke-WebJson([string]$Uri){for($i=1;$i-le3;$i++){try{return Invoke-RestMethod -Uri $Uri -Headers @{'User-Agent'="Vendoo-Updater/$UpdaterVersion";'Accept'='application/vnd.github+json'} -TimeoutSec 30}catch{if($i-eq3){throw};Start-Sleep -Seconds (2*$i)}}}
|
||||
function Download-File([string]$Uri,[string]$Destination,[string]$Label){
|
||||
for($attempt=1;$attempt-le3;$attempt++){try{
|
||||
$handler = New-Object Net.Http.HttpClientHandler
|
||||
$client = [Net.Http.HttpClient]::new($handler)
|
||||
$client.Timeout=[TimeSpan]::FromMinutes(5);$client.DefaultRequestHeaders.UserAgent.ParseAdd("Vendoo-Updater/$UpdaterVersion")
|
||||
$resp = $client.GetAsync($Uri,[Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result
|
||||
$null = $resp.EnsureSuccessStatusCode()
|
||||
$total=$resp.Content.Headers.ContentLength;$input=$resp.Content.ReadAsStreamAsync().Result;$output=[IO.File]::Open($Destination,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::None)
|
||||
try{
|
||||
$buf=New-Object byte[] 65536;$readTotal=0L;$lastReported=-10
|
||||
while(($read=$input.Read($buf,0,$buf.Length))-gt0){
|
||||
Assert-NotCancelled;$output.Write($buf,0,$read);$readTotal+=$read
|
||||
if($total -and $total -gt 0){
|
||||
$percent=[Math]::Min(100,[int][Math]::Floor(($readTotal*100.0)/$total))
|
||||
if($percent -ge ($lastReported+10) -or $readTotal -ge $total){
|
||||
$bucket=[Math]::Min(100,[int]([Math]::Floor($percent/10)*10))
|
||||
if($bucket -gt $lastReported){
|
||||
$lastReported=$bucket
|
||||
Write-Event 'INFO' ("${Label}: {0}% ({1:N1} von {2:N1} MB)" -f $bucket,($readTotal/1048576),($total/1048576))
|
||||
Write-State 18 'GitHub CLI' ("${Label} wird geladen: {0}%" -f $bucket)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}finally{$output.Dispose();$input.Dispose();$resp.Dispose();$client.Dispose();$handler.Dispose()}
|
||||
if(-not $total){Write-Event 'INFO' "${Label}: Download abgeschlossen."}
|
||||
return
|
||||
}catch{if($attempt-eq3){throw "$Label konnte nach drei Versuchen nicht geladen werden: $($_.Exception.Message)"};Write-Event 'WARN' "${Label}: Downloadversuch $attempt fehlgeschlagen; erneuter Versuch.";Start-Sleep -Seconds (3*$attempt)}}
|
||||
}
|
||||
|
||||
function Ensure-GitHubCli{
|
||||
$managed=Join-Path $ToolsRoot 'github-cli\current\bin\gh.exe';if(Test-Path $managed){$test=Invoke-External $managed @('--version') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub CLI prüfen';if($test.Code-eq0){$script:Tools['gh.exe']=$managed;return $managed}}
|
||||
Write-State 18 'GitHub CLI' 'Die offizielle portable GitHub CLI wird sicher geladen und per SHA-256 geprüft.'
|
||||
$release=Invoke-WebJson 'https://api.github.com/repos/cli/cli/releases/latest';$arch=if([Environment]::Is64BitOperatingSystem){'amd64'}else{'386'};$asset=@($release.assets|Where-Object{$_.name -like "gh_*_windows_$arch.zip"}|Select-Object -First 1);$checks=@($release.assets|Where-Object{$_.name -like '*_checksums.txt'}|Select-Object -First 1)
|
||||
if(-not $asset -or -not $checks){throw 'Die offiziellen GitHub-CLI-Release-Dateien konnten nicht ermittelt werden.'}
|
||||
$tmp=Join-Path $env:TEMP "vendoo-gh-$PID";New-Item -ItemType Directory -Path $tmp -Force|Out-Null;$zip=Join-Path $tmp $asset.name;$checkFile=Join-Path $tmp $checks.name;$extract=Join-Path $tmp 'extract'
|
||||
try{Download-File $asset.browser_download_url $zip 'GitHub CLI';Download-File $checks.browser_download_url $checkFile 'Prüfsummen';$line=Get-Content $checkFile|Where-Object{$_ -match [regex]::Escape($asset.name)}|Select-Object -First 1;if(-not $line){throw 'Für das GitHub-CLI-Archiv wurde keine offizielle Prüfsumme gefunden.'};$expected=($line -split '\s+')[0].ToLowerInvariant();$actual=(Get-FileHash $zip -Algorithm SHA256).Hash.ToLowerInvariant();if($actual-ne$expected){throw 'SHA-256-Prüfung der GitHub CLI fehlgeschlagen.'};New-Item -ItemType Directory -Path $extract -Force|Out-Null;Expand-Archive $zip $extract -Force;$source=Get-ChildItem $extract -Filter gh.exe -File -Recurse|Select-Object -First 1;if(-not $source){throw 'Das geprüfte GitHub-CLI-Archiv enthält keine gh.exe.'};$targetRoot=Split-Path -Parent (Split-Path -Parent $managed);if(Test-Path $targetRoot){Remove-Item $targetRoot -Recurse -Force};New-Item -ItemType Directory -Path (Split-Path -Parent $managed) -Force|Out-Null;Copy-Item $source.FullName $managed -Force;$test=Invoke-External $managed @('--version') -TimeoutSeconds 20 -Label 'GitHub CLI validieren';$script:Tools['gh.exe']=$managed;Write-Event 'OK' "GitHub CLI $($release.tag_name) wurde geprüft und lokal bereitgestellt.";return $managed}finally{Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue}
|
||||
}
|
||||
function Test-GitHubAuthentication([string]$GhPath){
|
||||
$status=Invoke-External $GhPath @('auth','status','--hostname','github.com') -TimeoutSeconds 20 -AllowFailure -Label 'GitHub-Anmeldung prüfen' -Quiet
|
||||
return ($status.Code -eq 0)
|
||||
}
|
||||
function Invoke-VisibleGitHubLogin([string]$GhPath){
|
||||
$runtimeDirectory=Split-Path -Parent $StatusFile
|
||||
$loginScript=Join-Path $runtimeDirectory 'github-login.cmd'
|
||||
$escapedGh=$GhPath.Replace('"','""')
|
||||
$lines=@(
|
||||
'@echo off',
|
||||
'chcp 65001 >nul',
|
||||
'title Vendoo - GitHub Anmeldung',
|
||||
'echo ============================================================',
|
||||
'echo Vendoo - Sichere GitHub Anmeldung',
|
||||
'echo ============================================================',
|
||||
'echo.',
|
||||
'echo Der einmalige GitHub-Code wird angezeigt und in die',
|
||||
'echo Zwischenablage kopiert. Folge danach dem Browserdialog.',
|
||||
'echo Dieses Fenster schliesst sich nach erfolgreicher Anmeldung.',
|
||||
'echo.',
|
||||
('"{0}" auth login --hostname github.com --git-protocol https --web --clipboard' -f $escapedGh),
|
||||
'set "VENDOO_GH_LOGIN_EXIT=%ERRORLEVEL%"',
|
||||
'echo.',
|
||||
'if "%VENDOO_GH_LOGIN_EXIT%"=="0" (',
|
||||
' echo GitHub-Anmeldung erfolgreich. Vendoo faehrt automatisch fort.',
|
||||
') else (',
|
||||
' echo GitHub-Anmeldung wurde nicht abgeschlossen. Fehlercode: %VENDOO_GH_LOGIN_EXIT%',
|
||||
' echo Das Fenster kann geschlossen werden; der Vendoo Updater bleibt geoeffnet.',
|
||||
' pause',
|
||||
')',
|
||||
'exit /b %VENDOO_GH_LOGIN_EXIT%'
|
||||
)
|
||||
[IO.File]::WriteAllLines($loginScript,$lines,(New-Object Text.UTF8Encoding($false)))
|
||||
Write-State 26 'GitHub-Anmeldung' 'Im geöffneten Anmeldefenster den Gerätecode bestätigen und den Browserdialog abschließen. Danach geht es automatisch weiter.' 'waiting'
|
||||
Write-Event 'WAIT' 'Sichtbares GitHub-Anmeldefenster wird geöffnet.'
|
||||
try{
|
||||
$loginProcess=Start-Process -FilePath 'cmd.exe' -ArgumentList @('/d','/c',('"{0}"' -f $loginScript)) -WorkingDirectory $runtimeDirectory -WindowStyle Normal -PassThru
|
||||
}catch{
|
||||
throw "Das sichtbare GitHub-Anmeldefenster konnte nicht gestartet werden: $($_.Exception.Message)"
|
||||
}
|
||||
$deadline=(Get-Date).AddMinutes(10);$lastStatus=Get-Date
|
||||
while((Get-Date)-lt$deadline){
|
||||
Assert-NotCancelled
|
||||
if(Test-GitHubAuthentication $GhPath){
|
||||
Write-Event 'OK' 'GitHub-Anmeldung wurde bestätigt.'
|
||||
try{if(-not $loginProcess.HasExited){$loginProcess.CloseMainWindow()|Out-Null}}catch{}
|
||||
return
|
||||
}
|
||||
if($loginProcess.HasExited){
|
||||
Start-Sleep -Seconds 1
|
||||
if(Test-GitHubAuthentication $GhPath){Write-Event 'OK' 'GitHub-Anmeldung wurde bestätigt.';return}
|
||||
throw "Die GitHub-Anmeldung wurde nicht abgeschlossen. Das Anmeldefenster endete mit Exit-Code $($loginProcess.ExitCode)."
|
||||
}
|
||||
if(((Get-Date)-$lastStatus).TotalSeconds -ge 15){
|
||||
Write-Event 'INFO' 'GitHub-Anmeldung wartet auf Abschluss im Browser.'
|
||||
$lastStatus=Get-Date
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
try{if(-not $loginProcess.HasExited){Stop-Tree $loginProcess.Id}}catch{}
|
||||
throw 'Die GitHub-Anmeldung wurde nicht innerhalb von zehn Minuten abgeschlossen.'
|
||||
}
|
||||
function Check-ProductionVersion{try{$r=Invoke-RestMethod -Uri "$PublicBaseUrl/readyz?updater=$Stamp" -TimeoutSec 20 -Headers @{'Cache-Control'='no-cache';'User-Agent'="Vendoo-Updater/$UpdaterVersion"};return [pscustomobject]@{Ready=[bool]$r.ok;Version=[string]$r.version}}catch{return [pscustomobject]@{Ready=$false;Version='unbekannt'}}}
|
||||
function Assert-Package{foreach($f in @('package.json','package-lock.json','update-manifest.json','release-manifest.json','db\schema.sql','tools\apply-release-manifest.mjs')){if(-not(Test-Path(Join-Path $PackageRoot $f))){throw "Paketdatei fehlt: $f"}};$p=Get-Content (Join-Path $PackageRoot 'package.json') -Raw -Encoding UTF8|ConvertFrom-Json;$m=Get-Content (Join-Path $PackageRoot 'update-manifest.json') -Raw -Encoding UTF8|ConvertFrom-Json;$r=Get-Content (Join-Path $PackageRoot 'release-manifest.json') -Raw -Encoding UTF8|ConvertFrom-Json;if([string]$p.version-ne$TargetVersion -or [string]$m.version-ne$TargetVersion -or [string]$r.version-ne$TargetVersion){throw "Paket-, Update- und Release-Manifest-Version müssen exakt $TargetVersion sein."};if([string]$r.updaterVersion-ne$UpdaterVersion){throw "Release-Manifest gehört zu Updater $($r.updaterVersion), erwartet wird $UpdaterVersion."};if([string]$r.mode-ne'overlay-with-explicit-removals'){throw "Unzulässiger Release-Modus: $($r.mode)"}}
|
||||
|
||||
try{
|
||||
Write-Event 'INFO' "Vendoo Production Updater $UpdaterVersion gestartet. Zielversion $TargetVersion."
|
||||
$session=Load-Session
|
||||
Set-Step 'package' 'active';Write-State 3 'Paketprüfung' 'Version, Manifest und sicherheitsrelevante Paketdateien werden geprüft.';Assert-Package;if($SelfTestFailAt-eq'package'){throw 'Simulierter Paketfehler.'};Complete-Step 'package';Write-Event 'OK' 'Paketprüfung erfolgreich.'
|
||||
Set-Step 'tools' 'active';Write-State 9 'Systemprüfung' 'Git, Node.js und npm werden mit festen Zeitlimits geprüft.'
|
||||
$git=Get-GitPath;if(-not$git){throw 'Git für Windows wurde nicht gefunden. Bitte Git für Windows installieren; danach kann derselbe Updater erneut gestartet werden.'};$node=Get-NodePath;$npm=Get-NpmPath;if(-not$node -or -not$npm){throw 'Node.js 22 LTS oder npm wurde nicht gefunden.'};$gv=Invoke-External $git @('--version') -TimeoutSeconds 20 -Label 'Git prüfen';$nv=Invoke-External $node @('--version') -TimeoutSeconds 20 -Label 'Node.js prüfen';if($nv.Output -notmatch 'v?(\d+)\.' -or [int]$Matches[1]-lt22){throw "Node.js 22 oder neuer ist erforderlich. Gefunden: $($nv.Output.Trim())"};$gh=Ensure-GitHubCli;Complete-Step 'tools';Write-Event 'OK' 'Systemprüfung erfolgreich.'
|
||||
Set-Step 'github' 'active';Write-State 24 'GitHub-Verbindung' 'Anmeldung und Repository-Berechtigungen werden geprüft.'
|
||||
if(-not (Test-GitHubAuthentication $gh)){Invoke-VisibleGitHubLogin $gh}
|
||||
Invoke-External $gh @('auth','setup-git','--hostname','github.com') -TimeoutSeconds 60 -Label 'Git-Anmeldung konfigurieren'|Out-Null
|
||||
$repo=Invoke-External $gh @('repo','view',$Repository,'--json','viewerPermission,defaultBranchRef,isPrivate') -TimeoutSeconds 60 -Label 'Repository-Zugriff prüfen';$repoObj=$repo.Output|ConvertFrom-Json;if([string]$repoObj.viewerPermission -notin @('ADMIN','MAINTAIN','WRITE')){throw "Für $Repository fehlt Schreibzugriff. GitHub meldet $($repoObj.viewerPermission)."};Complete-Step 'github';Write-Event 'OK' "GitHub-Zugriff bestätigt: $($repoObj.viewerPermission)."
|
||||
Set-Step 'production' 'active';Write-State 32 'Produktionsvorbereitung' 'Aktueller Produktionsstand und Coolify Auto Deploy werden geprüft.'
|
||||
$prod=Check-ProductionVersion;Write-Event 'INFO' "Produktion meldet ready=$($prod.Ready), version=$($prod.Version)."
|
||||
if(-not$SkipProductionDeploy){Write-Event 'OK' 'Kein Coolify-Webhook erforderlich: Nach dem Merge wird das bestehende Coolify Auto Deploy der GitHub-Verknüpfung verwendet.'}
|
||||
Complete-Step 'production'
|
||||
if($SelfTest){foreach($id in @('workspace','tests','pullrequest','merge','deploy','verify')){Set-Step $id 'active';Write-State 50 "Selbsttest: $id" 'Darstellung, Statuswechsel und Protokoll werden simuliert.';Start-Sleep -Milliseconds 350;Complete-Step $id};Write-State 100 'Selbsttest erfolgreich' 'Die Updater-Oberfläche und der Worker-Vertrag funktionieren.' 'success';return}
|
||||
|
||||
if([bool]$session.merged){Write-Event 'INFO' 'Gespeicherter Wiederaufnahmepunkt: main wurde bereits aktualisiert.';foreach($id in @('workspace','tests','pullrequest','merge')){Complete-Step $id}}
|
||||
else {
|
||||
Set-Step 'workspace' 'active'
|
||||
Write-State 40 'Arbeitskopie' 'main wird isoliert geklont; das vollständige Release-Manifest wird ohne Leeren der Baseline angewendet.'
|
||||
if (Test-Path -LiteralPath $WorkRoot) { Remove-Item -LiteralPath $WorkRoot -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null
|
||||
Invoke-External $gh @('repo','clone',$Repository,$CloneDir,'--','--branch',$BaseBranch,'--single-branch') -WorkingDirectory $WorkRoot -TimeoutSeconds 300 -Label 'Repository klonen' | Out-Null
|
||||
Invoke-External $git @('switch','-c',$Branch) -WorkingDirectory $CloneDir -TimeoutSeconds 60 -Label 'Release-Branch erstellen' | Out-Null
|
||||
Invoke-External $node @((Join-Path $PackageRoot 'tools\apply-release-manifest.mjs'),$PackageRoot,$CloneDir) -TimeoutSeconds 300 -Label 'SHA-256-geprüftes Release-Manifest anwenden' | Out-Null
|
||||
Complete-Step 'workspace'
|
||||
|
||||
Set-Step 'tests' 'active'
|
||||
Write-State 52 'Abnahmetests' 'Abhängigkeiten werden reproduzierbar installiert; danach läuft der vollständige verify:all-Vertrag.'
|
||||
Invoke-External $npm @('ci','--no-audit','--no-fund') -WorkingDirectory $CloneDir -TimeoutSeconds 1200 -Label 'npm ci' | Out-Null
|
||||
Write-State 61 'Abnahmetests' 'Alle Vendoo-Sicherheits-, Datenbank-, Modul- und Release-Gates laufen.'
|
||||
Invoke-External $npm @('run','verify:all') -WorkingDirectory $CloneDir -TimeoutSeconds 3600 -Label 'npm run verify:all' | Out-Null
|
||||
Complete-Step 'tests'
|
||||
Write-Event 'OK' 'Alle lokalen Abnahmetests sind grün.'
|
||||
|
||||
Set-Step 'pullrequest' 'active'
|
||||
Write-State 70 'Pull Request' 'Der geprüfte Stand wird committed und in einen kontrollierten Pull Request übertragen.'
|
||||
Invoke-External $git @('config','user.name','Vendoo Production Updater') -WorkingDirectory $CloneDir -Label 'Git-Name setzen' | Out-Null
|
||||
$uid = (Invoke-External $gh @('api','user','--jq','.id') -Label 'GitHub-Benutzer-ID').Output.Trim()
|
||||
$ulogin = (Invoke-External $gh @('api','user','--jq','.login') -Label 'GitHub-Benutzername').Output.Trim()
|
||||
Invoke-External $git @('config','user.email',"$uid+$ulogin@users.noreply.github.com") -WorkingDirectory $CloneDir -Label 'Git-E-Mail setzen' | Out-Null
|
||||
Invoke-External $git @('add','--all') -WorkingDirectory $CloneDir -Label 'Änderungen vormerken' | Out-Null
|
||||
$diff = Invoke-External $git @('diff','--cached','--quiet') -WorkingDirectory $CloneDir -AllowFailure -Label 'Änderungen prüfen'
|
||||
|
||||
if ($diff.Code -eq 0) {
|
||||
Write-Event 'INFO' 'main enthält bereits denselben Source-Stand.'
|
||||
$session.merged = $true
|
||||
$session.stage = 'merged'
|
||||
Save-Session $session
|
||||
Complete-Step 'pullrequest'
|
||||
Complete-Step 'merge'
|
||||
}
|
||||
elseif ($diff.Code -eq 1) {
|
||||
Invoke-External $git @('commit','-m',"release: Vendoo $TargetVersion production update") -WorkingDirectory $CloneDir -TimeoutSeconds 120 -Label 'Release committen' | Out-Null
|
||||
Invoke-External $git @('push','--force','--set-upstream','origin',$Branch) -WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Release-Branch pushen' | Out-Null
|
||||
$existing = Invoke-External $gh @('pr','list','--repo',$Repository,'--head',$Branch,'--state','open','--json','url','--jq','.[0].url') -AllowFailure -Label 'Vorhandenen Pull Request suchen'
|
||||
$prUrl = $existing.Output.Trim()
|
||||
if (-not $prUrl) {
|
||||
$body = Join-Path $WorkRoot 'pr-body.md'
|
||||
$bodyText = @"
|
||||
## Vendoo $TargetVersion – Production Update
|
||||
|
||||
Erstellt durch Vendoo Production Updater $UpdaterVersion.
|
||||
|
||||
- vollständiges `npm run verify:all` erfolgreich
|
||||
- keine Datenbanken, Uploads, Secrets oder `/app/data`-Inhalte
|
||||
- Coolify Auto Deploy mit exakter Versionsprüfung
|
||||
"@
|
||||
$bodyText | Set-Content -LiteralPath $body -Encoding UTF8
|
||||
$prUrl = (Invoke-External $gh @('pr','create','--repo',$Repository,'--base',$BaseBranch,'--head',$Branch,'--title',"Vendoo $TargetVersion – Production Update",'--body-file',$body) -WorkingDirectory $CloneDir -TimeoutSeconds 180 -Label 'Pull Request erstellen').Output.Trim()
|
||||
}
|
||||
$session.prUrl = $prUrl
|
||||
$session.stage = 'pr'
|
||||
Save-Session $session
|
||||
Complete-Step 'pullrequest'
|
||||
Write-Event 'OK' "Pull Request: $prUrl"
|
||||
|
||||
Set-Step 'merge' 'active'
|
||||
Write-State 78 'GitHub-Prüfungen' 'Alle gemeldeten GitHub-Checks werden abgewartet.'
|
||||
$rollup = Invoke-External $gh @('pr','view',$prUrl,'--repo',$Repository,'--json','statusCheckRollup') -Label 'GitHub-Checks ermitteln'
|
||||
$rollupObj = $rollup.Output | ConvertFrom-Json
|
||||
if (@($rollupObj.statusCheckRollup).Count -gt 0) {
|
||||
Invoke-External $gh @('pr','checks',$prUrl,'--repo',$Repository,'--watch','--fail-fast') -WorkingDirectory $CloneDir -TimeoutSeconds 2700 -Label 'GitHub-Checks abwarten' | Out-Null
|
||||
}
|
||||
Write-State 82 'Merge' 'Der vollständig geprüfte Pull Request wird per Squash nach main übernommen.'
|
||||
Invoke-External $gh @('pr','merge',$prUrl,'--repo',$Repository,'--squash','--delete-branch','--subject',"Vendoo $TargetVersion – Production Update") -WorkingDirectory $CloneDir -TimeoutSeconds 300 -Label 'Pull Request mergen' | Out-Null
|
||||
$session.merged = $true
|
||||
$session.stage = 'merged'
|
||||
Save-Session $session
|
||||
Complete-Step 'merge'
|
||||
Write-Event 'OK' 'main wurde kontrolliert aktualisiert.'
|
||||
}
|
||||
else {
|
||||
throw "Git-Diff-Prüfung lieferte unerwarteten Exit-Code $($diff.Code)."
|
||||
}
|
||||
}
|
||||
|
||||
if($SkipProductionDeploy){foreach($id in @('deploy','verify')){foreach($s in $script:Steps){if($s.id-eq$id){$s.state='skipped'}}};Write-State 100 'Source aktualisiert' 'GitHub wurde aktualisiert; das Produktionsdeployment wurde auf Wunsch übersprungen.' 'success';return}
|
||||
Set-Step 'deploy' 'active';Write-State 87 'Coolify Auto Deploy' 'main wurde aktualisiert. Coolify übernimmt das Deployment über die bestehende GitHub-Verknüpfung.'
|
||||
$session.stage='deploy';Save-Session $session;Write-Event 'INFO' 'Warte auf Coolify Auto Deploy. Es wird kein Webhook und kein GitHub Secret benötigt.'
|
||||
$deadline=(Get-Date).AddMinutes(15);$last='';$nextLog=Get-Date
|
||||
do{
|
||||
Assert-NotCancelled
|
||||
$prod=Check-ProductionVersion
|
||||
$last="ready=$($prod.Ready), version=$($prod.Version)"
|
||||
if((Get-Date)-ge$nextLog){Write-Event 'INFO' "Coolify Auto Deploy: $last";$nextLog=(Get-Date).AddSeconds(30)}
|
||||
if($prod.Ready -and $prod.Version-eq$TargetVersion){break}
|
||||
Start-Sleep -Seconds 10
|
||||
}while((Get-Date)-lt$deadline)
|
||||
if(-not($prod.Ready -and $prod.Version-eq$TargetVersion)){throw "Coolify hat die Zielversion innerhalb von 15 Minuten nicht bereitgestellt. Letzter Stand: $last. In Coolify bei der Vendoo-Anwendung unter Advanced prüfen, ob Auto Deploy aktiviert ist."}
|
||||
Complete-Step 'deploy'
|
||||
Set-Step 'verify' 'active';Write-State 97 'Produktionsprüfung' 'Die öffentlich erreichbare Readiness und die exakte Zielversion werden abschließend bestätigt.'
|
||||
$prod=Check-ProductionVersion
|
||||
if(-not($prod.Ready -and $prod.Version-eq$TargetVersion)){throw "Die abschließende Produktionsprüfung ist fehlgeschlagen. Stand: ready=$($prod.Ready), version=$($prod.Version)"}
|
||||
Complete-Step 'verify';$session.stage='complete';Save-Session $session;Write-State 100 'Update erfolgreich' "Vendoo $TargetVersion läuft geprüft in Produktion." 'success';Write-Event 'OK' "Vendoo $TargetVersion wurde vollständig aktualisiert.";return
|
||||
}catch [System.OperationCanceledException]{Fail-Step;Write-State 100 'Update abgebrochen' $_.Exception.Message 'cancelled' $_.Exception.Message 'Der gespeicherte Wiederaufnahmepunkt bleibt erhalten. Mit „Erneut versuchen“ kann der sichere Ablauf fortgesetzt werden.';Write-Event 'WARN' $_.Exception.Message;return
|
||||
}catch{Fail-Step;$msg=$_.Exception.Message;$remediation=if ($script:CurrentStep -eq 'tools'){'Systemvoraussetzung korrigieren und „Erneut versuchen“ wählen. Es wurden noch keine GitHub- oder Produktionsänderungen vorgenommen.'}elseif ($script:CurrentStep -in @('deploy','verify')){'main kann bereits aktualisiert sein. „Erneut versuchen“ setzt beim Deployment beziehungsweise der Produktionsprüfung fort.'}else{'„Erneut versuchen“ verwendet den gespeicherten Stand. Vor einem erfolgreichen Merge bleibt main unverändert.'};Write-State 100 'Update sicher gestoppt' $msg 'failed' $msg $remediation;Write-Event 'ERROR' $msg;return
|
||||
}finally{if(Test-Path $InputFile){Remove-Item $InputFile -Force -ErrorAction SilentlyContinue}}
|
||||
+2
-27
@@ -153,12 +153,7 @@ configureAiModule({
|
||||
stopBatchJobs: stopImageBatchJobLoop,
|
||||
});
|
||||
configureFluxStudioModule({ start: startFluxPromptJobLoop, stop: stopFluxPromptJobLoop });
|
||||
configureDeploymentsModule({
|
||||
createBackup,
|
||||
verifyStoredBackup,
|
||||
checkForUpdates,
|
||||
getPlatformSnapshot: () => platformKernel.snapshot(),
|
||||
});
|
||||
configureDeploymentsModule({ createBackup, checkForUpdates });
|
||||
configurePublishingModule({
|
||||
async start() {
|
||||
const migration = migrateLegacyEbayQueue();
|
||||
@@ -614,8 +609,7 @@ function moduleOwnerForApiPath(pathname) {
|
||||
if (path.startsWith('/publishing') || path.startsWith('/publish') || path.startsWith('/ebay') || path.startsWith('/etsy') || path.startsWith('/extension/publishing') || path.startsWith('/schedule')) return 'vendoo.publishing';
|
||||
if (path.startsWith('/flux') || path.startsWith('/local-image')) return 'vendoo.flux-studio';
|
||||
if (path.startsWith('/quality')) return 'vendoo.quality';
|
||||
if (path.startsWith('/ai-model-photos') || /^\/listings\/[^/]+\/ai-model/.test(path)) return 'vendoo.product-image-tools';
|
||||
if (path.startsWith('/image-batch') || path === '/generate' || path.startsWith('/providers') || path.startsWith('/prompt-lab')) return 'vendoo.ai';
|
||||
if (path.startsWith('/ai-model') || path.startsWith('/image-batch') || path === '/generate' || path.startsWith('/providers') || path.startsWith('/prompt-lab') || /^\/listings\/[^/]+\/ai-model/.test(path)) return 'vendoo.ai';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -652,25 +646,6 @@ platformKernel.routes.register(app, {
|
||||
handler: async (_req, res) => res.json({ modules: platformKernel.modules.snapshot() }),
|
||||
});
|
||||
|
||||
platformKernel.routes.register(app, {
|
||||
id: 'platform.modules.navigation',
|
||||
method: 'GET',
|
||||
path: '/api/modules/navigation',
|
||||
owner: 'vendoo.system',
|
||||
policy: 'platform.authenticated',
|
||||
audit: 'platform.modules.navigation',
|
||||
rateLimit: 'default',
|
||||
csrf: false,
|
||||
stability: 'stable',
|
||||
handler: async (_req, res) => res.json({
|
||||
modules: platformKernel.modules.snapshot().map(module => ({
|
||||
id: module.id,
|
||||
enabled: Boolean(module.enabled && module.state === 'running'),
|
||||
state: module.state,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
platformKernel.routes.register(app, {
|
||||
id: 'themes.contract.read',
|
||||
method: 'GET',
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
# Vendoo 1.42.0 – grafischer Zero-Edit-Installationsassistent
|
||||
# Vendoo 1.41.2 – grafischer Zero-Edit-Installationsassistent
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$VendooDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$SetupScript = Join-Path $VendooDir 'setup.ps1'
|
||||
@@ -90,7 +90,7 @@ function Set-Configuration([string]$Target, [string]$Access, [int]$Port, [string
|
||||
}
|
||||
|
||||
$form = New-Object System.Windows.Forms.Form
|
||||
$form.Text = 'Vendoo 1.42.0 – Installation'
|
||||
$form.Text = 'Vendoo 1.41.2 – Installation'
|
||||
$form.StartPosition = 'CenterScreen'
|
||||
$form.Size = New-Object System.Drawing.Size(720, 620)
|
||||
$form.MinimumSize = New-Object System.Drawing.Size(720, 620)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$VendooDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$Version = '1.42.0'
|
||||
$Version = '1.41.2'
|
||||
$Port = 8124
|
||||
$InstallModeFile = Join-Path $VendooDir '.vendoo-install-mode'
|
||||
|
||||
@@ -831,7 +831,7 @@ function Start-VendooLocal {
|
||||
Write-Host ''
|
||||
Write-Step '...' 'Starte Vendoo-Server...'
|
||||
Push-Location $VendooDir
|
||||
Start-Process "http://localhost:$Port/?build=1.42.0"
|
||||
Start-Process "http://localhost:$Port/?build=1.41.2"
|
||||
& node bootstrap.mjs
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
APP_VERSION="1.42.0"
|
||||
APP_VERSION="1.41.2"
|
||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
MODE_FILE="$ROOT_DIR/.vendoo-install-mode"
|
||||
ENV_FILE="$ROOT_DIR/.env"
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { applyReleaseManifest } from './release-manifest-lib.mjs';
|
||||
|
||||
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
|
||||
if (invokedPath === fileURLToPath(import.meta.url)) {
|
||||
const [, , source, destination] = process.argv;
|
||||
if (!source || !destination) {
|
||||
console.error('Aufruf: node tools/apply-release-manifest.mjs <Release-Paket> <geklontes Zielrepository>');
|
||||
process.exit(64);
|
||||
}
|
||||
try {
|
||||
const result = applyReleaseManifest(source, destination);
|
||||
console.log(`Release-Manifest angewendet: ${result.copied} Payload-Dateien und ${result.controlCopied} Steuerdatei geprüft und kopiert; ${result.removed} explizite Löschungen.`);
|
||||
} catch (error) {
|
||||
console.error(`[FEHLER] Release-Manifest konnte nicht angewendet werden: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
+27
-30
@@ -1,46 +1,43 @@
|
||||
import {
|
||||
copyFileSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { cpSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { dirname, join, relative, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { generateReleaseManifest } from './generate-release-manifest.mjs';
|
||||
import { MANIFEST_FILE, readAndValidateManifest } from './release-manifest-lib.mjs';
|
||||
import { shouldSkipStagingEntry } from './prepare-git-staging.mjs';
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
||||
const version = pkg.version;
|
||||
const outputRoot = join(root, 'release-output');
|
||||
const packageName = `Vendoo_${pkg.version}`;
|
||||
const packageName = `Vendoo_${version}`;
|
||||
const stage = join(outputRoot, packageName);
|
||||
const normalize = value => value.split(sep).join('/');
|
||||
|
||||
// The committed manifest is the only release payload contract. Regenerate it first so
|
||||
// stale hashes or accidentally omitted source files make the release fail immediately.
|
||||
generateReleaseManifest(root);
|
||||
const manifest = readAndValidateManifest(root);
|
||||
|
||||
rmSync(outputRoot, { recursive: true, force: true });
|
||||
mkdirSync(stage, { recursive: true });
|
||||
for (const entry of manifest.files) {
|
||||
const source = join(root, entry.path);
|
||||
const target = join(stage, entry.path);
|
||||
mkdirSync(dirname(target), { recursive: true });
|
||||
copyFileSync(source, target);
|
||||
if (statSync(target).size !== entry.size) throw new Error(`Release-Kopie hat falsche Größe: ${entry.path}`);
|
||||
|
||||
function copyTree(src, dest, rel = '') {
|
||||
for (const name of readdirSync(src)) {
|
||||
const source = join(src, name);
|
||||
const childRel = rel ? join(rel, name) : name;
|
||||
const stat = lstatSync(source);
|
||||
if (shouldSkipStagingEntry(childRel, stat)) continue;
|
||||
const target = join(dest, name);
|
||||
if (stat.isDirectory()) {
|
||||
mkdirSync(target, { recursive: true });
|
||||
copyTree(source, target, childRel);
|
||||
} else if (stat.isFile()) {
|
||||
mkdirSync(dirname(target), { recursive: true });
|
||||
cpSync(source, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
copyFileSync(join(root, MANIFEST_FILE), join(stage, MANIFEST_FILE));
|
||||
copyTree(root, stage);
|
||||
|
||||
const checksums = [];
|
||||
function hashTree(dir) {
|
||||
for (const name of readdirSync(dir).sort((a, b) => a.localeCompare(b, 'en'))) {
|
||||
for (const name of readdirSync(dir).sort((a, b) => a.localeCompare(b))) {
|
||||
const abs = join(dir, name);
|
||||
const stat = statSync(abs);
|
||||
const stat = lstatSync(abs);
|
||||
if (stat.isDirectory()) hashTree(abs);
|
||||
else {
|
||||
const rel = normalize(relative(stage, abs));
|
||||
@@ -51,7 +48,7 @@ function hashTree(dir) {
|
||||
}
|
||||
}
|
||||
hashTree(stage);
|
||||
writeFileSync(join(stage, 'FILE_CHECKSUMS_SHA256.txt'), `${checksums.join('\n')}\n`, 'utf8');
|
||||
writeFileSync(join(outputRoot, 'RELEASE_NAME.txt'), `${packageName}\n`, 'utf8');
|
||||
console.log(`Manifestbasiertes Release-Staging erstellt: ${stage}`);
|
||||
console.log(`${manifest.files.length} Payload-Dateien und ${checksums.length} Paketdateien mit SHA-256 erfasst.`);
|
||||
writeFileSync(join(stage, 'FILE_CHECKSUMS_SHA256.txt'), checksums.join('\n') + '\n', 'utf8');
|
||||
writeFileSync(join(outputRoot, 'RELEASE_NAME.txt'), packageName + '\n', 'utf8');
|
||||
console.log(`Release-Staging erstellt: ${stage}`);
|
||||
console.log(`${checksums.length} Dateien mit SHA-256 erfasst.`);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { buildReleaseManifest, MANIFEST_FILE } from './release-manifest-lib.mjs';
|
||||
|
||||
export function generateReleaseManifest(root) {
|
||||
const base = resolve(root);
|
||||
const pkg = JSON.parse(readFileSync(join(base, 'package.json'), 'utf8'));
|
||||
const update = JSON.parse(readFileSync(join(base, 'update-manifest.json'), 'utf8'));
|
||||
const manifest = buildReleaseManifest(base, {
|
||||
version: pkg.version,
|
||||
updaterVersion: update.updaterVersion || update.updater_version,
|
||||
removedFiles: Array.isArray(update.removedFiles) ? update.removedFiles : [],
|
||||
});
|
||||
writeFileSync(join(base, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
||||
return manifest;
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
|
||||
if (invokedPath === fileURLToPath(import.meta.url)) {
|
||||
const root = process.argv[2] || process.cwd();
|
||||
try {
|
||||
const manifest = generateReleaseManifest(root);
|
||||
console.log(`Release-Manifest erstellt: ${manifest.files.length} Dateien, ${manifest.removedFiles.length} explizite Löschungen.`);
|
||||
} catch (error) {
|
||||
console.error(`[FEHLER] Release-Manifest konnte nicht erstellt werden: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
import {
|
||||
chmodSync,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
} from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
||||
|
||||
export const MANIFEST_SCHEMA = 'vendoo-release-manifest-v1';
|
||||
export const MANIFEST_FILE = 'release-manifest.json';
|
||||
export const CONTROL_RELEASE_FILES = Object.freeze([MANIFEST_FILE]);
|
||||
export const REQUIRED_RELEASE_FILES = Object.freeze([
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
'bootstrap.mjs',
|
||||
'server.mjs',
|
||||
'lib/db.mjs',
|
||||
'db/schema.sql',
|
||||
'update-manifest.json',
|
||||
'UPDATE_VENDOO.bat',
|
||||
'scripts/updater/Vendoo.Updater.Preflight.ps1',
|
||||
'scripts/updater/Vendoo.Updater.UI.ps1',
|
||||
'scripts/updater/Vendoo.Updater.Host.ps1',
|
||||
'scripts/updater/Vendoo.Updater.Worker.ps1',
|
||||
'tools/apply-release-manifest.mjs',
|
||||
]);
|
||||
|
||||
const allowedEnvTemplates = new Set(['.env.example', '.env.docker.example']);
|
||||
const skippedTopLevelDirs = new Set([
|
||||
'.git', 'node_modules', 'runtime', 'docker-data', 'uploads', 'backups', 'logs',
|
||||
'sessions', 'tmp', 'temp', '__pycache__', 'release-output', 'operations', 'updates',
|
||||
'modules', 'archiv', 'archive', 'archives', 'data',
|
||||
]);
|
||||
const skippedLocalAiDirs = new Set(['local-ai/comfyui', 'local-ai/downloads', 'local-ai/extract-temp']);
|
||||
const skippedExactFiles = new Set([
|
||||
MANIFEST_FILE,
|
||||
'file_checksums_sha256.txt',
|
||||
'.vendoo-install-mode', '.vendoo-github-state.json',
|
||||
'pending-operation.json', 'last-operation.json', 'first-admin-code.txt',
|
||||
]);
|
||||
const skippedExtensions = new Set([
|
||||
'.db', '.sqlite', '.sqlite3', '.log', '.key', '.pem', '.pfx', '.p12', '.pyc',
|
||||
]);
|
||||
|
||||
export function normalizeReleasePath(value) {
|
||||
return String(value).split('\\').join('/').replace(/^\.\//, '');
|
||||
}
|
||||
|
||||
export function assertSafeReleasePath(value, label = 'Release-Pfad') {
|
||||
const rel = normalizeReleasePath(value);
|
||||
if (!rel || rel === '.' || isAbsolute(rel) || rel.startsWith('/') || /^[A-Za-z]:\//.test(rel)) {
|
||||
throw new Error(`${label} ist nicht relativ: ${value}`);
|
||||
}
|
||||
const parts = rel.split('/');
|
||||
if (parts.some(part => !part || part === '.' || part === '..')) {
|
||||
throw new Error(`${label} enthält unzulässige Segmente: ${value}`);
|
||||
}
|
||||
if (parts[0].toLowerCase() === '.git') {
|
||||
throw new Error(`${label} darf .git nicht verändern: ${value}`);
|
||||
}
|
||||
return rel;
|
||||
}
|
||||
|
||||
export function shouldIncludeReleaseEntry(relativePath, stat) {
|
||||
const rel = normalizeReleasePath(relativePath);
|
||||
if (!rel) return false;
|
||||
const parts = rel.split('/');
|
||||
const top = parts[0].toLowerCase();
|
||||
const name = basename(rel);
|
||||
const lower = rel.toLowerCase();
|
||||
if (skippedTopLevelDirs.has(top)) return false;
|
||||
if (parts.some(part => part.toLowerCase() === '__pycache__')) return false;
|
||||
if ([...skippedLocalAiDirs].some(dir => lower === dir || lower.startsWith(`${dir}/`))) return false;
|
||||
if (/^\.env(?:\..+)?$/i.test(name) && !allowedEnvTemplates.has(rel)) return false;
|
||||
if (skippedExactFiles.has(name.toLowerCase())) return false;
|
||||
if (/\.(?:db|sqlite|sqlite3)-(?:wal|shm)$/i.test(name)) return false;
|
||||
if (skippedExtensions.has(extname(name).toLowerCase())) return false;
|
||||
if (stat.isSymbolicLink()) return false;
|
||||
return stat.isDirectory() || stat.isFile();
|
||||
}
|
||||
|
||||
export function sha256File(filePath) {
|
||||
const hash = createHash('sha256');
|
||||
hash.update(readFileSync(filePath));
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
export function listReleaseFiles(root) {
|
||||
const base = resolve(root);
|
||||
const files = [];
|
||||
function walk(current) {
|
||||
for (const name of readdirSync(current).sort((a, b) => a.localeCompare(b, 'en'))) {
|
||||
const absolute = join(current, name);
|
||||
const rel = normalizeReleasePath(relative(base, absolute));
|
||||
const stat = lstatSync(absolute);
|
||||
if (!shouldIncludeReleaseEntry(rel, stat)) continue;
|
||||
if (stat.isDirectory()) walk(absolute);
|
||||
else files.push(rel);
|
||||
}
|
||||
}
|
||||
walk(base);
|
||||
return files;
|
||||
}
|
||||
|
||||
export function buildReleaseManifest(root, { version, updaterVersion, removedFiles = [] } = {}) {
|
||||
if (!version) throw new Error('Release-Version fehlt.');
|
||||
if (!updaterVersion) throw new Error('Updater-Version fehlt.');
|
||||
const base = resolve(root);
|
||||
const files = listReleaseFiles(base).map(path => ({
|
||||
path,
|
||||
sha256: sha256File(join(base, path)),
|
||||
size: statSync(join(base, path)).size,
|
||||
}));
|
||||
const normalizedRemoved = [...new Set(removedFiles.map(path => assertSafeReleasePath(path, 'removedFiles-Eintrag')))].sort();
|
||||
const fileSet = new Set(files.map(entry => entry.path));
|
||||
for (const required of REQUIRED_RELEASE_FILES) {
|
||||
if (!fileSet.has(required)) throw new Error(`Pflichtdatei fehlt im Release-Payload: ${required}`);
|
||||
}
|
||||
for (const removed of normalizedRemoved) {
|
||||
if (fileSet.has(removed)) throw new Error(`Datei ist gleichzeitig Payload und Löschung: ${removed}`);
|
||||
if (CONTROL_RELEASE_FILES.includes(removed)) throw new Error(`Steuerdatei darf nicht als Löschung markiert werden: ${removed}`);
|
||||
}
|
||||
return {
|
||||
schema: MANIFEST_SCHEMA,
|
||||
version,
|
||||
updaterVersion,
|
||||
mode: 'overlay-with-explicit-removals',
|
||||
requiredFiles: [...REQUIRED_RELEASE_FILES],
|
||||
removedFiles: normalizedRemoved,
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
export function readAndValidateManifest(sourceRoot, { allowExtraFiles = false } = {}) {
|
||||
const base = resolve(sourceRoot);
|
||||
const manifestPath = join(base, MANIFEST_FILE);
|
||||
if (!existsSync(manifestPath)) throw new Error(`${MANIFEST_FILE} fehlt.`);
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
||||
if (manifest.schema !== MANIFEST_SCHEMA) throw new Error(`Unbekanntes Manifest-Schema: ${manifest.schema}`);
|
||||
if (manifest.mode !== 'overlay-with-explicit-removals') throw new Error(`Unzulässiger Release-Modus: ${manifest.mode}`);
|
||||
if (!Array.isArray(manifest.files) || manifest.files.length === 0) throw new Error('Release-Manifest enthält keine Dateien.');
|
||||
if (!Array.isArray(manifest.removedFiles)) throw new Error('removedFiles fehlt im Release-Manifest.');
|
||||
const seen = new Set();
|
||||
for (const entry of manifest.files) {
|
||||
if (!entry || typeof entry !== 'object') throw new Error('Ungültiger Datei-Eintrag im Release-Manifest.');
|
||||
const rel = assertSafeReleasePath(entry.path, 'Manifest-Dateipfad');
|
||||
if (seen.has(rel)) throw new Error(`Doppelter Manifest-Dateipfad: ${rel}`);
|
||||
seen.add(rel);
|
||||
if (!/^[a-f0-9]{64}$/.test(String(entry.sha256 || ''))) throw new Error(`Ungültige SHA-256-Prüfsumme: ${rel}`);
|
||||
if (!Number.isSafeInteger(entry.size) || entry.size < 0) throw new Error(`Ungültige Dateigröße: ${rel}`);
|
||||
}
|
||||
const removedSeen = new Set();
|
||||
for (const value of manifest.removedFiles) {
|
||||
const rel = assertSafeReleasePath(value, 'removedFiles-Eintrag');
|
||||
if (removedSeen.has(rel)) throw new Error(`Doppelter removedFiles-Eintrag: ${rel}`);
|
||||
removedSeen.add(rel);
|
||||
if (seen.has(rel)) throw new Error(`Datei ist gleichzeitig Payload und Löschung: ${rel}`);
|
||||
if (CONTROL_RELEASE_FILES.includes(rel)) throw new Error(`Steuerdatei darf nicht als Löschung markiert werden: ${rel}`);
|
||||
}
|
||||
const required = Array.isArray(manifest.requiredFiles) ? manifest.requiredFiles : REQUIRED_RELEASE_FILES;
|
||||
for (const value of REQUIRED_RELEASE_FILES) {
|
||||
if (!required.includes(value)) throw new Error(`Manifest-Pflichtdatei fehlt in requiredFiles: ${value}`);
|
||||
if (!seen.has(value)) throw new Error(`Manifest-Payload enthält Pflichtdatei nicht: ${value}`);
|
||||
}
|
||||
const actualFiles = listReleaseFiles(base);
|
||||
const actualSet = new Set(actualFiles);
|
||||
if (!allowExtraFiles) {
|
||||
for (const rel of actualFiles) if (!seen.has(rel)) throw new Error(`Sichere Paketdatei fehlt im Manifest: ${rel}`);
|
||||
}
|
||||
for (const rel of seen) if (!actualSet.has(rel)) throw new Error(`Manifest verweist auf fehlende Paketdatei: ${rel}`);
|
||||
for (const entry of manifest.files) {
|
||||
const filePath = join(base, entry.path);
|
||||
const stat = statSync(filePath);
|
||||
if (stat.size !== entry.size) throw new Error(`Dateigröße stimmt nicht: ${entry.path}`);
|
||||
const actualHash = sha256File(filePath);
|
||||
if (actualHash !== entry.sha256) throw new Error(`SHA-256 stimmt nicht: ${entry.path}`);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
export function applyReleaseManifest(sourceRoot, destinationRoot) {
|
||||
const source = resolve(sourceRoot);
|
||||
const destination = resolve(destinationRoot);
|
||||
if (source === destination) throw new Error('Quell- und Zielordner dürfen nicht identisch sein.');
|
||||
if (!existsSync(join(destination, '.git'))) throw new Error(`Ziel ist kein geklontes Git-Repository: ${destination}`);
|
||||
const manifest = readAndValidateManifest(source);
|
||||
for (const rel of manifest.removedFiles) {
|
||||
rmSync(join(destination, rel), { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
}
|
||||
let copied = 0;
|
||||
for (const entry of manifest.files) {
|
||||
const sourceFile = join(source, entry.path);
|
||||
const targetFile = join(destination, entry.path);
|
||||
mkdirSync(dirname(targetFile), { recursive: true });
|
||||
copyFileSync(sourceFile, targetFile);
|
||||
try { chmodSync(targetFile, statSync(sourceFile).mode & 0o777); } catch {}
|
||||
const targetHash = sha256File(targetFile);
|
||||
if (targetHash !== entry.sha256) throw new Error(`Zielprüfung nach Kopieren fehlgeschlagen: ${entry.path}`);
|
||||
copied += 1;
|
||||
}
|
||||
let controlCopied = 0;
|
||||
for (const rel of CONTROL_RELEASE_FILES) {
|
||||
const sourceFile = join(source, rel);
|
||||
const targetFile = join(destination, rel);
|
||||
mkdirSync(dirname(targetFile), { recursive: true });
|
||||
copyFileSync(sourceFile, targetFile);
|
||||
if (sha256File(sourceFile) !== sha256File(targetFile)) {
|
||||
throw new Error(`Steuerdatei-Prüfung nach Kopieren fehlgeschlagen: ${rel}`);
|
||||
}
|
||||
controlCopied += 1;
|
||||
}
|
||||
for (const required of REQUIRED_RELEASE_FILES) {
|
||||
if (!existsSync(join(destination, required))) throw new Error(`Pflichtdatei fehlt nach Release-Anwendung: ${required}`);
|
||||
}
|
||||
for (const controlFile of CONTROL_RELEASE_FILES) {
|
||||
if (!existsSync(join(destination, controlFile))) throw new Error(`Steuerdatei fehlt nach Release-Anwendung: ${controlFile}`);
|
||||
}
|
||||
return {
|
||||
source,
|
||||
destination,
|
||||
version: manifest.version,
|
||||
updaterVersion: manifest.updaterVersion,
|
||||
copied,
|
||||
controlCopied,
|
||||
removed: manifest.removedFiles.length,
|
||||
};
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { rmSync } from 'node:fs';
|
||||
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export async function closeVendooDatabase({ failures = [], label = 'Temporäre SQLite-Datenbank' } = {}) {
|
||||
try {
|
||||
const { db } = await import('../lib/db.mjs');
|
||||
if (db?.open) {
|
||||
try { db.pragma('wal_checkpoint(TRUNCATE)'); } catch {}
|
||||
db.close();
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
failures.push(`${label} konnte nicht sauber geschlossen werden: ${error.code || error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeTempDirectory(path, {
|
||||
failures = [],
|
||||
label = 'Temporärer Testordner',
|
||||
settleMs = 100,
|
||||
maxRetries = 30,
|
||||
retryDelay = 100,
|
||||
} = {}) {
|
||||
if (settleMs > 0) await wait(settleMs);
|
||||
try {
|
||||
rmSync(path, { recursive: true, force: true, maxRetries, retryDelay });
|
||||
return true;
|
||||
} catch (error) {
|
||||
failures.push(`${label} konnte nicht entfernt werden: ${error.code || error.message} (${path})`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,38 @@
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { closeVendooDatabase, removeTempDirectory } from './test-runtime-cleanup.mjs';
|
||||
|
||||
const tempPrefix = process.env.VENDOO_TEST_TEMP_PREFIX || 'vendoo-capability-graph-';
|
||||
const temp = mkdtempSync(join(tmpdir(), tempPrefix));
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.VENDOO_DATA_DIR = temp;
|
||||
process.env.VENDOO_DB_PATH = join(temp, 'db', 'vendoo.db');
|
||||
process.env.VENDOO_CONFIG_PATH = join(temp, 'config', 'vendoo.env');
|
||||
process.env.VENDOO_MODULE_STATE_PATH = join(temp, 'config', 'modules-state.json');
|
||||
process.env.VENDOO_MODULES_DIR = join(temp, 'modules');
|
||||
import { ModuleRegistry } from '../app/kernel/module-registry.mjs';
|
||||
import { loadBuiltInModules } from '../app/modules/catalog.mjs';
|
||||
|
||||
const failures = [];
|
||||
const assert = (condition, message) => { if (!condition) failures.push(message); };
|
||||
|
||||
const modules = loadBuiltInModules();
|
||||
const registry = new ModuleRegistry();
|
||||
for (const module of modules) registry.register(module);
|
||||
|
||||
let graph = null;
|
||||
|
||||
try {
|
||||
const { ModuleRegistry } = await import('../app/kernel/module-registry.mjs');
|
||||
const { loadBuiltInModules } = await import('../app/modules/catalog.mjs');
|
||||
const modules = loadBuiltInModules();
|
||||
const registry = new ModuleRegistry();
|
||||
for (const module of modules) registry.register(module);
|
||||
|
||||
try {
|
||||
graph = registry.validateGraph();
|
||||
} catch (error) {
|
||||
failures.push(`${error.code || 'CAPABILITY_GRAPH_INVALID'}: ${error.message}`);
|
||||
}
|
||||
|
||||
const settings = modules.find(item => item.manifest.id === 'vendoo.settings')?.manifest;
|
||||
const manager = modules.find(item => item.manifest.id === 'vendoo.module-manager')?.manifest;
|
||||
const deployments = modules.find(item => item.manifest.id === 'vendoo.deployments')?.manifest;
|
||||
assert(settings?.provides.includes('settings.configuration'), 'vendoo.settings stellt settings.configuration nicht bereit');
|
||||
assert(manager?.consumes.includes('settings.configuration'), 'vendoo.module-manager fordert settings.configuration nicht an');
|
||||
assert(graph?.enabledModules.length === 18, `Erwartet 18 registrierte Module, erhalten ${graph?.enabledModules.length ?? 0}`);
|
||||
assert(deployments?.consumes.includes('settings.configuration'), 'vendoo.deployments fordert settings.configuration nicht an');
|
||||
assert(deployments?.consumes.includes('operations.backup'), 'vendoo.deployments fordert operations.backup nicht an');
|
||||
assert(graph?.capabilities.includes('deployments.coolify'), 'Capability deployments.coolify fehlt im aufgelösten Gesamtgraph');
|
||||
assert(graph?.capabilities.includes('settings.configuration'), 'Capability settings.configuration fehlt im aufgelösten Gesamtgraph');
|
||||
assert(graph?.capabilities.includes('product-images.variants'), 'Capability product-images.variants fehlt im aufgelösten Gesamtgraph');
|
||||
assert(graph?.order.indexOf('vendoo.settings') < graph?.order.indexOf('vendoo.module-manager'), 'vendoo.settings wird nicht vor vendoo.module-manager gestartet');
|
||||
assert(graph?.order.indexOf('vendoo.module-manager') < graph?.order.indexOf('vendoo.deployments'), 'vendoo.module-manager wird nicht vor vendoo.deployments gestartet');
|
||||
} finally {
|
||||
await closeVendooDatabase({ failures, label: 'Capability-Graph-Testdatenbank' });
|
||||
await removeTempDirectory(temp, { failures, label: 'Capability-Graph-Testordner' });
|
||||
graph = registry.validateGraph();
|
||||
} catch (error) {
|
||||
failures.push(`${error.code || 'CAPABILITY_GRAPH_INVALID'}: ${error.message}`);
|
||||
}
|
||||
|
||||
const settings = modules.find(item => item.manifest.id === 'vendoo.settings')?.manifest;
|
||||
const manager = modules.find(item => item.manifest.id === 'vendoo.module-manager')?.manifest;
|
||||
const deployments = modules.find(item => item.manifest.id === 'vendoo.deployments')?.manifest;
|
||||
assert(settings?.provides.includes('settings.configuration'), 'vendoo.settings stellt settings.configuration nicht bereit');
|
||||
assert(manager?.consumes.includes('settings.configuration'), 'vendoo.module-manager fordert settings.configuration nicht an');
|
||||
assert(graph?.enabledModules.length === 17, `Erwartet 17 aktivierbare Module, erhalten ${graph?.enabledModules.length ?? 0}`);
|
||||
assert(deployments?.consumes.includes('settings.configuration'), 'vendoo.deployments fordert settings.configuration nicht an');
|
||||
assert(deployments?.consumes.includes('operations.backup'), 'vendoo.deployments fordert operations.backup nicht an');
|
||||
assert(graph?.capabilities.includes('deployments.coolify'), 'Capability deployments.coolify fehlt im aufgelösten Gesamtgraph');
|
||||
assert(graph?.capabilities.includes('settings.configuration'), 'Capability settings.configuration fehlt im aufgelösten Gesamtgraph');
|
||||
assert(graph?.order.indexOf('vendoo.settings') < graph?.order.indexOf('vendoo.module-manager'), 'vendoo.settings wird nicht vor vendoo.module-manager gestartet');
|
||||
|
||||
assert(graph?.order.indexOf('vendoo.module-manager') < graph?.order.indexOf('vendoo.deployments'), 'vendoo.module-manager wird nicht vor vendoo.deployments gestartet');
|
||||
|
||||
if (failures.length) {
|
||||
console.error(`Capability-Graph-Gate 1.41.2 fehlgeschlagen (${failures.length}):`);
|
||||
failures.forEach(item => console.error(`- ${item}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Capability-Graph-Gate 1.41.2 erfolgreich: ${graph.enabledModules.length} Module, ${graph.capabilities.length} Capabilities und vollständige Provider-Verträge geprüft.`);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFileSync } from 'node:fs';import { dirname,join,resolve } from 'node:path';import { fileURLToPath } from 'node:url';
|
||||
const root=resolve(dirname(fileURLToPath(import.meta.url)),'..');const read=r=>readFileSync(join(root,r),'utf8');const failures=[];const assert=(c,m)=>{if(!c)failures.push(m)};
|
||||
const pkg=JSON.parse(read('package.json'));assert(pkg.version==='1.43.0',`Paketversion ist ${pkg.version}`);const server=read('server.mjs');const catalog=read('app/modules/catalog.mjs');const kernel=read('app/kernel/platform-kernel.mjs');const frontend=read('public/app.js');assert(!frontend.includes('...state.currentListing,\n ...payload'),'Frontend sendet unkontrolliertes Listing-Objekt an die native Allowlist');
|
||||
const pkg=JSON.parse(read('package.json'));assert(pkg.version==='1.41.2',`Paketversion ist ${pkg.version}`);const server=read('server.mjs');const catalog=read('app/modules/catalog.mjs');const kernel=read('app/kernel/platform-kernel.mjs');const frontend=read('public/app.js');assert(!frontend.includes('...state.currentListing,\n ...payload'),'Frontend sendet unkontrolliertes Listing-Objekt an die native Allowlist');
|
||||
for(const marker of ['registerListingsRoutes','registerInventoryRoutes','registerMediaRoutes','configureMediaModule'])assert(server.includes(marker),`Serverregistrierung fehlt: ${marker}`);
|
||||
for(const legacy of ["app.post('/api/listings'","app.get('/api/listings'","app.get('/api/media-library'","app.get('/api/inventory/locations'","app.get('/api/trash'"])assert(!server.includes(legacy),`Legacy-Route verblieben: ${legacy}`);
|
||||
for(const id of ['listings','inventory','media']){const manifest=JSON.parse(read(`app/modules/${id}/module.json`));assert(manifest.status==='native',`${id} ist nicht nativ`);assert(read(`app/modules/${id}/routes.mjs`).includes("owner:'vendoo."+id)||read(`app/modules/${id}/routes.mjs`).includes("owner: 'vendoo."+id),`${id}-Routenbesitzer fehlt`)}
|
||||
@@ -8,4 +8,4 @@ for(const marker of ['createListingsModule','createInventoryModule','createMedia
|
||||
const listingService=read('app/modules/listings/service.mjs'),listingRoutes=read('app/modules/listings/routes.mjs'),inventoryService=read('app/modules/inventory/service.mjs'),mediaService=read('app/modules/media/service.mjs');
|
||||
for(const marker of ['ALLOWED_FIELDS','Unbekanntes Artikelfeld','LISTING_TRASH_NOT_FOUND','sanitizeRichHtml'])assert(listingService.includes(marker),`Listing-Schutz fehlt: ${marker}`);for(const marker of ['validateResourceLock','listings.permanent-delete','listing.soft_deleted'])assert(listingRoutes.includes(marker),`Listing-Route fehlt: ${marker}`);for(const marker of ['Maximal 500 Artikel','Unbekanntes Lagerfeld','INVENTORY_LOCATION_INVALID'])assert(inventoryService.includes(marker),`Inventory-Schutz fehlt: ${marker}`);for(const marker of ['normalizePath','p.includes(\'..\')','referenced','protectedPaths','validateFavorite','validateDelete'])assert(mediaService.includes(marker),`Media-Schutz fehlt: ${marker}`);
|
||||
for(const marker of ["policies.register('listings.permanent-delete'","policies.register('inventory.manage'","policies.register('media.delete'"])assert(kernel.includes(marker),`Policy fehlt: ${marker}`);
|
||||
if(failures.length){console.error(`Catalog-&-Media-Gate 1.43.0 fehlgeschlagen (${failures.length}):`);failures.forEach(x=>console.error(`- ${x}`));process.exit(1)}console.log('Catalog-&-Media-Gate 1.43.0 erfolgreich: native Listings-, Inventory- und Media-Routen, Payload-Allowlist, Locking, Papierkorbgrenzen, Bulk-Limits und Medien-Referenzschutz geprüft.');
|
||||
if(failures.length){console.error(`Catalog-&-Media-Gate 1.41.2 fehlgeschlagen (${failures.length}):`);failures.forEach(x=>console.error(`- ${x}`));process.exit(1)}console.log('Catalog-&-Media-Gate 1.41.2 erfolgreich: native Listings-, Inventory- und Media-Routen, Payload-Allowlist, Locking, Papierkorbgrenzen, Bulk-Limits und Medien-Referenzschutz geprüft.');
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { closeVendooDatabase, removeTempDirectory } from './test-runtime-cleanup.mjs';
|
||||
|
||||
const tempPrefix = process.env.VENDOO_TEST_TEMP_PREFIX || 'vendoo-invitations-';
|
||||
const temp = mkdtempSync(join(tmpdir(), tempPrefix));
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.VENDOO_DATA_DIR = temp;
|
||||
process.env.VENDOO_DB_PATH = join(temp, 'db', 'vendoo.db');
|
||||
process.env.VENDOO_CONFIG_PATH = join(temp, 'config', 'vendoo.env');
|
||||
|
||||
const failures = [];
|
||||
const assert = (condition, message) => { if (!condition) failures.push(message); };
|
||||
try {
|
||||
const identity = await import('../app/core/identity/identity-store.mjs');
|
||||
const { createUsersService } = await import('../app/modules/users/service.mjs');
|
||||
const first = identity.createAuthToken('tester@example.invalid', 'invite', 'viewer', null);
|
||||
assert(first.token && first.expiresAt, 'Einladungstoken oder Ablauf fehlt');
|
||||
let invitations = identity.listInvitations();
|
||||
assert(invitations.length === 1, `Erwartet eine Einladung, erhalten ${invitations.length}`);
|
||||
assert(invitations[0].status === 'pending', `Neue Einladung ist ${invitations[0].status}`);
|
||||
assert(!Object.hasOwn(invitations[0], 'token_hash'), 'Token-Hash wird in der Einladungsliste ausgegeben');
|
||||
assert(invitations[0].role === 'viewer', `Minimale Testerrolle ist ${invitations[0].role}`);
|
||||
|
||||
const second = identity.createAuthToken('tester@example.invalid', 'invite', 'viewer', null);
|
||||
invitations = identity.listInvitations();
|
||||
assert(invitations.filter(item => item.status === 'pending').length === 1, 'Erneutes Senden lässt mehrere gültige Einladungen offen');
|
||||
const pending = invitations.find(item => item.status === 'pending');
|
||||
const revoked = identity.revokeInvitation(pending.id);
|
||||
assert(revoked?.status === 'revoked', 'Einladung wurde nicht widerrufen');
|
||||
assert(identity.validateAuthToken(second.token) === null, 'Widerrufener Link bleibt gültig');
|
||||
|
||||
const service = createUsersService();
|
||||
const prepared = service.prepareInvite({ email: 'minimal@example.invalid', name: 'Minimal', createdBy: null, method: 'magic_link' });
|
||||
assert(prepared.role === 'viewer', `Service-Standardrolle ist ${prepared.role}`);
|
||||
|
||||
const routes = readFileSync(new URL('../app/modules/users/routes.mjs', import.meta.url), 'utf8');
|
||||
for (const marker of ['/api/admin/users/invitations', '/api/admin/users/invitations/:id/resend', "'user.invitation_revoked'", "policy: 'users.manage'", 'csrf: true']) {
|
||||
assert(routes.includes(marker), `Einladungs-Routenvertrag fehlt: ${marker}`);
|
||||
}
|
||||
const frontend = readFileSync(new URL('../public/app.js', import.meta.url), 'utf8');
|
||||
assert(frontend.includes('loadAdminInvitations'), 'Einladungsübersicht fehlt im Frontend');
|
||||
assert(frontend.includes("invite-role').value = 'viewer'"), 'Einladungsdialog startet nicht mit Leserrolle');
|
||||
} finally {
|
||||
await closeVendooDatabase({ failures, label: 'Einladungs-Testdatenbank' });
|
||||
await removeTempDirectory(temp, { failures, label: 'Einladungs-Testordner' });
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error(`Controlled-Invitations-Gate 1.42.0 fehlgeschlagen (${failures.length}):`);
|
||||
failures.forEach(failure => console.error(`- ${failure}`));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Controlled-Invitations-Gate 1.42.0 erfolgreich: Leser-Standardrolle, 48h-Ablauf, Einmalgültigkeit, erneutes Senden, Widerruf, Secret-Grenze, RBAC und UI geprüft.');
|
||||
process.exit(0);
|
||||
@@ -1,139 +0,0 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
const APP = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
||||
const BASE = process.argv[2] ? resolve(process.argv[2]) : null;
|
||||
|
||||
function execDbExecBlocks(db, sourcePath) {
|
||||
const text = readFileSync(sourcePath, 'utf8');
|
||||
const blocks = [...text.matchAll(/db\.exec\(`([\s\S]*?)`\)/g)];
|
||||
let applied = 0;
|
||||
for (const [, sql] of blocks) {
|
||||
if (sql.includes('${')) continue;
|
||||
try { db.exec(sql); applied += 1; } catch { /* additive ALTER blocks intentionally tolerate existing columns */ }
|
||||
}
|
||||
if (text.includes('operations-center-1.30.0')) {
|
||||
db.prepare("INSERT OR IGNORE INTO system_migrations (migration_key, app_version, description) VALUES ('operations-center-1.30.0','1.30.0','Backup, Restore, Update Center und Extension-Diagnose')").run();
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
function columns(db, table) {
|
||||
return new Set(db.prepare(`PRAGMA table_info(${JSON.stringify(table)})`).all().map((row) => row.name));
|
||||
}
|
||||
|
||||
function verify(db) {
|
||||
const requiredTables = new Set([
|
||||
'flux_images', 'flux_prompt_jobs', 'flux_prompt_job_variants', 'image_edit_versions',
|
||||
'image_batch_profiles', 'image_batch_jobs', 'image_batch_job_items', 'quality_reports',
|
||||
'publishing_jobs', 'publishing_events', 'operation_backups', 'operation_settings',
|
||||
'update_operations', 'system_migrations', 'extension_clients', 'users', 'sessions',
|
||||
'audit_log', 'resource_locks', 'theme_profiles', 'user_theme_preferences',
|
||||
]);
|
||||
const existingTables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((row) => row.name));
|
||||
const missingTables = [...requiredTables].filter((name) => !existingTables.has(name));
|
||||
const checks = {
|
||||
flux_images: ['favorite', 'job_id', 'variant_index', 'prompt_mode', 'parameters_json'],
|
||||
flux_prompt_jobs: ['archived_at'],
|
||||
image_edit_versions: ['root_path', 'source_path', 'output_path', 'context', 'operations_json', 'width', 'height', 'format', 'quality', 'file_size', 'user_id', 'created_at'],
|
||||
image_batch_jobs: ['name', 'status', 'phase', 'source_count', 'profile_count', 'total_items', 'completed_items', 'failed_items', 'cancelled_items', 'pause_requested', 'cancel_requested', 'settings_json', 'created_at', 'updated_at'],
|
||||
image_batch_job_items: ['job_id', 'source_path', 'profile_id', 'profile_key', 'profile_name', 'status', 'phase', 'settings_json', 'output_path', 'error_message', 'created_at', 'updated_at'],
|
||||
quality_reports: ['listing_id', 'score', 'grade', 'ready', 'blockers', 'warnings', 'recommendations', 'report_json', 'analyzed_at'],
|
||||
publishing_jobs: ['id', 'listing_id', 'platform', 'mode', 'action', 'status', 'phase', 'idempotency_key', 'external_id', 'external_url', 'offer_id', 'retries', 'max_retries', 'next_attempt_at', 'claimed_by', 'claimed_at', 'error_code', 'error_message', 'payload_json', 'result_json', 'created_at', 'started_at', 'completed_at', 'updated_at'],
|
||||
publishing_events: ['id', 'job_id', 'listing_id', 'platform', 'level', 'event_type', 'message', 'details_json', 'created_at'],
|
||||
publish_log: ['offer_id', 'sync_status', 'last_synced_at', 'ended_at', 'updated_at'],
|
||||
operation_backups: ['id', 'kind', 'label', 'file_name', 'file_path', 'file_size', 'sha256', 'status', 'verified_at', 'options_json', 'manifest_json', 'created_at'],
|
||||
operation_settings: ['key', 'value', 'updated_at'],
|
||||
update_operations: ['id', 'version_from', 'version_to', 'source_name', 'archive_path', 'sha256', 'status', 'details_json', 'created_at', 'applied_at', 'error_message'],
|
||||
system_migrations: ['id', 'migration_key', 'app_version', 'description', 'applied_at'],
|
||||
extension_clients: ['client_id', 'browser', 'version', 'platform', 'server_url', 'user_agent', 'capabilities_json', 'first_seen', 'last_seen'],
|
||||
users: ['id', 'email', 'name', 'password_hash', 'role', 'active', 'avatar_color', 'last_active_at', 'failed_login_count', 'locked_until', 'password_changed_at', 'created_at', 'invited_by'],
|
||||
sessions: ['id', 'user_id', 'token_hash', 'ip', 'user_agent', 'expires_at', 'last_seen_at', 'revoked_at', 'created_at'],
|
||||
audit_log: ['id', 'user_id', 'user_email', 'action', 'target_type', 'target_id', 'details', 'ip', 'created_at'],
|
||||
resource_locks: ['id', 'resource_type', 'resource_id', 'user_id', 'token_hash', 'acquired_at', 'renewed_at', 'expires_at', 'client_label'],
|
||||
theme_profiles: ['id', 'name', 'base_theme', 'values_json', 'created_by', 'created_at', 'updated_at'],
|
||||
user_theme_preferences: ['user_id', 'theme_id', 'density', 'reduced_motion', 'updated_at'],
|
||||
};
|
||||
const missingColumns = {};
|
||||
for (const [table, required] of Object.entries(checks)) {
|
||||
const existing = columns(db, table);
|
||||
const missing = required.filter((name) => !existing.has(name));
|
||||
if (missing.length) missingColumns[table] = missing.sort();
|
||||
}
|
||||
if (missingTables.length || Object.keys(missingColumns).length) {
|
||||
throw new Error(JSON.stringify({ missing_tables: missingTables.sort(), missing_columns: missingColumns }));
|
||||
}
|
||||
}
|
||||
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), 'vendoo-db-migrations-'));
|
||||
let fresh;
|
||||
let existing;
|
||||
try {
|
||||
fresh = new DatabaseSync(join(tempRoot, 'fresh.db'));
|
||||
fresh.exec(readFileSync(join(APP, 'db/schema.sql'), 'utf8'));
|
||||
const freshBlocks = execDbExecBlocks(fresh, join(APP, 'lib/db.mjs'));
|
||||
verify(fresh);
|
||||
|
||||
existing = new DatabaseSync(join(tempRoot, 'existing.db'));
|
||||
existing.exec(readFileSync(join(APP, 'db/schema.sql'), 'utf8'));
|
||||
if (BASE) execDbExecBlocks(existing, join(BASE, 'lib/db.mjs'));
|
||||
existing.prepare("INSERT INTO flux_images (prompt,image_path,width,height,seed,steps,style) VALUES ('Bestand','ai-generated/existing.png',768,768,42,4,'none')").run();
|
||||
const existingBlocks = execDbExecBlocks(existing, join(APP, 'lib/db.mjs'));
|
||||
verify(existing);
|
||||
const row = existing.prepare("SELECT prompt,image_path,favorite FROM flux_images WHERE image_path='ai-generated/existing.png'").get();
|
||||
if (!row || row.prompt !== 'Bestand' || row.image_path !== 'ai-generated/existing.png' || row.favorite !== 0) {
|
||||
throw new Error(`Bestandsdaten verändert: ${JSON.stringify(row)}`);
|
||||
}
|
||||
|
||||
const insertJob = existing.prepare(`INSERT INTO flux_prompt_jobs
|
||||
(id,prompt,final_prompt,width,height,steps,style,profile,prompt_mode,assistant,variants,seed_base,seed_lock,status,phase,settings_json)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
|
||||
for (const [jobId, status] of [['job-complete', 'completed'], ['job-failed', 'failed'], ['job-active', 'running']]) {
|
||||
insertJob.run(jobId, 'Test', 'Test', 768, 768, 6, 'none', 'balanced', 'precise', 1, 1, 42, 0, status, status, '{}');
|
||||
}
|
||||
existing.prepare("UPDATE flux_prompt_jobs SET archived_at=datetime('now') WHERE id IN ('job-complete','job-failed','job-active') AND status NOT IN ('queued','running','cancelling')").run();
|
||||
const archived = new Set(existing.prepare('SELECT id FROM flux_prompt_jobs WHERE archived_at IS NOT NULL').all().map((item) => item.id));
|
||||
if (archived.size !== 2 || !archived.has('job-complete') || !archived.has('job-failed')) {
|
||||
throw new Error(`Archivschutz inkorrekt: ${JSON.stringify([...archived])}`);
|
||||
}
|
||||
|
||||
existing.prepare("INSERT INTO listings (title, platform, ai_provider, photos, tags) VALUES ('Publishing Test', 'ebay-de', 'openai', '[]', '[]')").run();
|
||||
const listingId = existing.prepare('SELECT last_insert_rowid() AS id').get().id;
|
||||
existing.prepare("INSERT INTO publishing_jobs (id, listing_id, platform, mode, action, status, phase, idempotency_key) VALUES ('pub-job-1', ?, 'ebay-de', 'api', 'publish', 'queued', 'queued', 'key-1')").run(listingId);
|
||||
existing.prepare("INSERT INTO publishing_events (job_id, listing_id, platform, level, event_type, message) VALUES ('pub-job-1', ?, 'ebay-de', 'info', 'created', 'Auftrag erstellt')").run(listingId);
|
||||
let duplicateGuard = false;
|
||||
try {
|
||||
existing.prepare("INSERT INTO publishing_jobs (id, listing_id, platform, mode, action, status, phase, idempotency_key) VALUES ('pub-job-2', ?, 'ebay-de', 'api', 'publish', 'queued', 'queued', 'key-1')").run(listingId);
|
||||
} catch { duplicateGuard = true; }
|
||||
if (!duplicateGuard) throw new Error('Idempotency-Key verhindert keinen doppelten Auftrag');
|
||||
existing.prepare("UPDATE publishing_jobs SET status='running', phase='publish' WHERE id='pub-job-1'").run();
|
||||
existing.prepare("UPDATE publishing_jobs SET status='queued', phase='recovered' WHERE status='running'").run();
|
||||
const recovered = existing.prepare("SELECT status, phase FROM publishing_jobs WHERE id='pub-job-1'").get();
|
||||
if (recovered.status !== 'queued' || recovered.phase !== 'recovered') throw new Error(`Publishing-Recovery fehlgeschlagen: ${JSON.stringify(recovered)}`);
|
||||
|
||||
existing.prepare("INSERT INTO operation_settings (key,value) VALUES ('auto_backup_enabled','1')").run();
|
||||
existing.prepare("INSERT INTO operation_backups (id,kind,file_name,file_path,status) VALUES ('backup-1','manual','test.zip','/tmp/test.zip','verified')").run();
|
||||
existing.prepare("INSERT INTO update_operations (id,version_from,version_to,archive_path,status) VALUES ('update-1','1.29.0','1.30.0','/tmp/update.zip','staged')").run();
|
||||
existing.prepare("INSERT INTO extension_clients (client_id,browser,version,platform) VALUES ('client-1','chrome','1.9.0','popup')").run();
|
||||
const migration = existing.prepare("SELECT app_version FROM system_migrations WHERE migration_key='operations-center-1.30.0'").get();
|
||||
if (!migration || migration.app_version !== '1.30.0') throw new Error(`Operations-Migration fehlt: ${JSON.stringify(migration)}`);
|
||||
const extension = existing.prepare("SELECT browser,version FROM extension_clients WHERE client_id='client-1'").get();
|
||||
if (!extension || extension.browser !== 'chrome' || extension.version !== '1.9.0') throw new Error(`Extension-Client fehlerhaft: ${JSON.stringify(extension)}`);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
fresh_db_exec_blocks: freshBlocks,
|
||||
existing_db_exec_blocks: existingBlocks,
|
||||
existing_row_preserved: true,
|
||||
publishing_hardening: { tables: ['publishing_jobs', 'publishing_events'], idempotency_key_unique: true, restart_recovery: true, events_persisted: true },
|
||||
operations_center: { backup_tables: true, update_tables: true, extension_heartbeat: true, migration_recorded: true },
|
||||
tables: ['flux_images', 'flux_prompt_jobs', 'flux_prompt_job_variants', 'image_edit_versions', 'image_batch_profiles', 'image_batch_jobs', 'image_batch_job_items', 'quality_reports', 'publishing_jobs', 'publishing_events', 'operation_backups', 'operation_settings', 'update_operations', 'system_migrations', 'extension_clients', 'theme_profiles', 'user_theme_preferences'],
|
||||
}, null, 2));
|
||||
} finally {
|
||||
try { fresh?.close(); } catch {}
|
||||
try { existing?.close(); } catch {}
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -14,7 +14,7 @@ const fluxModule = read('app/modules/flux-studio/index.mjs');
|
||||
const ci = read('.github/workflows/ci.yml');
|
||||
const release = read('.github/workflows/release.yml');
|
||||
|
||||
if (pkg.version !== '1.43.0') failures.push(`package.json-Version ist ${pkg.version}`);
|
||||
if (pkg.version !== '1.41.2') failures.push(`package.json-Version ist ${pkg.version}`);
|
||||
expect(server, 'startFluxPromptJobLoop, stopFluxPromptJobLoop', 'Server importiert Start- und Stop-Funktion');
|
||||
expect(server, 'configureFluxStudioModule({ start: startFluxPromptJobLoop, stop: stopFluxPromptJobLoop })', 'FLUX-Lifecycle ist vollständig verdrahtet');
|
||||
expect(fluxJobs, 'export function stopFluxPromptJobLoop()', 'fehlender Stop-Export');
|
||||
@@ -29,8 +29,8 @@ expect(release, 'npm run verify:esm-exports', 'Release prüft ESM-Exportverträg
|
||||
expect(release, 'npm run verify:flux-loop', 'Release prüft FLUX-Hotfix');
|
||||
|
||||
if (failures.length) {
|
||||
console.error(`FLUX-Loop-Hotfix-Gate 1.43.0 fehlgeschlagen (${failures.length}):`);
|
||||
console.error(`FLUX-Loop-Hotfix-Gate 1.41.2 fehlgeschlagen (${failures.length}):`);
|
||||
failures.forEach(item => console.error(`- ${item}`));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('FLUX-Loop-Hotfix-Gate 1.43.0 erfolgreich: Stop-Export, Lifecycle-Verdrahtung und CI-Verträge geprüft.');
|
||||
console.log('FLUX-Loop-Hotfix-Gate 1.41.2 erfolgreich: Stop-Export, Lifecycle-Verdrahtung und CI-Verträge geprüft.');
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
|
||||
const root = resolve(fileURLToPath(new URL('..', import.meta.url)));
|
||||
const source = readFileSync(join(root, 'app/core/identity/identity-store.mjs'), 'utf8');
|
||||
const match = source.match(/db\.exec\(`([\s\S]*?)`\);/);
|
||||
if (!match) throw new Error('Identity-Schema-Block fehlt');
|
||||
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), 'vendoo-identity-140-'));
|
||||
const db = new DatabaseSync(join(tempRoot, 'identity.db'));
|
||||
try {
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
db.exec(match[1]);
|
||||
|
||||
for (const statement of [
|
||||
'ALTER TABLE users ADD COLUMN failed_login_count INTEGER NOT NULL DEFAULT 0',
|
||||
'ALTER TABLE users ADD COLUMN locked_until TEXT',
|
||||
'ALTER TABLE users ADD COLUMN password_changed_at TEXT',
|
||||
'ALTER TABLE sessions ADD COLUMN last_seen_at TEXT',
|
||||
'ALTER TABLE sessions ADD COLUMN revoked_at TEXT',
|
||||
]) {
|
||||
try { db.exec(statement); } catch { /* additive migration may already exist */ }
|
||||
}
|
||||
|
||||
const insertUser = db.prepare('INSERT INTO users(email,name,role,password_hash) VALUES(?,?,?,?)');
|
||||
insertUser.run('admin@example.invalid', 'Admin', 'admin', 'salt:hash');
|
||||
insertUser.run('editor@example.invalid', 'Editor', 'editor', 'salt:hash');
|
||||
const adminId = db.prepare("SELECT id FROM users WHERE email='admin@example.invalid'").get().id;
|
||||
const editorId = db.prepare("SELECT id FROM users WHERE email='editor@example.invalid'").get().id;
|
||||
const insertSession = db.prepare("INSERT INTO sessions(user_id,token_hash,expires_at,last_seen_at) VALUES(?,?,datetime('now','+1 day'),datetime('now'))");
|
||||
insertSession.run(adminId, 'admin-token');
|
||||
insertSession.run(editorId, 'editor-token');
|
||||
const editorSession = db.prepare('SELECT id FROM sessions WHERE user_id=?').get(editorId).id;
|
||||
|
||||
let result = db.prepare("UPDATE sessions SET revoked_at=datetime('now') WHERE id=? AND user_id=? AND revoked_at IS NULL").run(editorSession, adminId);
|
||||
if (result.changes !== 0) throw new Error('Fremde Sitzung konnte durch falschen Eigentümer widerrufen werden');
|
||||
result = db.prepare("UPDATE sessions SET revoked_at=datetime('now') WHERE id=? AND user_id=? AND revoked_at IS NULL").run(editorSession, editorId);
|
||||
if (result.changes !== 1) throw new Error('Eigene Sitzung konnte nicht widerrufen werden');
|
||||
|
||||
db.exec('CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)');
|
||||
db.prepare('INSERT OR REPLACE INTO settings(key,value) VALUES(?,?)').run('default_language', 'de');
|
||||
if (db.prepare("SELECT value FROM settings WHERE key='default_language'").get().value !== 'de') {
|
||||
throw new Error('Settings-Persistenz fehlgeschlagen');
|
||||
}
|
||||
|
||||
const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((row) => row.name));
|
||||
const required = ['users', 'sessions', 'auth_tokens', 'audit_log', 'login_history', 'settings'];
|
||||
const missing = required.filter((name) => !tables.has(name));
|
||||
if (missing.length) throw new Error(`Fehlende Identity-/Settings-Tabellen: ${missing.join(', ')}`);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
tables: required.sort(),
|
||||
foreign_session_blocked: true,
|
||||
own_session_revoked: true,
|
||||
}));
|
||||
} finally {
|
||||
db.close();
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -10,7 +10,7 @@ const assert = (condition, message) => { if (!condition) failures.push(message);
|
||||
const expectThrow = (fn, message) => { try { fn(); failures.push(message); } catch {} };
|
||||
|
||||
const pkg = JSON.parse(read('package.json'));
|
||||
assert(pkg.version === '1.43.0', `Paketversion ist ${pkg.version}`);
|
||||
assert(pkg.version === '1.41.2', `Paketversion ist ${pkg.version}`);
|
||||
const server = read('server.mjs');
|
||||
const authFacade = read('lib/auth.mjs');
|
||||
const identityStore = read('app/core/identity/identity-store.mjs');
|
||||
@@ -44,5 +44,5 @@ for (const [id, expected] of [['auth','native'],['users','native'],['sessions','
|
||||
assert(manifest.status === expected, `${id}-Modul ist nicht nativ`);
|
||||
}
|
||||
|
||||
if (failures.length) { console.error(`Identity-&-Settings-Gate 1.43.0 fehlgeschlagen (${failures.length}):`); failures.forEach(item => console.error(`- ${item}`)); process.exit(1); }
|
||||
console.log('Identity-&-Settings-Gate 1.43.0 erfolgreich: native Auth-, Benutzer-, Sitzungs- und Settings-Routen, letzten-Admin-Schutz, Sitzungs-Eigentümerprüfung und Settings-Allowlist geprüft.');
|
||||
if (failures.length) { console.error(`Identity-&-Settings-Gate 1.41.2 fehlgeschlagen (${failures.length}):`); failures.forEach(item => console.error(`- ${item}`)); process.exit(1); }
|
||||
console.log('Identity-&-Settings-Gate 1.41.2 erfolgreich: native Auth-, Benutzer-, Sitzungs- und Settings-Routen, letzten-Admin-Schutz, Sitzungs-Eigentümerprüfung und Settings-Allowlist geprüft.');
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { closeVendooDatabase, removeTempDirectory } from './test-runtime-cleanup.mjs';
|
||||
|
||||
const tempPrefix = process.env.VENDOO_TEST_TEMP_PREFIX || 'vendoo-module-defaults-';
|
||||
const temp = mkdtempSync(join(tmpdir(), tempPrefix));
|
||||
const temp = mkdtempSync(join(tmpdir(), 'vendoo-module-defaults-'));
|
||||
process.env.VENDOO_DATA_DIR = temp;
|
||||
process.env.VENDOO_DB_PATH = join(temp, 'db', 'vendoo.db');
|
||||
process.env.VENDOO_CONFIG_PATH = join(temp, 'config', 'vendoo.env');
|
||||
process.env.VENDOO_MODULE_STATE_PATH = join(temp, 'config', 'modules-state.json');
|
||||
process.env.VENDOO_MODULES_DIR = join(temp, 'modules');
|
||||
@@ -14,28 +11,21 @@ process.env.VENDOO_INITIAL_DISABLED_MODULES = 'vendoo.flux-studio';
|
||||
|
||||
const failures = [];
|
||||
const assert = (condition, message) => { if (!condition) failures.push(message); };
|
||||
|
||||
try {
|
||||
const { createPlatformKernel } = await import('../app/kernel/platform-kernel.mjs');
|
||||
const first = createPlatformKernel({ version: '1.43.0-test', hasPermission: () => true });
|
||||
const first = createPlatformKernel({ version: '1.41.2-test', hasPermission: () => true });
|
||||
assert(first.modules.describe('vendoo.flux-studio')?.enabled === false, 'FLUX Studio ist beim ersten Start nicht deaktiviert');
|
||||
assert(first.modules.describe('vendoo.product-image-tools')?.enabled === false, 'Produktbild Studio ist beim ersten Start nicht deaktiviert');
|
||||
assert(first.modules.describe('vendoo.deployments')?.enabled === true, 'Geschütztes Deployment-Modul ist nicht aktiviert');
|
||||
const { ModuleStateStore } = await import('../app/core/modules/module-state-store.mjs');
|
||||
const stateStore = new ModuleStateStore(process.env.VENDOO_MODULE_STATE_PATH);
|
||||
stateStore.setEnabled('vendoo.flux-studio', true, { source: 'test' });
|
||||
stateStore.setEnabled('vendoo.product-image-tools', true, { source: 'test' });
|
||||
const second = createPlatformKernel({ version: '1.43.0-test', hasPermission: () => true });
|
||||
assert(second.modules.describe('vendoo.flux-studio')?.enabled === true, 'Gespeicherte FLUX-Aktivierung wird durch Initialwert überschrieben');
|
||||
assert(second.modules.describe('vendoo.product-image-tools')?.enabled === true, 'Gespeicherte Produktbild-Studio-Aktivierung wird durch Initialwert überschrieben');
|
||||
new ModuleStateStore(process.env.VENDOO_MODULE_STATE_PATH).setEnabled('vendoo.flux-studio', true, { source: 'test' });
|
||||
const second = createPlatformKernel({ version: '1.41.2-test', hasPermission: () => true });
|
||||
assert(second.modules.describe('vendoo.flux-studio')?.enabled === true, 'Gespeicherte Admin-Aktivierung wird durch Initialwert überschrieben');
|
||||
} finally {
|
||||
await closeVendooDatabase({ failures, label: 'Initial-Modul-Testdatenbank' });
|
||||
await removeTempDirectory(temp, { failures, label: 'Initial-Modul-Testordner' });
|
||||
rmSync(temp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error(`Initial-Module-Gate 1.43.0 fehlgeschlagen (${failures.length}):`);
|
||||
console.error(`Initial-Module-Gate 1.41.2 fehlgeschlagen (${failures.length}):`);
|
||||
failures.forEach(item => console.error(`- ${item}`));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Initial-Module-Gate 1.43.0 erfolgreich: FLUX und Produktbild Studio sind beim ersten Start aus, bleiben nach bewusster Admin-Aktivierung dauerhaft aktivierbar und geben ihre temporäre SQLite-Datenbank sauber frei.');
|
||||
console.log('Initial-Module-Gate 1.41.2 erfolgreich: FLUX ist beim ersten Start aus und bleibt nach bewusster Admin-Aktivierung dauerhaft aktivierbar.');
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { readdirSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { resolve } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
const root = resolve(import.meta.dirname, '..');
|
||||
const gate = resolve(root, 'tools', 'verify-initial-disabled-modules-1.41.2.mjs');
|
||||
const prefix = `vendoo-module-defaults-regression-${process.pid}-${Date.now()}-`;
|
||||
const failures = [];
|
||||
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
const result = spawnSync(process.execPath, [gate], {
|
||||
cwd: root,
|
||||
env: { ...process.env, VENDOO_TEST_TEMP_PREFIX: prefix },
|
||||
encoding: 'utf8',
|
||||
timeout: 60_000,
|
||||
});
|
||||
if (result.error) failures.push(`Durchlauf ${attempt}: ${result.error.message}`);
|
||||
if (result.status !== 0) {
|
||||
failures.push(`Durchlauf ${attempt}: Exit-Code ${result.status}
|
||||
${result.stdout || ''}
|
||||
${result.stderr || ''}`.trim());
|
||||
}
|
||||
}
|
||||
|
||||
const leftovers = readdirSync(tmpdir(), { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory() && entry.name.startsWith(prefix))
|
||||
.map(entry => entry.name);
|
||||
if (leftovers.length) failures.push(`Temporäre Testordner wurden nicht entfernt: ${leftovers.join(', ')}`);
|
||||
|
||||
if (failures.length) {
|
||||
console.error(`Initial-Module-Cleanup-Regression 1.43.0 fehlgeschlagen (${failures.length}):`);
|
||||
failures.forEach(item => console.error(`- ${item}`));
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Initial-Module-Cleanup-Regression 1.43.0 erfolgreich: drei isolierte Durchläufe ohne offene SQLite-Datei oder Temp-Rest.');
|
||||
@@ -1,189 +0,0 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { dirname, join, normalize, resolve, win32 } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
||||
const command = packageJson?.scripts?.['verify:installer-shell'] || '';
|
||||
const isWindowsHost = process.platform === 'win32' || process.env.VENDOO_TEST_WINDOWS_SHELL === '1';
|
||||
|
||||
function fail(message) {
|
||||
console.error(`Installer-Shell-Portabilitätsgate fehlgeschlagen: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (/^\s*sh\s+/i.test(command)) {
|
||||
fail('package.json darf den POSIX-Befehl sh nicht direkt voraussetzen.');
|
||||
}
|
||||
if (command !== 'node tools/verify-installer-shell.mjs') {
|
||||
fail(`Unerwarteter npm-Vertrag: ${command}`);
|
||||
}
|
||||
|
||||
const shellContract = readFileSync(join(root, 'tools', 'verify-installer-shell.sh'), 'utf8');
|
||||
for (const marker of [
|
||||
'VENDOO_INSTALLER_SHELL_HOST',
|
||||
'MINGW*|MSYS*|CYGWIN*',
|
||||
'POSIX-Modus 0600',
|
||||
'show_file',
|
||||
'assert_env_permissions',
|
||||
'--exclude=.git',
|
||||
]) {
|
||||
if (!shellContract.includes(marker)) fail(`Shell-Testvertrag fehlt: ${marker}`);
|
||||
}
|
||||
|
||||
function run(file, args, options = {}) {
|
||||
return spawnSync(file, args, {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
windowsHide: true,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function addCandidate(set, value) {
|
||||
if (!value) return;
|
||||
const candidate = normalize(value.replace(/^"|"$/g, '').trim());
|
||||
if (candidate) set.add(candidate);
|
||||
}
|
||||
|
||||
function addGitRootCandidates(set, gitRoot) {
|
||||
if (!gitRoot) return;
|
||||
addCandidate(set, join(gitRoot, 'bin', 'sh.exe'));
|
||||
addCandidate(set, join(gitRoot, 'usr', 'bin', 'sh.exe'));
|
||||
}
|
||||
|
||||
function pathApiFor(value) {
|
||||
return /^[a-zA-Z]:[\\/]/.test(value) ? win32 : { normalize, dirname };
|
||||
}
|
||||
|
||||
function deriveGitRootFromExecutable(executable) {
|
||||
const raw = executable.replace(/^"|"$/g, '').trim();
|
||||
const api = pathApiFor(raw);
|
||||
const clean = api.normalize(raw);
|
||||
const parent = api.dirname(clean);
|
||||
const leaf = parent.toLowerCase().split(/[\\/]/).pop();
|
||||
if (['cmd', 'bin', 'usr'].includes(leaf)) {
|
||||
return api.dirname(parent);
|
||||
}
|
||||
return api.dirname(clean);
|
||||
}
|
||||
|
||||
function deriveGitRootFromExecPath(execPath) {
|
||||
const raw = execPath.replace(/^"|"$/g, '').trim();
|
||||
const api = pathApiFor(raw);
|
||||
const clean = api.normalize(raw);
|
||||
const lowered = clean.toLowerCase().replace(/\\/g, '/');
|
||||
for (const marker of ['/mingw64/', '/mingw32/', '/usr/libexec/', '/libexec/']) {
|
||||
const index = lowered.indexOf(marker);
|
||||
if (index > 0) return clean.slice(0, index);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findWindowsGitBash() {
|
||||
const candidates = new Set();
|
||||
addCandidate(candidates, process.env.VENDOO_SH);
|
||||
addCandidate(candidates, process.env.SHELL);
|
||||
|
||||
for (const base of [process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.LOCALAPPDATA]) {
|
||||
if (!base) continue;
|
||||
addGitRootCandidates(candidates, join(base, base === process.env.LOCALAPPDATA ? 'Programs' : '', 'Git'));
|
||||
}
|
||||
|
||||
const whereGit = run('where.exe', ['git.exe']);
|
||||
if (whereGit.status === 0) {
|
||||
for (const line of whereGit.stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
|
||||
addGitRootCandidates(candidates, deriveGitRootFromExecutable(line));
|
||||
}
|
||||
}
|
||||
|
||||
const execPath = run('git.exe', ['--exec-path']);
|
||||
if (execPath.status === 0) {
|
||||
const gitRoot = deriveGitRootFromExecPath(execPath.stdout);
|
||||
if (gitRoot) addGitRootCandidates(candidates, gitRoot);
|
||||
}
|
||||
|
||||
const whereSh = run('where.exe', ['sh.exe']);
|
||||
if (whereSh.status === 0) {
|
||||
for (const line of whereSh.stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean)) {
|
||||
addCandidate(candidates, line);
|
||||
}
|
||||
}
|
||||
|
||||
return [...candidates].find((candidate) => existsSync(candidate)) || null;
|
||||
}
|
||||
|
||||
// Regression contracts for common Git-for-Windows layouts.
|
||||
const standardRoot = deriveGitRootFromExecutable('C:\\Program Files\\Git\\cmd\\git.exe');
|
||||
if (standardRoot.toLowerCase() !== win32.normalize('C:\\Program Files\\Git').toLowerCase()) {
|
||||
fail(`Git-Root aus cmd\\git.exe falsch abgeleitet: ${standardRoot}`);
|
||||
}
|
||||
const execRoot = deriveGitRootFromExecPath('C:\\Program Files\\Git\\mingw64\\libexec\\git-core');
|
||||
if (!execRoot || execRoot.toLowerCase() !== win32.normalize('C:\\Program Files\\Git').toLowerCase()) {
|
||||
fail(`Git-Root aus git --exec-path falsch abgeleitet: ${execRoot}`);
|
||||
}
|
||||
|
||||
let shell;
|
||||
if (isWindowsHost) {
|
||||
shell = process.platform === 'win32' ? findWindowsGitBash() : 'sh';
|
||||
if (!shell) {
|
||||
fail('Git Bash wurde trotz installiertem Git nicht gefunden. Erwartet wird bin\\sh.exe oder usr\\bin\\sh.exe innerhalb der Git-for-Windows-Installation.');
|
||||
}
|
||||
} else {
|
||||
const probe = run('sh', ['-c', 'printf vendoo-shell-ok']);
|
||||
if (probe.status !== 0 || probe.stdout !== 'vendoo-shell-ok') {
|
||||
fail('Eine funktionsfähige POSIX-Shell wurde nicht gefunden.');
|
||||
}
|
||||
shell = 'sh';
|
||||
}
|
||||
|
||||
if (isWindowsHost) {
|
||||
for (const script of ['setup.sh', 'tools/verify-installer-shell.sh']) {
|
||||
const syntax = run(shell, ['-n', script], {
|
||||
env: {
|
||||
...process.env,
|
||||
NO_COLOR: '1',
|
||||
FORCE_COLOR: '0',
|
||||
MSYS2_ARG_CONV_EXCL: '*',
|
||||
},
|
||||
timeout: 60_000,
|
||||
});
|
||||
if (syntax.stdout) process.stdout.write(syntax.stdout);
|
||||
if (syntax.stderr) process.stderr.write(syntax.stderr);
|
||||
if (syntax.error) fail(`Shell-Syntaxprüfung konnte nicht gestartet werden (${script}): ${syntax.error.message}`);
|
||||
if (syntax.signal) fail(`Shell-Syntaxprüfung wurde durch Signal ${syntax.signal} beendet (${script}).`);
|
||||
if (syntax.status !== 0) {
|
||||
fail(`Shell-Syntaxprüfung fehlgeschlagen (${script}, Exit-Code ${syntax.status}). Verwendete Shell: ${shell}`);
|
||||
}
|
||||
}
|
||||
console.log('Installer-Shell-Portabilitätsgate erfolgreich: Git Bash hat setup.sh und den Linux-Funktionstest syntaktisch geprüft; die funktionale Docker-/NAS-/VPS-Simulation läuft im verpflichtenden Linux-CI-Gate.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const result = run(shell, ['tools/verify-installer-shell.sh'], {
|
||||
env: {
|
||||
...process.env,
|
||||
NO_COLOR: '1',
|
||||
FORCE_COLOR: '0',
|
||||
VENDOO_INSTALLER_SHELL_HOST: 'posix',
|
||||
},
|
||||
timeout: 180_000,
|
||||
});
|
||||
|
||||
if (result.stdout) process.stdout.write(result.stdout);
|
||||
if (result.stderr) process.stderr.write(result.stderr);
|
||||
if (result.error) fail(`Shell-Gate konnte nicht gestartet werden: ${result.error.message}`);
|
||||
if (result.signal) fail(`Shell-Gate wurde durch Signal ${result.signal} beendet.`);
|
||||
if (result.status !== 0) {
|
||||
const stdout = String(result.stdout || '').trim();
|
||||
const stderr = String(result.stderr || '').trim();
|
||||
const diagnostics = [
|
||||
stderr ? `Shell-Fehlerausgabe:\n${stderr}` : '',
|
||||
stdout ? `Shell-Standardausgabe:\n${stdout}` : '',
|
||||
].filter(Boolean).join('\n\n');
|
||||
fail(`Shell-Gate endete mit Exit-Code ${result.status}. Verwendete Shell: ${shell}${diagnostics ? `\n${diagnostics}` : ''}`);
|
||||
}
|
||||
|
||||
console.log(`Installer-Shell-Portabilitätsgate erfolgreich: ${shell} führte die vollständige Linux-/NAS-/VPS-Simulation mit strikter POSIX-Rechteprüfung aus.`);
|
||||
+18
-100
@@ -3,80 +3,9 @@ set -eu
|
||||
|
||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
TMP_DIR=$(mktemp -d)
|
||||
HOST_KIND=${VENDOO_INSTALLER_SHELL_HOST:-auto}
|
||||
|
||||
cleanup() { rm -rf "$TMP_DIR"; }
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
fail() {
|
||||
printf '%s\n' "[FEHLER] $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
show_file() {
|
||||
label=$1
|
||||
file=$2
|
||||
printf '%s\n' "--- $label: $file ---" >&2
|
||||
if [ -f "$file" ]; then cat "$file" >&2; else printf '%s\n' '(Datei fehlt)' >&2; fi
|
||||
printf '%s\n' "--- Ende $label ---" >&2
|
||||
}
|
||||
|
||||
assert_file() {
|
||||
file=$1
|
||||
label=$2
|
||||
[ -f "$file" ] || fail "$label fehlt: $file"
|
||||
}
|
||||
|
||||
assert_dir() {
|
||||
dir=$1
|
||||
label=$2
|
||||
[ -d "$dir" ] || fail "$label fehlt: $dir"
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
pattern=$1
|
||||
file=$2
|
||||
label=$3
|
||||
if ! grep -q "$pattern" "$file"; then
|
||||
show_file "$label" "$file"
|
||||
fail "$label enthält das erwartete Muster nicht: $pattern"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_token() {
|
||||
file=$1
|
||||
if ! grep -Eq '^VENDOO_SETUP_TOKEN=[0-9a-f]{64}$' "$file"; then
|
||||
show_file '.env' "$file"
|
||||
fail 'VENDOO_SETUP_TOKEN fehlt oder ist nicht 64-stellig hexadezimal.'
|
||||
fi
|
||||
}
|
||||
|
||||
is_windows_shell() {
|
||||
if [ "$HOST_KIND" = windows ]; then return 0; fi
|
||||
if [ "$HOST_KIND" = posix ]; then return 1; fi
|
||||
case "$(uname -s 2>/dev/null || printf unknown)" in
|
||||
MINGW*|MSYS*|CYGWIN*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
assert_env_permissions() {
|
||||
file=$1
|
||||
if is_windows_shell; then
|
||||
# Git Bash runs on NTFS and cannot reliably represent POSIX mode 0600.
|
||||
# The Linux/NAS/VPS branch below remains strict. On Windows we verify
|
||||
# that the installer still contains and executes the chmod hardening.
|
||||
assert_contains 'chmod 600 "$ENV_FILE"' "$ROOT_DIR/setup.sh" 'setup.sh-Härtung'
|
||||
assert_contains 'chmod 600 "$tmp"' "$ROOT_DIR/setup.sh" 'setup.sh-Härtung'
|
||||
[ -r "$file" ] && [ -w "$file" ] || fail '.env ist unter Git Bash nicht les- und schreibbar.'
|
||||
printf '%s\n' '[INFO] NTFS/Git-Bash erkannt: POSIX-Modus 0600 wird statisch geprüft; exakte Modusprüfung erfolgt auf Linux/NAS/VPS.'
|
||||
return 0
|
||||
fi
|
||||
|
||||
mode=$(stat -c '%a' "$file" 2>/dev/null || true)
|
||||
[ "$mode" = 600 ] || fail ".env besitzt auf POSIX nicht Modus 600, sondern: ${mode:-unbekannt}"
|
||||
}
|
||||
|
||||
mkdir -p "$TMP_DIR/bin"
|
||||
cat > "$TMP_DIR/bin/docker" <<'EOF'
|
||||
#!/usr/bin/env sh
|
||||
@@ -94,43 +23,32 @@ run_mode() {
|
||||
input=$2
|
||||
dir="$TMP_DIR/$name"
|
||||
mkdir -p "$dir"
|
||||
|
||||
if ! tar -C "$ROOT_DIR" --exclude=.git --exclude=node_modules --exclude=release-output -cf - . | tar -C "$dir" -xf -; then
|
||||
fail "Release-Kopie für Modus $name konnte nicht erstellt werden."
|
||||
fi
|
||||
|
||||
tar -C "$ROOT_DIR" --exclude=node_modules --exclude=release-output -cf - . | tar -C "$dir" -xf -
|
||||
FAKE_DOCKER_LOG="$dir/docker.log"
|
||||
export FAKE_DOCKER_LOG
|
||||
if ! printf '%b' "$input" | PATH="$TMP_DIR/bin:$PATH" sh "$dir/setup.sh" install > "$dir/out.log" 2>&1; then
|
||||
show_file "setup.sh-Ausgabe ($name)" "$dir/out.log"
|
||||
show_file "Docker-Aufrufe ($name)" "$dir/docker.log"
|
||||
fail "setup.sh ist im Modus $name fehlgeschlagen."
|
||||
fi
|
||||
printf '%b' "$input" | PATH="$TMP_DIR/bin:$PATH" sh "$dir/setup.sh" install > "$dir/out.log" 2>&1
|
||||
|
||||
assert_file "$dir/out.log" "Installer-Ausgabe ($name)"
|
||||
assert_file "$dir/.env" ".env ($name)"
|
||||
assert_file "$dir/docker.log" "Docker-Protokoll ($name)"
|
||||
assert_contains 'keine Datei musste manuell bearbeitet werden' "$dir/out.log" "Installer-Ausgabe ($name)"
|
||||
assert_token "$dir/.env"
|
||||
assert_contains 'docker compose build --pull' "$dir/docker.log" "Docker-Protokoll ($name)"
|
||||
assert_contains 'docker compose up -d --remove-orphans' "$dir/docker.log" "Docker-Protokoll ($name)"
|
||||
assert_env_permissions "$dir/.env"
|
||||
grep -q 'keine Datei musste manuell bearbeitet werden' "$dir/out.log"
|
||||
grep -q '^VENDOO_SETUP_TOKEN=[0-9a-f]\{64\}$' "$dir/.env"
|
||||
grep -q 'docker compose build --pull' "$dir/docker.log"
|
||||
grep -q 'docker compose up -d --remove-orphans' "$dir/docker.log"
|
||||
test "$(stat -c '%a' "$dir/.env")" = 600
|
||||
}
|
||||
|
||||
run_mode docker '1\n8124\nnein\n'
|
||||
assert_contains '^VENDOO_PUBLISH_ADDRESS=127.0.0.1$' "$TMP_DIR/docker/.env" 'Docker-.env'
|
||||
assert_contains '^VENDOO_BIND_HOST=0.0.0.0$' "$TMP_DIR/docker/.env" 'Docker-.env'
|
||||
grep -q '^VENDOO_PUBLISH_ADDRESS=127.0.0.1$' "$TMP_DIR/docker/.env"
|
||||
grep -q '^VENDOO_BIND_HOST=0.0.0.0$' "$TMP_DIR/docker/.env"
|
||||
|
||||
run_mode nas '2\n9124\nhttp://192.168.1.50:9124\nnein\n'
|
||||
assert_dir "$TMP_DIR/nas/runtime/config" 'NAS-Konfigurationsordner'
|
||||
assert_dir "$TMP_DIR/nas/runtime/db" 'NAS-Datenbankordner'
|
||||
assert_contains '^VENDOO_PUBLISH_ADDRESS=0.0.0.0$' "$TMP_DIR/nas/.env" 'NAS-.env'
|
||||
assert_contains '^PUBLIC_BASE_URL=http://192.168.1.50:9124$' "$TMP_DIR/nas/.env" 'NAS-.env'
|
||||
test -d "$TMP_DIR/nas/runtime/config"
|
||||
test -d "$TMP_DIR/nas/runtime/db"
|
||||
grep -q '^VENDOO_PUBLISH_ADDRESS=0.0.0.0$' "$TMP_DIR/nas/.env"
|
||||
grep -q '^PUBLIC_BASE_URL=http://192.168.1.50:9124$' "$TMP_DIR/nas/.env"
|
||||
|
||||
run_mode vps '3\n8124\nvendoo.example.invalid\nnein\n'
|
||||
assert_contains '^VENDOO_DOMAIN=vendoo.example.invalid$' "$TMP_DIR/vps/.env" 'VPS-.env'
|
||||
assert_contains '^PUBLIC_BASE_URL=https://vendoo.example.invalid$' "$TMP_DIR/vps/.env" 'VPS-.env'
|
||||
assert_contains '^VENDOO_COOKIE_SECURE=true$' "$TMP_DIR/vps/.env" 'VPS-.env'
|
||||
assert_contains '^VENDOO_TRUST_PROXY=true$' "$TMP_DIR/vps/.env" 'VPS-.env'
|
||||
grep -q '^VENDOO_DOMAIN=vendoo.example.invalid$' "$TMP_DIR/vps/.env"
|
||||
grep -q '^PUBLIC_BASE_URL=https://vendoo.example.invalid$' "$TMP_DIR/vps/.env"
|
||||
grep -q '^VENDOO_COOKIE_SECURE=true$' "$TMP_DIR/vps/.env"
|
||||
grep -q '^VENDOO_TRUST_PROXY=true$' "$TMP_DIR/vps/.env"
|
||||
|
||||
printf '%s\n' 'Shell-Installer-Gate erfolgreich: Docker-, NAS- und VPS-Dialog erzeugen valide Konfiguration; POSIX-Dateirechte werden auf Linux/NAS/VPS strikt geprüft.'
|
||||
printf '%s\n' 'Shell-Installer-Gate erfolgreich: Docker-, NAS- und VPS-Dialog erzeugen valide, geschützte Konfiguration ohne manuelle Dateibearbeitung.'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user