Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a245ff309 | ||
|
|
6f48827f4d |
@@ -1,4 +1,4 @@
|
||||
# Vendoo 1.35.0 – Beispielkonfiguration für Windows, Docker und NAS
|
||||
# Vendoo 1.37.0 – Beispielkonfiguration für Windows, Docker und NAS
|
||||
# Diese Datei enthält ausschließlich Platzhalter. Für den Betrieb als .env kopieren.
|
||||
# Niemals eine produktive .env in Release-ZIPs, Git oder Support-Logs aufnehmen.
|
||||
|
||||
@@ -44,7 +44,7 @@ VENDOO_DOMAIN=
|
||||
|
||||
# Optional: öffentliche JSON-Manifest-URL für die Updateprüfung
|
||||
VENDOO_UPDATE_MANIFEST_URL=
|
||||
# Server-, Mehrbenutzer- und Deployment-Sicherheit (1.35.0)
|
||||
# Server-, Mehrbenutzer- und Deployment-Sicherheit (1.37.0)
|
||||
# Standardmäßig nur auf diesem PC erreichbar. Für LAN bewusst auf 0.0.0.0 setzen.
|
||||
VENDOO_BIND_HOST=127.0.0.1
|
||||
# Kommagetrennte zusätzliche Hostnamen/IPs, z. B. vendoo.intern,192.168.178.50
|
||||
@@ -59,7 +59,7 @@ VENDOO_READ_RATE_LIMIT=360
|
||||
VENDOO_MUTATION_RATE_LIMIT=120
|
||||
|
||||
|
||||
# Produktionsbereitstellung 1.35.0
|
||||
# Produktionsbereitstellung 1.37.0
|
||||
# Mindestens 32 zufällige Zeichen. Wird beim ersten Admin-Setup abgefragt.
|
||||
VENDOO_SETUP_TOKEN=
|
||||
VENDOO_REQUIRE_SETUP_TOKEN=true
|
||||
@@ -78,3 +78,27 @@ VENDOO_OPERATIONS_DIR=
|
||||
VENDOO_LOG_DIR=
|
||||
# Docker setzt automatisch /app/data/config/vendoo.env; lokal bleibt dieser Wert leer.
|
||||
VENDOO_CONFIG_PATH=
|
||||
|
||||
# Security, Secrets & Observability (1.37.0)
|
||||
# Normalerweise automatisch verwaltet; keine manuelle Bearbeitung erforderlich.
|
||||
# VENDOO_SECRETS_PATH=/app/data/config/secrets.enc.json
|
||||
# VENDOO_MASTER_KEY_FILE=/app/data/config/master.key
|
||||
# Optional: externer 32-Byte-Master-Key als Base64 oder 64 Hex-Zeichen.
|
||||
# VENDOO_MASTER_KEY=
|
||||
VENDOO_JSON_LOGS=true
|
||||
SMTP_TLS_REJECT_UNAUTHORIZED=true
|
||||
# Coolify / GitHub production deployment
|
||||
VENDOO_INITIAL_DISABLED_MODULES=vendoo.flux-studio
|
||||
VENDOO_DEPLOY_PROVIDER=none
|
||||
COOLIFY_API_URL=
|
||||
COOLIFY_RESOURCE_UUID=
|
||||
COOLIFY_TRIGGER_MODE=webhook
|
||||
COOLIFY_DEPLOY_METHOD=GET
|
||||
COOLIFY_DEPLOY_ENDPOINT=/api/v1/deploy?uuid={uuid}&force={force}
|
||||
VENDOO_GITHUB_REPOSITORY=Masterluke77/vendoo
|
||||
VENDOO_GITHUB_BRANCH=main
|
||||
VENDOO_GHCR_IMAGE=ghcr.io/masterluke77/vendoo
|
||||
VENDOO_UPDATE_CHANNEL=stable
|
||||
VENDOO_BACKUP_BEFORE_DEPLOY=true
|
||||
VENDOO_DEPLOY_BACKUP_UPLOADS=false
|
||||
# COOLIFY_API_TOKEN and COOLIFY_DEPLOY_WEBHOOK_URL belong in the encrypted secret store.
|
||||
|
||||
@@ -42,12 +42,48 @@ jobs:
|
||||
- name: Git-Staging-Filter Regressionstest
|
||||
run: npm run verify:git-staging
|
||||
|
||||
- name: Git-Ignore-Grenzen für Quell- und Runtimeordner prüfen
|
||||
run: npm run verify:ignore-boundaries
|
||||
|
||||
- name: Syntaxprüfung
|
||||
run: npm run check
|
||||
|
||||
- name: Produktiven Source-Root und Archivgrenzen 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: Vollständigen Modul-Capability-Graph prüfen
|
||||
run: npm run verify:capabilities
|
||||
|
||||
- name: Coolify-Deployment und Web-Updates prüfen
|
||||
run: npm run verify:coolify
|
||||
|
||||
- name: Initial deaktivierte Module prüfen
|
||||
run: npm run verify:initial-modules
|
||||
|
||||
- name: Plattformarchitektur und Design Tokens prüfen
|
||||
run: npm run verify:architecture
|
||||
|
||||
- name: Theme Manager, Accessibility und Komponenten-Tokens prüfen
|
||||
run: npm run verify:themes
|
||||
|
||||
- name: Security, Secrets und Observability prüfen
|
||||
run: npm run verify:security
|
||||
|
||||
- name: Auth, Benutzer, Sitzungen und Einstellungen prüfen
|
||||
run: npm run verify:identity
|
||||
|
||||
- name: Artikel, Lager und Medien prüfen
|
||||
run: npm run verify:catalog
|
||||
|
||||
- name: Modulmanager, Pakete und Aktivierungsgrenzen prüfen
|
||||
run: npm run verify:modules
|
||||
|
||||
- name: Deployment-Gate
|
||||
run: npm run verify:deployment
|
||||
|
||||
|
||||
@@ -47,8 +47,20 @@ jobs:
|
||||
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
|
||||
@@ -113,12 +125,13 @@ jobs:
|
||||
|
||||
- name: Container-Metadaten erzeugen
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}},value=${{ github.event.release.tag_name }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ github.event.release.tag_name }}
|
||||
type=raw,value=stable,enable=${{ !github.event.release.prerelease }}
|
||||
type=raw,value=latest,enable=${{ !github.event.release.prerelease }}
|
||||
|
||||
- name: Container bauen und veröffentlichen
|
||||
|
||||
@@ -13,22 +13,22 @@
|
||||
node_modules/
|
||||
|
||||
# SQLite runtime data
|
||||
db/*.db
|
||||
db/*.db-shm
|
||||
db/*.db-wal
|
||||
db/*.sqlite
|
||||
db/*.sqlite3
|
||||
/db/*.db
|
||||
/db/*.db-shm
|
||||
/db/*.db-wal
|
||||
/db/*.sqlite
|
||||
/db/*.sqlite3
|
||||
|
||||
# User and runtime data
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
backups/
|
||||
logs/
|
||||
tmp/
|
||||
temp/
|
||||
sessions/
|
||||
data/
|
||||
docker-data/
|
||||
/uploads/*
|
||||
!/uploads/.gitkeep
|
||||
/backups/
|
||||
/logs/
|
||||
/tmp/
|
||||
/temp/
|
||||
/sessions/
|
||||
/data/
|
||||
/docker-data/
|
||||
*.log
|
||||
|
||||
# OS and editor files
|
||||
@@ -43,42 +43,54 @@ compose.override.yaml
|
||||
docker-compose.override.yml
|
||||
|
||||
# Local AI runtimes and downloaded model data
|
||||
local-ai/comfyui/
|
||||
local-ai/downloads/
|
||||
local-ai/extract-temp/
|
||||
local-ai/fashn-vton/.venv/
|
||||
local-ai/fashn-vton/engine/
|
||||
local-ai/fashn-vton/weights/
|
||||
local-ai/fashn-vton/outputs/
|
||||
local-ai/fashn-vton/runtime/
|
||||
local-ai/fashn-vton/hf-cache/
|
||||
local-ai/fashn-vton/fashn-vton-*.zip
|
||||
local-ai/fashn-vton/python311/
|
||||
local-ai/fashn-vton/uv/
|
||||
local-ai/fashn-vton/uv-cache/
|
||||
local-ai/fashn-vton/downloads/
|
||||
/local-ai/comfyui/
|
||||
/local-ai/downloads/
|
||||
/local-ai/extract-temp/
|
||||
/local-ai/fashn-vton/.venv/
|
||||
/local-ai/fashn-vton/engine/
|
||||
/local-ai/fashn-vton/weights/
|
||||
/local-ai/fashn-vton/outputs/
|
||||
/local-ai/fashn-vton/runtime/
|
||||
/local-ai/fashn-vton/hf-cache/
|
||||
/local-ai/fashn-vton/fashn-vton-*.zip
|
||||
/local-ai/fashn-vton/python311/
|
||||
/local-ai/fashn-vton/uv/
|
||||
/local-ai/fashn-vton/uv-cache/
|
||||
/local-ai/fashn-vton/downloads/
|
||||
|
||||
# Historical handover and feature-package archives (never source)
|
||||
/Archiv/
|
||||
/archive/
|
||||
/archives/
|
||||
|
||||
# Local update, rollback and slice archives
|
||||
updates/
|
||||
/updates/
|
||||
|
||||
# Operations Center runtime
|
||||
operations/staging/
|
||||
operations/updates/
|
||||
operations/rollback/
|
||||
operations/apply-*/
|
||||
operations/pending-operation.json
|
||||
operations/last-operation.json
|
||||
/operations/staging/
|
||||
/operations/updates/
|
||||
/operations/rollback/
|
||||
/operations/apply-*/
|
||||
/operations/pending-operation.json
|
||||
/operations/last-operation.json
|
||||
|
||||
# Deployment assistant state
|
||||
.vendoo-install-mode
|
||||
runtime/
|
||||
/.vendoo-install-mode
|
||||
/runtime/
|
||||
# Generated deployment and release output
|
||||
release-output/
|
||||
/release-output/
|
||||
*.release.zip
|
||||
*.release.sha256.txt
|
||||
.vendoo-github-state.json
|
||||
/.vendoo-github-state.json
|
||||
|
||||
# First-run and local installer state
|
||||
operations/first-admin-code.txt
|
||||
config/local/
|
||||
/operations/first-admin-code.txt
|
||||
/config/local/
|
||||
|
||||
|
||||
# Installed Vendoo module packages (runtime)
|
||||
/modules/
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Vendoo 1.31.0 – Ist- und Lückenanalyse
|
||||
|
||||
Stand der Prüfung: 09.07.2026
|
||||
Geprüfte Basis: `Vendoo_Feature_1_31_0_Multiuser_Security_Foundation.zip`
|
||||
Verifizierte SHA-256 der Basis: `cad2670d663c98964b7c3b4e95e5f6e475a3bcae12921339a0a84576b77d325b`
|
||||
|
||||
Die Bewertung basiert auf dem real enthaltenen Code. Paketnamen und Dokumentation wurden nicht als alleiniger Funktionsnachweis verwendet.
|
||||
|
||||
| Bereich | Status in 1.31.0 | Reale Fundstelle | Risiko/Lücke | Änderung in 1.32.0 |
|
||||
|---|---|---|---|---|
|
||||
| Versionen | bestätigt | `package.json`, Lockfile, UI-Build und Manifest = 1.31.0 | keine | auf 1.32.0 synchronisiert |
|
||||
| Rollen/RBAC | bestätigt | `lib/security.mjs`, globaler `apiPermissionGuard` | einzelne Sonderrouten bleiben zusätzlich manuell zu beobachten | unverändert erhalten |
|
||||
| Sessions/Login-Schutz | bestätigt, Setup-Ausnahme riskant | `lib/auth.mjs`, gehashte Tokens, Sperrlogik, Widerruf; `requireAuth` übersprang im Setup-Modus alle APIs | vor erstem Admin waren geschützte API-Routen unnötig offen; kein Offline-Notfallreset für verlorenen letzten Admin | API im Setup-Modus gesperrt; Notfallreset als Folgepunkt dokumentiert |
|
||||
| CSRF/Cookies | vorhanden | Double-Submit-Cookie und Header, `HttpOnly`, dynamisches `Secure`, `SameSite=lax` | Cookie-Modus nicht konfigurierbar; Proxyannahme nur boolesch/ein Hop | Cookie-Modus konfigurierbar, Proxy-Hops/Scopes unterstützt |
|
||||
| CORS/Origin | vorhanden, unvollständig | Host-/Origin-Middleware in `server.mjs` | jede Browser-Extension-Origin wurde pauschal akzeptiert | in Produktion nur konkrete Extension-Origins, unsicherer Opt-in separat |
|
||||
| Audit/Locks | bestätigt | Audit-Log, `resource_locks`, Adminansicht | keine kritische Deployment-Lücke | erhalten |
|
||||
| Health | teilweise | `/healthz` liefert Prozessstatus | keine Readiness-, DB- oder Schreibpfadprüfung | `/readyz` mit DB- und Volume-Schreibtest |
|
||||
| Shutdown | fehlt | direkter `app.listen()` ohne Signalbehandlung | WAL/Jobs können beim Containerstopp hart beendet werden | SIGTERM/SIGINT, Server-Close, WAL-Checkpoint, DB-Close |
|
||||
| Datenbankpfad | unvollständig | fest `db/vendoo.db` im Codeordner | Container-Image und Nutzerdaten nicht sauber trennbar | `VENDOO_DB_PATH` und zentrale Laufzeitpfade |
|
||||
| Uploadpfad | unvollständig | fest `uploads/` im Codeordner | keine getrennte Persistenz; öffentlich statisch erreichbar | konfigurierbarer Pfad, Zugriff hinter Anmeldung |
|
||||
| Uploadprüfung | Sicherheitsrisiko | Dateiendung **oder** `image/*` genügte | manipulierbarer MIME-Typ konnte beliebige Endungen umgehen | Endung **und** Bild-MIME erforderlich; Größenlimit bleibt aktiv |
|
||||
| Backups | bestätigt, aber codegebunden | verifizierte ZIPs mit DB/Uploads in `operations-center.mjs` | Pfade fest an Projektroot; Live-Volume-Kopie nicht dokumentiert | separate Backup-/Operations-Volumes und sichere Stop/Backup/Start-Anleitung |
|
||||
| Restore/Update | vorhanden, gestuft | Operations Center und `setup.ps1` | Container darf Anwendungscode nicht in-place überschreiben | Docker-Update nur über neues Image; Windows-Update bleibt erhalten |
|
||||
| Docker/Compose | fehlt | keine entsprechenden Dateien | kein reproduzierbarer NAS-/Produktionsbetrieb | Dockerfile, Compose, Healthcheck, non-root, read-only, Volumes |
|
||||
| Reverse Proxy | teilweise | Host/Origin, `PUBLIC_BASE_URL`, `trust proxy` | kein fertiger Produktionsleitfaden | HTTPS-/Proxy-Dokumentation und sichere Standardbindung |
|
||||
| FLUX/ComfyUI | getrennt im Konzept | externer URL-Endpunkt; lokaler Windows-Installer | Bootstrap schreibt in Projektordner und passt nicht zu read-only Container | Container startet standardmäßig ohne FLUX; externer Dienst bleibt optional |
|
||||
| Browser-Erweiterungen | real vorhanden | Chrome/Edge/Firefox/Safari-Strukturen und Pakete | Remote-Kopplung nutzt weiterhin Login-Cookie statt widerrufbarem Extension-Token | CORS gehärtet; Tokenkopplung bleibt geplanter 1.34.0-Slice |
|
||||
| UI-Begriffe/Kernbereiche | weitgehend bestätigt | Navigation, Artikel, Galerie, SmartCopy, Publish und Erweiterungen im realen HTML/JS | vollständige Browser-End-to-End-Abnahme nicht statisch beweisbar | keine unnötige UI-Neuentwicklung in diesem Deployment-Slice |
|
||||
| VTO | entfernt | kein aktiver FASHN-/VTO-Dienst; freier FLUX-Workflow ohne `LoadImage` | historische Dokumente enthalten weiterhin Kontext | keine VTO-Rückkehr |
|
||||
|
||||
## Ergebnis
|
||||
|
||||
1.31.0 besitzt eine echte Mehrbenutzer- und Sicherheitsgrundlage, war aber noch kein sauber containerisierbarer Produktionsstand. Die Hauptlücken lagen in der festen Kopplung von Code und Nutzerdaten, fehlender Readiness, fehlendem geordneten Shutdown, fehlenden Containerdateien sowie zu breiten Extension-Origin- und Uploadregeln.
|
||||
|
Before Width: | Height: | Size: 1.1 MiB |
@@ -1,43 +0,0 @@
|
||||
# Secrets and local configuration
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.docker.example
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.pfx
|
||||
*.crt
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# SQLite runtime data
|
||||
db/*.db
|
||||
db/*.db-shm
|
||||
db/*.db-wal
|
||||
db/*.sqlite
|
||||
db/*.sqlite3
|
||||
|
||||
# User and runtime data
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
backups/
|
||||
logs/
|
||||
tmp/
|
||||
temp/
|
||||
sessions/
|
||||
data/
|
||||
docker-data/
|
||||
*.log
|
||||
|
||||
# OS and editor files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Local overrides
|
||||
compose.override.yaml
|
||||
docker-compose.override.yml
|
||||
@@ -1,270 +0,0 @@
|
||||
## Vendoo 1.12.0 – OpenRouter Diagnostics & Mobile Mediathek
|
||||
|
||||
- Mobiler QR-Upload besitzt jetzt getrennte Aktionen für **Kamera** und **Mediathek**
|
||||
- Mehrfachauswahl aus der Smartphone-Mediathek ohne `capture`-Zwang
|
||||
- HEIC/HEIF werden beim mobilen Upload nach Möglichkeit automatisch in JPG konvertiert
|
||||
- OpenRouter-Free-Erkennung bewertet nur bildrelevante Preisfelder
|
||||
- OpenRouter versucht je Modell mehrere kompatible Chat-Request-Varianten und danach die Images API
|
||||
- Referenzbilder werden nur an Modelle gesendet, die Bildinput unterstützen
|
||||
- Klare Diagnose, wenn aktuell **0 kostenlose Bildmodelle** verfügbar sind
|
||||
- Bei Fehlern werden weiterhin alle passenden Free-Modelle nacheinander versucht; Auto fällt anschließend auf OpenAI zurück
|
||||
|
||||
## Vendoo 1.11.0 – LAN QR Upload, OpenRouter Fallback & Mobile Nav Fix
|
||||
|
||||
- QR-Handy-Upload verwendet automatisch eine erreichbare LAN-IP statt localhost
|
||||
- Vendoo lauscht für lokale Netzwerkzugriffe auf `0.0.0.0` und zeigt erkannte LAN-Adressen beim Start an
|
||||
- Neue Einstellung für `PUBLIC_BASE_URL` inklusive automatischer LAN-IP-Erkennung
|
||||
- OpenRouter probiert bei Fehlern automatisch weitere passende Free-Bildmodelle und danach – im Auto-Modus – OpenAI
|
||||
- OpenRouter-Bildgenerierung unterstützt Chat-Completions und Images-API als zweistufigen Fallback
|
||||
- Mobile Navigation vollständig gehärtet: Overlay, Close-Button, Fokus, Escape, Scroll-Lock und responsive Labels
|
||||
|
||||
## Vendoo 1.10.0 – AI Jobs, 1–3 Varianten & History
|
||||
|
||||
- AI-Bildgenerierung läuft jetzt über eine Job-Queue statt nur synchron
|
||||
- Mindestanzahl auf **1 Bild** gesenkt, Maximum bleibt **3 Bilder**
|
||||
- Neue Modi: **Model**, **Ghost Mannequin**, **Flatlay**
|
||||
- Listing-Editor zeigt jetzt eine **AI-Bild-Historie** zum schnellen Übernehmen früherer Generierungen
|
||||
- Settings erweitert um Standard-Modus für AI-Bilder
|
||||
|
||||
## Vendoo 1.9.0 – AI Provider Settings Center
|
||||
|
||||
- Neuer Settings-Bereich für AI-Model-Fotos mit Provider-Steuerung
|
||||
- Admin kann jetzt `auto`, `openrouter` oder `openai` für Bildgenerierung wählen
|
||||
- OpenRouter-Free-Modelle können direkt aus der UI geladen und geprüft werden
|
||||
- Safety-Flags, Standard-Preset, Variantenanzahl und Fallback-Modell sind in der UI konfigurierbar
|
||||
|
||||
## Vendoo 1.8.0 – OpenRouter Free Image Discovery
|
||||
|
||||
- AI-Model-Fotos unterstützen jetzt einen Provider-Modus `auto`
|
||||
- OpenRouter Free-Bildmodelle werden dynamisch über die Models-API abgefragt und bei Verfügbarkeit bevorzugt genutzt
|
||||
- Fallback auf OpenAI (`gpt-image-1` oder konfiguriertes Modell), wenn keine freien OpenRouter-Modelle verfügbar sind
|
||||
- Neue API-Route für verfügbare OpenRouter-Bildmodelle sowie Persistenz von Provider und Modell je Generierung
|
||||
|
||||
## Vendoo 1.7.0 – AI-Model-Fotos & Safety Layer
|
||||
|
||||
- Neuer sicherer AI-Model-Foto-Workflow für Kleidung ohne Person auf dem Originalfoto
|
||||
- 2–3 KI-generierte Zusatzbilder mit fiktiven erwachsenen Models in Studio-, Indoor- oder Outdoor-Looks
|
||||
- Precheck & Output-Safety: blockiert Unterwäsche, Bademode, transparente Erotik-Looks, Kinderkleidung und Quellen mit Personen
|
||||
- Neue Backend-Routen, Datenbanktabellen und Listing-Integration für Generator und Detailansicht
|
||||
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.6.2] - 2026-07-07
|
||||
|
||||
### Branding
|
||||
|
||||
- neues Vendoo-Hauptlogo in die App-Sidebar integriert
|
||||
- neues Branding auf Login und mobilem QR-Fotoupload integriert
|
||||
- vollständiges Favicon-Paket mit ICO, PNG, Apple Touch Icon und Webmanifest eingebunden
|
||||
- mobile App-Icon-Größen von 16 bis 512 Pixel ergänzt
|
||||
- Chrome-, Edge- und Firefox-Erweiterungen auf die neue Vendoo-Icon-Familie aktualisiert
|
||||
- Vinted Assistant und minimierter Assistant verwenden das neue Logo
|
||||
|
||||
## [1.6.1] - 2026-07-07
|
||||
|
||||
### Verbessert
|
||||
|
||||
- Dashboard ist jetzt der erste Menüpunkt und die Standard-Startseite.
|
||||
- Dashboard-KPI-Karten verwenden eine fließende Auto-Fit-Struktur statt starrer Sechserspalten.
|
||||
- Sparklines besitzen eine reservierte Diagrammfläche und überlagern keine Texte mehr.
|
||||
- Umsatzdiagramme werden bei Größenänderungen automatisch neu berechnet.
|
||||
- Dashboard-Charts passen sich an Notebook-, Tablet- und Mobilbreiten an.
|
||||
- Studio-Flow-Karten verwenden proportionale Bildflächen mit vollständiger `contain`-Darstellung.
|
||||
- Produktbilder werden nicht mehr abgeschnitten und erhalten einen sauberen Fehlerzustand.
|
||||
- Ready-Karten verteilen sich automatisch auf die verfügbare Breite.
|
||||
|
||||
## [1.6.0] - 2026-07-07
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- neuer Sidebar-Menüpunkt **Dashboard** mit interaktiven Kennzahlen, Umsatzverlauf, Plattform- und Kategorieauswertung
|
||||
- Dashboard-Bereiche für Fokusaufgaben, Top Seller, Aktivität, Benachrichtigungen, AI-Qualität und Systemstatus
|
||||
- Fotoarchiv-Endpunkt: mehrere Fotos werden serverseitig als gültiges ZIP erzeugt
|
||||
- Foto-Download im Generator und vollständigen Listing-Editor
|
||||
|
||||
### Geändert
|
||||
|
||||
- Studio Flow vollständig neu strukturiert: Bereit, Geplant, Veröffentlicht, Verkauft und Benötigt Aufmerksamkeit
|
||||
- die fachlich unpassenden sichtbaren Stufen „Neu erfasst“ und „AI-Prüfung“ wurden entfernt
|
||||
- Studio Flow zeigt nur priorisierte Artikel statt einer überfüllten Spaltenwand
|
||||
- geplante Listings, Probleme und Aktivitäten sind in klar getrennten Arbeitsbereichen organisiert
|
||||
- Dashboard und Studio Flow für Notebook, iPad, Tablet und Smartphone neu priorisiert
|
||||
|
||||
### Behoben
|
||||
|
||||
- Publish-Center lädt Listings beim ersten Öffnen mit Wiederholungslogik und verhindert veraltete parallele Renderings
|
||||
- Publish-Ladefehler zeigt jetzt eine konkrete Wiederholen-Aktion statt einer Sackgasse
|
||||
- bei einem Foto erfolgt ein normaler Download, bei mehreren Fotos automatisch ein ZIP-Download
|
||||
|
||||
## [1.5.0] - 2026-07-07
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- Browser-Erweiterungen in getrennten Ordnern für Chrome, Edge und Firefox
|
||||
- vollständiger Design-Relaunch von Popup und Vinted Assistant
|
||||
- neues Vendoo-Link-Icon in 16, 32, 48 und 128 Pixel
|
||||
- QR-basierter Handy-Fotoupload für Generator und Listing-Editor
|
||||
- sichere, zeitlich begrenzte Mobile-Upload-Sitzungen
|
||||
- responsive Layout-Gates für Desktop, Notebook, iPad und Smartphone
|
||||
- umschaltbare Plattformvorschau und HTML-Codeansicht
|
||||
|
||||
### Verbessert
|
||||
|
||||
- Produktbilder der Extension werden vollständig dargestellt
|
||||
- CSRF-Unterstützung für schreibende Extension-Requests
|
||||
- öffentliche Serveradresse statt festem localhost für Extension-Bilder
|
||||
- Plattformvorschau und HTML-Code sind in Generator und Listing-Editor als Umschalter zusammengeführt
|
||||
- Layouts für Desktop, Notebook, iPad und Smartphone neu priorisiert
|
||||
- freigegebene Extension- und Icon-Mockups als verbindliche Designreferenz archiviert
|
||||
|
||||
### Behoben
|
||||
|
||||
- Studio Flow: „Prüfen & bearbeiten“ wechselt jetzt zuverlässig zur sichtbaren Listing-Detailansicht.
|
||||
- Detailaufrufe aus Attention Queue und anderen Bereichen verwenden denselben sicheren Navigationspfad.
|
||||
- Ungültige Listing-IDs werden abgefangen und als Hinweis angezeigt.
|
||||
|
||||
- Windows-Installer behandelt npm `ENOTEMPTY`/`EPERM` robust, bereinigt unvollstaendige `node_modules`, protokolliert npm-Ausgaben und wiederholt die Installation einmal.
|
||||
- Installation nutzt mit vorhandenem Lockfile reproduzierbar `npm ci` statt eines stillen `npm install`.
|
||||
|
||||
### Geplant
|
||||
|
||||
- Lager, Vorlagen, Einstellungen und Admin gemäß Design-Freeze
|
||||
- Docker- und Online-Readiness
|
||||
|
||||
## [1.4.0-full-listing-editor] - 2026-07-07
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- Listings lassen sich jetzt vollständig in einem eigenen Arbeitsbereich bearbeiten
|
||||
- Fotoverwaltung mit Hinzufügen, Entfernen, Cover-Auswahl, Drag-to-Sort und vollständigem Bildeditor ergänzt
|
||||
- WYSIWYG-Editor und HTML-Code sind im Listing-Editor bidirektional und live synchronisiert
|
||||
- Generator zeigt HTML-Code und Plattformvorschau gleichzeitig; Änderungen werden ohne Umschalten übernommen
|
||||
- Plattform-HTML wird während der Bearbeitung live neu gerendert
|
||||
- neue Datenbankspalte `description_html` mit Migration und Rückwärtskompatibilität
|
||||
- neuer API-Endpunkt `POST /api/html-render` für nicht persistierende Live-Vorschauen
|
||||
- Editor-HTML und vollständiges Plattform-HTML können getrennt kopiert werden
|
||||
|
||||
### Sicherheit
|
||||
|
||||
- Rich-Text-HTML wird client- und serverseitig auf eine begrenzte Tag- und Attributmenge reduziert
|
||||
- Plattformvorschauen laufen in sandboxed iframes
|
||||
- Listing-Updates verwenden jetzt eine feste Feld-Allowlist statt beliebiger SQL-Spaltennamen
|
||||
|
||||
### Behoben
|
||||
|
||||
- `photos` werden bei Listing-Updates korrekt als JSON gespeichert
|
||||
- Bildbearbeitung funktioniert nun auch für bereits gespeicherte Listings
|
||||
- formatierte Beschreibungen bleiben nach Speichern und erneutem Öffnen erhalten
|
||||
|
||||
## [1.3.0-publish-center] - 2026-07-07
|
||||
|
||||
### Geändert
|
||||
|
||||
- Publish-Bereich als durchgängiges operatives Publish-Center neu aufgebaut
|
||||
- Smart Copy, Direkt-Publishing, eBay Queue und Zeitplanung in einer gemeinsamen Oberfläche zusammengeführt
|
||||
- bildorientierte Listing-Auswahl mit Suche, Plattform-/Statusfilter, Sortierung und Mehrfachauswahl ergänzt
|
||||
- internes Workspace-Modell statt separater Queue- und Smart-Copy-Modale eingeführt
|
||||
- direkte Listing-Bearbeitung vor Etsy-/eBay-Veröffentlichung integriert
|
||||
- Publish-Bereitschaft, Pflichtfelder, Integrationsstatus und Fotoprüfung sichtbar gemacht
|
||||
- Queue-Status, Schnellaktionen und Publish-Aktivität in einer festen Statusleiste gebündelt
|
||||
- eBay-Fehler, Wiederholungen und geplante Veröffentlichungen inline bedienbar gemacht
|
||||
- responsive Darstellung und Dark Mode für das komplette Publish-Center ergänzt
|
||||
|
||||
### Behoben
|
||||
|
||||
- Batch-Smart-Copy öffnete denselben Vorgang teilweise doppelt
|
||||
- nicht passende Etsy-Batch-Auswahl wird jetzt gefiltert statt blind veröffentlicht
|
||||
- Produktbilder im Publish-Center werden vollständig und ohne Beschnitt dargestellt
|
||||
- Queue- und Planungsaktionen bleiben im Vendoo-Kontext und verlieren nicht den aktuellen Arbeitsstand
|
||||
|
||||
## [1.2.0-listing-studio] - 2026-07-07
|
||||
|
||||
### Geändert
|
||||
|
||||
- Listing-Generator als dreigeteilter Studio-Arbeitsplatz neu aufgebaut
|
||||
- Foto-Cover, Sortierung, Qualitätscheck und vollständige Bildanpassung integriert
|
||||
- AI-Konfiguration mit Plattform, Provider, Modell, Sprache, Varianten und Vorlagen neu strukturiert
|
||||
- artikelspezifische Verkäufernotizen mit Zeichenzähler ergänzt
|
||||
- Ergebnisbereich mit Live-Zählern, Qualitätswert, Attributen, Gebühren und HTML-Vorschau modernisiert
|
||||
- responsive Darstellung und Dark Mode für den Generator vollständig ergänzt
|
||||
|
||||
### API
|
||||
|
||||
- `POST /api/generate` akzeptiert `template_id` und `seller_notes`
|
||||
- globale Hinweise, Vorlagenhinweise und Artikelhinweise werden dedupliziert kombiniert
|
||||
- Standard-Tags aus Vorlagen werden mit AI-Tags zusammengeführt
|
||||
|
||||
### Behoben
|
||||
|
||||
- Upload begrenzt neue Bilder clientseitig auf die verbleibenden Plätze bis maximal zehn
|
||||
- Upload-Fehler werden mit HTTP-Status und Servermeldung korrekt angezeigt
|
||||
- Dateinamen in Vorschaubildern werden URL-sicher ausgegeben
|
||||
|
||||
## [1.0.1-ui-foundation.1] - 2026-07-07
|
||||
|
||||
### Hinzugefügt
|
||||
|
||||
- zentrale Vendoo-Design-Tokens für Light und Dark Mode
|
||||
- neue kompakte App-Shell und Hauptnavigation
|
||||
- Command-Search-Einstieg mit `Ctrl/⌘ + K`
|
||||
- neue Startseite `Studio Flow`
|
||||
- Flow-Stufen: Neu erfasst, AI-Prüfung, Bereit, Geplant, Veröffentlicht, Verkauft
|
||||
- Attention Queue und Activity Stream
|
||||
- kontextueller Artikel-Inspector
|
||||
- neuer Endpunkt `GET /api/dashboard/workflow`
|
||||
- Dokumentation `docs/UI_FOUNDATION_2026.md`
|
||||
- sicherer `.gitignore`
|
||||
|
||||
### Sicherheit
|
||||
|
||||
- `.env`, SQLite-Laufzeitdateien, Uploads und `node_modules` aus dem Übergabepaket entfernt
|
||||
- keine produktiven Zugangsdaten in der bereinigten Version
|
||||
|
||||
## [1.1.1-ui-polish] - 2026-07-07
|
||||
|
||||
### Geändert
|
||||
|
||||
- Etiketten und Artikeldatenblätter öffnen jetzt im internen Vendoo Print Studio statt in einem neuen Browserfenster
|
||||
- maßstabsgetreue A4-Vorschau mit zwei Etikettenformaten, Exemplaren und Preisoption
|
||||
- Druck erfolgt im aktuellen Vendoo-Fenster über den normalen Systemdruckdialog
|
||||
- Studio-Flow-Produktbilder verwenden vollständige automatische Anpassung (`object-fit: contain`)
|
||||
- Inspector- und Attention-Bilder werden ebenfalls ohne Beschnitt dargestellt
|
||||
- Vinted-Extension auf die freigegebene Vendoo-Designsprache 2026 umgestellt
|
||||
- Extension-Popup und eingeblendetes Vinted-Panel modernisiert
|
||||
- Produktbilder der Extension werden sicher über den Background-Service-Worker als Data-URL geladen
|
||||
- Pfade von Upload-Bildern werden korrekt URL-codiert und Bildfehler erhalten einen sichtbaren Zustand
|
||||
- fehlerhaft doppelt codierte deutsche Texte in der Extension repariert
|
||||
|
||||
### Behoben
|
||||
|
||||
- Produktvorschaubilder in der Vinted-Extension wurden durch Seiten-CSP/Mixed-Content-Konstellationen teilweise nicht angezeigt
|
||||
- große oder hochformatige Produktbilder wurden im Studio Flow abgeschnitten
|
||||
|
||||
## [1.1.0-ui-listings] - 2026-07-07
|
||||
|
||||
### Geändert
|
||||
|
||||
- Listings-/Historie-Seite vollständig an die freigegebene Vendoo-Designrichtung 2026 angepasst
|
||||
- neue Filterleiste mit Suche, Plattform, Status und Zeitraum
|
||||
- benutzerdefinierbare sichtbare Tabellenspalten mit lokaler Speicherung
|
||||
- dichte, bildorientierte Listing-Tabelle mit SKU, Plattform, AI-Provider, Preis, Status, Lagerort und Publish-Status
|
||||
- neue Batch-Aktionsleiste für Publish, Nachbearbeitung, Preis, Status, Duplizieren, Etiketten und Löschen
|
||||
- moderne Seitennavigation und Seitengrößensteuerung
|
||||
- neue Exportauswahl für CSV und JSON
|
||||
- verbesserter leerer Zustand und responsive Darstellung
|
||||
- bestehende CRUD-, Export-, Druck-, Status- und Papierkorb-Funktionen erhalten
|
||||
|
||||
## [1.6.1] - 2026-07-07
|
||||
|
||||
### Geändert
|
||||
|
||||
- Vendoo Assistant für Chrome, Edge und Firefox vollständig verdichtet und visuell neu strukturiert.
|
||||
- Queue-Karten zeigen Bild, Status, SKU, Preis und direkte Aktionen ohne große Leerflächen.
|
||||
- „Nur ausfüllen“ ist jetzt die primäre und sichere Einzelaktion.
|
||||
- Daten können in Vinted eingetragen werden, ohne automatisch einen Entwurf anzulegen oder zu veröffentlichen.
|
||||
- „Daten + Bilder ausfüllen“ überträgt Bilder und Felder, führt aber keinen Abschluss aus.
|
||||
- Entwurf und Veröffentlichung liegen bewusst in einem getrennten, aufklappbaren Abschlussbereich.
|
||||
- Popup öffnet Vinted mit einem vorgemerkten Listing und füllt es nach dem Laden aus.
|
||||
- Extension-Version auf 1.4.0 angehoben.
|
||||
@@ -1,81 +0,0 @@
|
||||
# Vendoo — Projekt-Kontext
|
||||
|
||||
Du arbeitest am Projekt "Vendoo" — ein lokaler Multi-Plattform Listing-Generator mit AI, Lagerverwaltung und Publishing-System.
|
||||
|
||||
Lies zuerst `FEATURES.md` im Projektroot — das ist die vollstaendige Feature-Dokumentation mit Tech Stack, Projektstruktur, DB-Schema, allen 80+ API-Endpunkten und 17 Feature-Beschreibungen.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend:** Node.js + Express (ESM, `"type": "module"`)
|
||||
- **Frontend:** Vanilla HTML/CSS/JS — kein Framework, kein Build-Step, kein TypeScript
|
||||
- **Datenbank:** SQLite via `better-sqlite3` (WAL-Modus)
|
||||
- **AI:** Anthropic Claude, OpenAI, OpenRouter, Ollama (lokal)
|
||||
- **Bilder:** `sharp` (Resize, BG-Remove, Watermark)
|
||||
- **Rich-Text:** Quill Editor
|
||||
- **E-Mail:** Nodemailer (SMTP)
|
||||
- **Extension:** Chrome Manifest v3
|
||||
|
||||
## Dateistruktur (wo was hingehoert)
|
||||
|
||||
| Aenderung | Datei(en) |
|
||||
|---|---|
|
||||
| Neue API-Route | `server.mjs` — Route hinzufuegen + Import erweitern |
|
||||
| Neue DB-Funktion | `lib/db.mjs` — Funktion exportieren |
|
||||
| Neue DB-Spalte | `lib/db.mjs` — MIGRATIONS-Array erweitern (try/catch ALTER TABLE Pattern) |
|
||||
| Neues UI-Tab | `public/index.html` (Sidebar-Button + Section), `public/app.js` (TAB_TITLES + switchToTab + Logik), `public/style.css` |
|
||||
| Neuer AI-Provider | `lib/ai-*.mjs` + `lib/ai-router.mjs` erweitern |
|
||||
| Neue Plattform | `platforms/*.mjs` + `lib/categories.mjs` + `lib/fees.mjs` + `lib/html-templates.mjs` |
|
||||
| Styling | `public/style.css` — CSS Custom Properties fuer Theming |
|
||||
|
||||
## Konventionen
|
||||
|
||||
- **Sprache:** UI ist komplett Deutsch
|
||||
- **SKU-Format:** `VD-XXXX` (auto-generiert, 4-stellig aufsteigend)
|
||||
- **Navigation:** Sidebar mit `data-tab` Attributen, Tab-Inhalte als `<section id="..." class="tab-content">`
|
||||
- **Topbar-Titel:** `TAB_TITLES` Map in `app.js` muss fuer neue Tabs erweitert werden
|
||||
- **Dark Mode:** CSS-Variablen unter `[data-theme="dark"]`, niemals hardcoded Farben
|
||||
- **Auth:** Session-basiert, Rollen `admin`/`editor`, CSRF-Token bei state-changing Requests
|
||||
- **DB-Migrationen:** Neue Spalten als Eintrag in `MIGRATIONS`-Array (Zeile ~17-21 in db.mjs):
|
||||
```js
|
||||
const MIGRATIONS = [
|
||||
['spaltenname', 'TEXT'], // oder 'REAL', "TEXT DEFAULT 'wert'"
|
||||
];
|
||||
// Wird automatisch per try/catch ALTER TABLE ausgefuehrt
|
||||
```
|
||||
- **API-Pattern:** REST, JSON-Body, Fehler als `{ error: 'message' }`
|
||||
- **Frontend-API-Aufrufe:** `api(method, path, body)` Helper in app.js (fuegt Auth + CSRF automatisch hinzu)
|
||||
- **Toast-Benachrichtigungen:** `toast('Nachricht')` in app.js
|
||||
- **Quill-Editor:** `initQuill(selector, placeholder)` — gibt Editor-Instanz zurueck
|
||||
|
||||
## Server starten
|
||||
|
||||
```bash
|
||||
npm install # Dependencies installieren
|
||||
npm run dev # Dev-Server mit --watch (Port 8124)
|
||||
npm start # Produktiv-Server
|
||||
```
|
||||
|
||||
Oder: `setup.bat` fuer gefuehrte Installation.
|
||||
|
||||
## Wichtige Dateien nach Groesse
|
||||
|
||||
| Datei | Zeilen | Inhalt |
|
||||
|---|---|---|
|
||||
| `public/app.js` | ~3240 | Gesamte Frontend-Logik |
|
||||
| `server.mjs` | ~1250 | Alle API-Routen + Middleware |
|
||||
| `public/style.css` | ~1200 | Komplettes Styling + Dark Mode + Responsive |
|
||||
| `public/index.html` | ~845 | HTML-Struktur (Sidebar, Tabs, Modals, Forms) |
|
||||
| `lib/db.mjs` | ~390 | Datenbank-Zugriff (alle CRUD-Funktionen) |
|
||||
| `lib/categories.mjs` | ~360 | Kategorie-Baeume pro Plattform |
|
||||
| `lib/auth.mjs` | ~290 | Auth-System (Users, Sessions, Audit) |
|
||||
| `lib/ebay-api.mjs` | ~270 | eBay REST API + OAuth2 |
|
||||
| `lib/mailer.mjs` | ~210 | SMTP E-Mail-Versand |
|
||||
| `lib/etsy-api.mjs` | ~185 | Etsy REST API v3 + OAuth2 PKCE |
|
||||
| `extension/content.js` | ~1170 | Chrome Extension Content Script |
|
||||
|
||||
## Aktueller UI-Stand
|
||||
|
||||
- Version `1.3.0`
|
||||
- UI 2026 Slice 05: Publish-Center
|
||||
- Smart Copy, direkte Veröffentlichung, eBay Queue und Scheduling arbeiten in einer gemeinsamen Oberfläche.
|
||||
- Interne Publish-Arbeitsschritte dürfen keine separaten Browserfenster oder redundanten Modale öffnen.
|
||||
@@ -1,707 +0,0 @@
|
||||
## Slice 18 – Mobile Mediathek & OpenRouter Diagnostics
|
||||
|
||||
- Getrennte Kamera- und Mediathek-Auswahl im QR-Upload
|
||||
- Mobile Mehrfachauswahl sowie HEIC/HEIF-Konvertierung
|
||||
- Robuster OpenRouter-Request-Fallback pro Modell
|
||||
- Dynamische Free-Modell-Diagnose und klarer OpenAI-Fallback-Hinweis
|
||||
|
||||
## Slice 17 – LAN QR, OpenRouter Failover & Mobile Navigation
|
||||
|
||||
- Automatische private LAN-IP-Auswahl mit Adapter-Priorisierung
|
||||
- Konfigurierbare `PUBLIC_BASE_URL` für QR-Upload
|
||||
- Mehrstufiger OpenRouter-Failover über aktuelle Free-Bildmodelle
|
||||
- Chat-Completions- und Images-API-Fallback für OpenRouter
|
||||
- Robuste mobile Off-Canvas-Navigation
|
||||
|
||||
## Slice 16 – AI Job Queue & Multi-Mode Image Generation
|
||||
|
||||
- Queue-basierte AI-Bildgenerierung mit Status-Polling
|
||||
- 1–3 Varianten statt nur 2–3
|
||||
- Modi: Model / Ghost Mannequin / Flatlay
|
||||
- AI-Bildhistorie im Listing-Editor
|
||||
|
||||
## Slice 15 – AI Provider Settings Center
|
||||
|
||||
- UI für AI-Model-Foto-Provider und Free-Model-Discovery
|
||||
- Live-Abfrage aktueller OpenRouter-Bildmodelle aus der Settings-Seite
|
||||
- Konfigurierbare Safety-Flags und Fallback-Logik ohne Codeänderung
|
||||
|
||||
## Slice 14 – OpenRouter Free Image Discovery
|
||||
|
||||
- Dynamic Lookup gegen OpenRouter Models API (`output_modalities=image`)
|
||||
- Filter auf kostenlose Bildmodelle, die sich laufend ändern können
|
||||
- Provider-Automatik: OpenRouter Free zuerst, dann OpenAI-Fallback
|
||||
- Speicherung von `provider` und `provider_model` für spätere Nachvollziehbarkeit
|
||||
|
||||
## Slice 13 – AI-Model-Fotos mit Safety-Filter
|
||||
|
||||
- Sichert den Workflow für Modeartikel ohne Person im Originalbild ab
|
||||
- Erzeugt 1–3 zusätzliche Bilder als Model, Ghost Mannequin oder Flatlay
|
||||
- Blockiert Unterwäsche, Bademode, transparente Erotik-Looks und Kinderkleidung
|
||||
- Speichert Generierungen und erzeugte Bilder in separaten Tabellen für spätere Nachverfolgung
|
||||
|
||||
# Vendoo - Feature-Dokumentation
|
||||
|
||||
> Lokaler Multi-Plattform Listing-Generator mit AI, Lagerverwaltung und Publishing-System.
|
||||
> Stand: 2026-07-07 | Version 1.5.0
|
||||
|
||||
## Design-Freeze 2026
|
||||
|
||||
Die freigegebenen Referenzen in `MockupDesign/` sind die verbindliche UI-/UX-Richtung. Die Startseite wird als **Studio Flow** umgesetzt und bildet den Artikelzyklus `Erfassen → AI-Prüfung → Bereit → Geplant → Veröffentlicht → Verkauft` ab. Gemeinsame Design-Tokens, App-Shell, Navigation, Dark Mode und Kontextpanels werden zentral gepflegt.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Komponente | Technologie |
|
||||
|---|---|
|
||||
| Backend | Node.js + Express (ESM) |
|
||||
| Frontend | Vanilla HTML/CSS/JS (kein Framework, kein Build) |
|
||||
| Datenbank | SQLite via `better-sqlite3` (WAL-Modus) |
|
||||
| AI | Anthropic Claude, OpenAI, OpenRouter, Ollama (lokal) |
|
||||
| Bildverarbeitung | `sharp` (Resize, Hintergrund, Wasserzeichen) |
|
||||
| Rich-Text | Quill Editor |
|
||||
| QR-Codes | QRious |
|
||||
| E-Mail | Nodemailer (SMTP) |
|
||||
| Auth | Scrypt-Hashing, Session-basiert, Magic Links |
|
||||
| Extension | Chrome/Edge Manifest v3, Firefox WebExtension |
|
||||
|
||||
---
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
Vendoo/
|
||||
├── server.mjs # Express-Server (1253 Zeilen, alle API-Routen)
|
||||
├── package.json # Dependencies + Scripts (start, dev)
|
||||
├── setup.bat / setup.ps1 # Windows-Installer (Install/Update/Repair/Uninstall)
|
||||
├── .env / .env.example # Konfiguration (API-Keys, SMTP, Port)
|
||||
├── db/
|
||||
│ ├── schema.sql # DB-Schema (listings, templates, settings, publish_log)
|
||||
│ └── vendoo.db # SQLite-Datenbank
|
||||
├── lib/
|
||||
│ ├── db.mjs # Datenbank-Zugriff (alle CRUD-Funktionen, ~390 Zeilen)
|
||||
│ ├── auth.mjs # Auth-System (Users, Sessions, Rate-Limiting, Audit, ~290 Zeilen)
|
||||
│ ├── ai-router.mjs # AI-Provider-Dispatcher
|
||||
│ ├── ai-claude.mjs # Anthropic Claude Vision API
|
||||
│ ├── ai-openai.mjs # OpenAI GPT-4o Vision API
|
||||
│ ├── ai-openrouter.mjs # OpenRouter Multi-Modell API (~110 Zeilen)
|
||||
│ ├── ai-local.mjs # Ollama lokale Inferenz
|
||||
│ ├── images.mjs # Bildverarbeitung (Resize, BG-Remove, Watermark)
|
||||
│ ├── fees.mjs # Gebührenberechnung pro Plattform
|
||||
│ ├── categories.mjs # Kategorie-Bäume pro Plattform (~360 Zeilen)
|
||||
│ ├── html-templates.mjs # Plattform-spezifische HTML-Beschreibungs-Wrapper
|
||||
│ ├── mailer.mjs # SMTP E-Mail-Versand + Templates (~210 Zeilen)
|
||||
│ ├── etsy-api.mjs # Etsy REST API v3 + OAuth2 PKCE (~185 Zeilen)
|
||||
│ └── ebay-api.mjs # eBay REST API + OAuth2 (~270 Zeilen)
|
||||
├── platforms/
|
||||
│ ├── vinted.mjs # Vinted Prompt-Template + Parsing
|
||||
│ ├── ebay-ka.mjs # eBay Kleinanzeigen
|
||||
│ ├── ebay-de.mjs # eBay.de
|
||||
│ └── etsy.mjs # Etsy
|
||||
├── public/
|
||||
│ ├── index.html # Haupt-UI (Single Page, ~845 Zeilen)
|
||||
│ ├── login.html # Login-Seite (Setup, Magic Link, Passwort)
|
||||
│ ├── style.css # Komplettes Styling inkl. Dark Mode (~1200 Zeilen)
|
||||
│ └── app.js # Frontend-Logik (~3240 Zeilen)
|
||||
├── extension/
|
||||
│ ├── manifest.json # Chrome Extension Manifest v3
|
||||
│ ├── background.js # Service Worker
|
||||
│ ├── popup.html/js # Extension-Popup
|
||||
│ ├── content.js # Content Script für Vinted (~1170 Zeilen)
|
||||
│ ├── content.css # Injiziertes Styling
|
||||
│ └── vinted-categories.js # Vinted-Kategorie-Mapping
|
||||
└── uploads/ # Hochgeladene Produktfotos
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Datenbank-Schema
|
||||
|
||||
### Tabelle: `listings`
|
||||
| Spalte | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | Auto-Increment |
|
||||
| platform | TEXT NOT NULL | vinted, ebay-ka, ebay-de, etsy |
|
||||
| ai_provider | TEXT NOT NULL | claude, openai, openrouter, local |
|
||||
| title | TEXT | Listing-Titel |
|
||||
| description | TEXT | Beschreibungstext |
|
||||
| tags | TEXT (JSON) | Array von Tags/Hashtags |
|
||||
| photos | TEXT (JSON) | Array von Dateinamen |
|
||||
| seller_notes | TEXT | Verkaeuferbemerkungen |
|
||||
| brand | TEXT | Marke |
|
||||
| category | TEXT | Produktkategorie |
|
||||
| size | TEXT | Groesse |
|
||||
| color | TEXT | Farbe |
|
||||
| condition | TEXT | Zustand |
|
||||
| price | REAL | Preis in EUR |
|
||||
| status | TEXT DEFAULT 'active' | active, sold, reserved |
|
||||
| language | TEXT DEFAULT 'de' | Sprache (de,en,fr,nl,pl,es,it,pt) |
|
||||
| sku | TEXT | Artikelnummer (VD-0001 Format) |
|
||||
| storage_location | TEXT | Lagerort (z.B. "Regal A2") |
|
||||
| deleted_at | TEXT | Soft-Delete Timestamp |
|
||||
| created_at | TEXT | Erstellungsdatum |
|
||||
| updated_at | TEXT | Letzte Aenderung |
|
||||
|
||||
### Tabelle: `templates`
|
||||
| Spalte | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | Auto-Increment |
|
||||
| name | TEXT NOT NULL | Vorlagen-Name |
|
||||
| platform | TEXT | Plattform (null = alle) |
|
||||
| seller_notes | TEXT | Verkaeuferbemerkungen |
|
||||
| language | TEXT DEFAULT 'de' | Sprache |
|
||||
| default_tags | TEXT (JSON) | Standard-Tags |
|
||||
| created_at | TEXT | Erstellungsdatum |
|
||||
|
||||
### Tabelle: `publish_log`
|
||||
Tracking aller Veroeffentlichungen (listing_id, platform, status, external_id, external_url, published_at)
|
||||
|
||||
### Tabelle: `ebay_queue`
|
||||
eBay-Publish-Warteschlange (listing_id, status, ebay_item_id, ebay_url, error, retries)
|
||||
|
||||
### Tabelle: `scheduled_publish`
|
||||
Zeitgesteuerte Veroeffentlichungen (listing_id, platform, scheduled_at, status, error)
|
||||
|
||||
### Tabelle: `settings`
|
||||
Key-Value Store (default_platform, default_ai, seller_notes, API-Keys, SMTP-Config)
|
||||
|
||||
### Tabelle: `users`
|
||||
Benutzerverwaltung (email, name, password_hash, role, active, avatar_color, invited_by)
|
||||
|
||||
### Tabelle: `sessions`
|
||||
Auth-Sessions (user_id, token_hash, ip, user_agent, expires_at)
|
||||
|
||||
### Tabelle: `auth_tokens`
|
||||
Magic Links + Einladungen (email, token_hash, type, expires_at, used)
|
||||
|
||||
### Tabelle: `rate_limits`, `audit_log`, `login_history`
|
||||
Sicherheit und Nachverfolgung
|
||||
|
||||
---
|
||||
|
||||
## Features im Detail
|
||||
|
||||
### 1. AI-Listing-Generator (Kernfeature)
|
||||
|
||||
**Was:** Fotos hochladen → AI generiert komplettes Listing (Titel, Beschreibung, Tags, Metadaten)
|
||||
|
||||
**Ablauf:**
|
||||
1. Fotos per Drag & Drop oder Dateiauswahl hochladen (max. 10, je 20MB)
|
||||
2. Plattform waehlen (Vinted, eBay KA, eBay.de, Etsy)
|
||||
3. AI-Provider waehlen (Claude, OpenAI, OpenRouter, Ollama)
|
||||
4. Modell waehlen (bei OpenRouter/Ollama)
|
||||
5. Sprache waehlen (8 Sprachen)
|
||||
6. Varianten waehlen (1-3)
|
||||
7. Optional: Vorlage waehlen (Seller Notes)
|
||||
8. "Listing generieren" klicken
|
||||
9. AI analysiert Fotos und liefert: Titel, Beschreibung, Tags, Marke, Kategorie, Groesse, Farbe, Zustand, Preisvorschlag
|
||||
10. Ergebnis in editierbaren Feldern anzeigen (Quill WYSIWYG-Editor)
|
||||
11. HTML-Vorlage mit plattform-spezifischem Styling generieren
|
||||
12. Gebuehrenberechnung automatisch anzeigen
|
||||
13. Speichern erzeugt Listing mit auto-generierter SKU (VD-XXXX)
|
||||
|
||||
**Dateien:** `server.mjs` (POST /api/generate), `lib/ai-*.mjs`, `platforms/*.mjs`, `public/app.js`
|
||||
|
||||
**API:** `POST /api/generate`, `POST /api/upload`, `GET /api/providers`, `GET /api/platforms`
|
||||
|
||||
---
|
||||
|
||||
### 2. Foto-Management & Editor
|
||||
|
||||
**Upload:**
|
||||
- Drag & Drop Dropzone
|
||||
- Multi-File-Auswahl (nur Bilder: jpg, png, webp, gif)
|
||||
- Vorschau-Thumbnails mit Entfernen-Button
|
||||
- Drag-to-Sort (Reihenfolge aendern)
|
||||
|
||||
**Bildbearbeitung (Canvas-basiert):**
|
||||
- Helligkeit (-100 bis +100)
|
||||
- Kontrast (-100 bis +100)
|
||||
- Saettigung (-100 bis +100)
|
||||
- Drehen (links/rechts 90°)
|
||||
- Spiegeln (horizontal)
|
||||
- Zuschneiden (Crop-Tool)
|
||||
- Zuruecksetzen auf Original
|
||||
- Uebernehmen/Abbrechen
|
||||
|
||||
**Bildtools:**
|
||||
- Hintergrund entfernen (Corner-Sample-Methode, Toleranz 60)
|
||||
- Wasserzeichen hinzufuegen (Text-Overlay, 4% Schriftgroesse, 40% Opazitaet)
|
||||
|
||||
**API:** `POST /api/upload`, `POST /api/images/remove-bg`, `POST /api/images/watermark`
|
||||
|
||||
---
|
||||
|
||||
### 3. Listing-Verwaltung (Historie)
|
||||
|
||||
**Such- und Filteroptionen:**
|
||||
- Volltextsuche (Titel, Beschreibung, Marke) mit 300ms Debounce
|
||||
- Plattform-Filter (Dropdown)
|
||||
- Status-Filter (Aktiv, Verkauft, Reserviert)
|
||||
- Datumsbereich-Filter (Von/Bis)
|
||||
|
||||
**Listenansicht:**
|
||||
- Thumbnail + Titel + SKU-Badge
|
||||
- Plattform + AI-Provider + Preis + Datum
|
||||
- Lagerort-Anzeige (wenn zugewiesen)
|
||||
- Publish-Status-Badges pro Plattform (published/copied/pending)
|
||||
- Inline-Status-Aenderung (Dropdown)
|
||||
- Duplizieren-Button
|
||||
- Loeschen-Button (Soft-Delete → Papierkorb)
|
||||
|
||||
**Batch-Operationen:**
|
||||
- Alle auswaehlen / Einzelauswahl
|
||||
- Ausgewaehlte loeschen
|
||||
- Alle loeschen (gefiltert)
|
||||
- Status massenweise aendern (Aktiv/Verkauft/Reserviert)
|
||||
- Etiketten drucken (ausgewaehlte)
|
||||
|
||||
**Pagination:**
|
||||
- Seitengroesse: 10 / 20 / 50 / Alle
|
||||
- Vor/Zurueck (oben + unten)
|
||||
- Seite X von Y, Gesamtanzahl
|
||||
|
||||
**Export:**
|
||||
- CSV-Export (mit Plattform/Status-Filter)
|
||||
- JSON-Export (alle gefilterten Listings)
|
||||
|
||||
**Detail-Ansicht:**
|
||||
- vollständiger Listing-Arbeitsbereich statt reduziertem Detailformular
|
||||
- Fotogalerie mit großem Cover, Thumbnail-Leiste und vollständiger Bilddarstellung
|
||||
- Fotos hinzufügen, entfernen, per Drag & Drop sortieren und als Cover festlegen
|
||||
- vorhandene Bilder direkt mit Helligkeit, Kontrast, Sättigung, Rotation, Spiegelung und Crop bearbeiten
|
||||
- QR-Code (SKU + ID, via QRious)
|
||||
- Alle Felder editierbar: Plattform, AI-Provider, Sprache, SKU, Status, Titel, Tags, Verkäufernotizen, Marke, Kategorie, Groesse, Farbe, Zustand, Preis, Lagerort
|
||||
- Beschreibung gleichzeitig im Quill-WYSIWYG und im HTML-Code bearbeiten
|
||||
- WYSIWYG- und HTML-Codeansicht sind live und bidirektional synchronisiert
|
||||
- separate sandboxed Plattformvorschau
|
||||
- Editor-HTML und vollständiges Plattform-HTML separat kopierbar
|
||||
- Aenderungen speichern
|
||||
- Alles kopieren
|
||||
- Etikett erstellen
|
||||
- Datenblatt erstellen
|
||||
- Loeschen
|
||||
|
||||
**API:** `GET/POST/PUT/DELETE /api/listings`, `POST /api/listings/:id/duplicate`, `GET /api/export/csv`
|
||||
|
||||
---
|
||||
|
||||
### 4. Vorlagen-System (Templates)
|
||||
|
||||
**3 vordefinierte Vorlagen (Seed):**
|
||||
1. "Sneaker & Schuhe" (Vinted, DE) — Versand-/Zustandshinweise + Sneaker-Tags
|
||||
2. "Vintage Kleidung" (Vinted, DE) — Vintage-spezifische Hinweise + Retro-Tags
|
||||
3. "Elektronik & Zubehoer" (eBay.de, DE) — Funktionshinweise + Technik-Tags
|
||||
|
||||
**Verwaltung:**
|
||||
- Liste aller Vorlagen (Name, Plattform, Sprache)
|
||||
- Erstellen: Name, Plattform (optional), Sprache, Seller Notes (Quill), Standard-Tags
|
||||
- Bearbeiten / Loeschen
|
||||
- Vorlagen-Auswahl im Generator (Dropdown)
|
||||
- Bei Auswahl: uebernimmt Plattform + Sprache + Seller Notes + Tags
|
||||
|
||||
**API:** `GET/POST/PUT/DELETE /api/templates`
|
||||
|
||||
---
|
||||
|
||||
### 5. Publishing-System
|
||||
|
||||
**UI 2026 / Slice 05 – Publish-Center:**
|
||||
- gemeinsame Arbeitsoberfläche für Smart Copy, Direkt-Publishing, eBay Queue und Zeitplanung
|
||||
- bildorientierte Listing-Auswahl mit Suche, Filtern, Sortierung und Mehrfachauswahl
|
||||
- inline bearbeitbare Publish-Daten mit Pflichtfeld- und Integrationsprüfung
|
||||
- Queue-Status, Fehlerwiederholung, Aktivitätsverlauf und Schnellaktionen in einer Statusleiste
|
||||
- keine separaten Browserfenster für interne Publish-Arbeitsschritte
|
||||
|
||||
**Smart Copy (manuelles Veroeffentlichen):**
|
||||
- Schritt-fuer-Schritt-Anleitung pro Plattform
|
||||
- Fotogalerie mit Download
|
||||
- Kopierbare Felder: Titel, Beschreibung (Text + HTML), Tags, Preis, Metadaten
|
||||
- "Plattform oeffnen" Button (Direkt-URL)
|
||||
- "Als veroeffentlicht markieren" Button
|
||||
|
||||
**Direkte API-Veroeffentlichung:**
|
||||
- Etsy: OAuth2 + PKCE → Listing erstellen + Bilder hochladen
|
||||
- eBay.de: OAuth2 → Inventory Item + Offer + Publish
|
||||
|
||||
**eBay-Warteschlange:**
|
||||
- Listings zur Queue hinzufuegen (Batch)
|
||||
- Queue-Ansicht: SKU, Titel, Status, Fehler
|
||||
- Alle verarbeiten / Fehlgeschlagene wiederholen / Queue leeren
|
||||
- Status-Tracking: pending → uploading → published/failed
|
||||
|
||||
**Zeitgesteuerte Veroeffentlichung:**
|
||||
- Datum + Uhrzeit waehlen
|
||||
- Plattform waehlen
|
||||
- Batch-Scheduling moeglich
|
||||
- Uebersicht aller geplanten Veroeffentlichungen
|
||||
- Background-Job prüft alle 60 Sekunden auf faellige Items
|
||||
|
||||
**Publish-Status-Tracking:**
|
||||
- Publish-Log pro Listing + Plattform
|
||||
- Status-Badges in Historie + Publish-Ansicht
|
||||
|
||||
**API:** `POST /api/publish/smart-copy`, `POST /api/publish/mark`, `POST /api/publish/etsy`, `POST /api/ebay/publish/:id`, `GET/POST/DELETE /api/ebay/queue`, `GET/POST/DELETE /api/schedule`
|
||||
|
||||
---
|
||||
|
||||
### 6. Lagerverwaltung (Inventory)
|
||||
|
||||
**Uebersicht:**
|
||||
- Statistik-Karten: Artikel gesamt, Lagerorte, Aktive Artikel, Lagerwert (EUR)
|
||||
- Lagerort-Kacheln mit: Name, Artikelanzahl (aktiv/verkauft/reserviert), Gesamtwert
|
||||
- "Ohne Lagerort" Kachel fuer nicht zugewiesene Artikel
|
||||
- Neuen Lagerort anlegen
|
||||
|
||||
**Detail-Ansicht (pro Lagerort):**
|
||||
- Artikelliste mit Checkboxen
|
||||
- SKU, Titel, Plattform, Preis, Status
|
||||
- Lagerort umbenennen
|
||||
- Ausgewaehlte Artikel an anderen Ort verschieben (Bulk)
|
||||
|
||||
**Integration:**
|
||||
- Lagerort-Feld in Listing-Detail editierbar
|
||||
- Lagerort-Feld im Generator (bei Erstellung setzbar)
|
||||
- Lagerort in Historie-Listenansicht sichtbar
|
||||
|
||||
**API:** `GET /api/inventory/locations`, `GET /api/inventory/unassigned`, `GET /api/inventory/location/:loc`, `PUT /api/inventory/bulk-location`
|
||||
|
||||
---
|
||||
|
||||
### 7. SKU / Artikelnummer-System
|
||||
|
||||
- Auto-Generierung: VD-0001, VD-0002, ... (4-stellig, aufsteigend)
|
||||
- Backfill fuer bestehende Listings ohne SKU
|
||||
- SKU in Detail-Ansicht editierbar
|
||||
- SKU in QR-Codes kodiert (JSON: {sku, id})
|
||||
- SKU auf Etiketten und Datenblaettern
|
||||
|
||||
---
|
||||
|
||||
### 8. Gebuehrenrechner
|
||||
|
||||
**Plattform-Gebuehren:**
|
||||
| Plattform | Provision | Fixkosten |
|
||||
|---|---|---|
|
||||
| Vinted | 5% Kaeuferschutz | + 0,70 EUR |
|
||||
| eBay Kleinanzeigen | 0% | Kostenlos |
|
||||
| eBay.de | 11% Verkaufsprovision | + 0,35 EUR |
|
||||
| Etsy | 6,5% + 4% Payment | + 0,30 + 0,20 EUR |
|
||||
|
||||
- Echtzeit-Berechnung bei Preiseingabe
|
||||
- Zeigt: Brutto, Gebuehren, Netto pro Plattform
|
||||
- Auch im Generator bei Preisvorschlag
|
||||
|
||||
**API:** `GET /api/fees`
|
||||
|
||||
---
|
||||
|
||||
### 9. Kategorie-System
|
||||
|
||||
- Hierarchischer Kategorie-Picker (Baum mit Gruppen/Untergruppen)
|
||||
- Suchfunktion mit Scoring (Name + Pfad)
|
||||
- Plattform-spezifische Kategorien (Vinted, eBay KA, eBay.de, Etsy)
|
||||
- Caching fuer Performance
|
||||
- Ausgewaehlte Kategorie mit Clear-Button
|
||||
|
||||
**API:** `GET /api/categories/:platform`, `GET /api/categories/:platform/match`
|
||||
|
||||
---
|
||||
|
||||
### 10. Druck-Funktionen
|
||||
|
||||
**Etiketten (Labels):**
|
||||
- 2-Spalten-Grid, A4-druckfertig
|
||||
- QR-Code + SKU + Titel (40 Zeichen) + Plattform + Preis
|
||||
- Batch-Druck fuer mehrere Listings
|
||||
|
||||
**Datenblaetter:**
|
||||
- Volle Listing-Details auf A4
|
||||
- QR-Code + bis zu 4 Fotos
|
||||
- Beschreibung (200 Zeichen) + Metadaten-Tabelle
|
||||
|
||||
---
|
||||
|
||||
### 11. Auth & Benutzerverwaltung
|
||||
|
||||
**Authentifizierung:**
|
||||
- Passwort-Login (Scrypt-Hashing)
|
||||
- Magic Links per E-Mail (15 Min gueltig)
|
||||
- Setup-Modus (erster Admin erstellt Konto)
|
||||
- CSRF-Token-Validierung
|
||||
- Rate-Limiting (5 Versuche / 15 Min)
|
||||
- Session-basiert (3 Tage Ablauf, automatische Bereinigung)
|
||||
|
||||
**Benutzerverwaltung (Admin):**
|
||||
- Benutzer einladen (Magic Link oder Auto-Passwort)
|
||||
- Rollen: Admin, Editor
|
||||
- Benutzer aktivieren/deaktivieren
|
||||
- Passwort zuruecksetzen
|
||||
- Login-Verlauf einsehen
|
||||
- Sessions verwalten (eigene + alle)
|
||||
- Einladung erneut senden
|
||||
|
||||
**Audit-Log:**
|
||||
- Alle Aktionen protokolliert (Login, CRUD, Settings, etc.)
|
||||
- Benutzer, Aktion, Ziel, IP, Timestamp
|
||||
- Paginierte Ansicht
|
||||
|
||||
**API:** `POST /auth/login`, `POST /auth/setup`, `POST /auth/invite`, `GET /api/me`, Admin-Routen unter `/api/admin/*`
|
||||
|
||||
---
|
||||
|
||||
### 12. E-Mail / SMTP
|
||||
|
||||
- Konfigurierbar ueber Admin-Panel oder .env
|
||||
- SMTP-Verbindungstest
|
||||
- Magic-Link-Versand
|
||||
- Einladungs-E-Mails mit Passwort
|
||||
- Gebrandetes HTML-Template (Vendoo-Header)
|
||||
- Fallback: Console-Output wenn SMTP nicht konfiguriert
|
||||
|
||||
**API:** `GET/PUT /api/admin/smtp`, `POST /api/admin/smtp/test`
|
||||
|
||||
---
|
||||
|
||||
### 13. System & Backup (Admin)
|
||||
|
||||
**System-Info:**
|
||||
- Node.js Version, Uptime, RAM-Verbrauch
|
||||
- Datenbank-Groesse
|
||||
- Benutzer- und Listing-Anzahl
|
||||
- Aktive Sessions
|
||||
|
||||
**Backup & Restore:**
|
||||
- DB-Download als .db-Datei
|
||||
- DB-Restore per Upload (erstellt Backup der alten DB)
|
||||
- Abgelaufene Sessions bereinigen
|
||||
|
||||
**API:** `GET /api/admin/system`, `GET /api/backup`, `POST /api/restore`
|
||||
|
||||
---
|
||||
|
||||
### 14. Chrome Extension
|
||||
|
||||
**Zweck:** Automatisches Ausfuellen von Vinted-Listing-Formularen
|
||||
|
||||
**Features:**
|
||||
- Verbindung zum Vendoo-Server (localhost:8124)
|
||||
- Queue-Verwaltung: naechstes Listing aus Warteschlange
|
||||
- Auto-Fill: Titel, Beschreibung, Fotos, Kategorie, Metadaten
|
||||
- Vinted-Kategorie-Mapping (337 Zeilen)
|
||||
- Status-Badge (Connected/Offline)
|
||||
- Batch-Verarbeitung mit konfigurierbarem Delay (Standard 45s)
|
||||
|
||||
**Host Permissions:** localhost:8124, vinted.de/.fr/.co.uk/.com
|
||||
|
||||
---
|
||||
|
||||
### 15. Einstellungen
|
||||
|
||||
- API-Keys: Anthropic, OpenAI, OpenRouter (maskiert angezeigt)
|
||||
- Ollama-URL (Standard: localhost:11434)
|
||||
- Etsy API Key + OAuth-Verbindung
|
||||
- eBay Client ID/Secret/RuName + OAuth + Sandbox-Toggle
|
||||
- Standard-Plattform, Standard-AI, Standard-Sprache
|
||||
- Verkaeuferbemerkungen (global, in jedes Listing eingefuegt)
|
||||
|
||||
**API:** `GET/PUT /api/settings`
|
||||
|
||||
---
|
||||
|
||||
### 16. UI / Design
|
||||
|
||||
- **Sidebar-Navigation** (links, 230px, fixed) mit 4 Gruppen: Allgemein, Listings, Tools, System
|
||||
- **Topbar** mit dynamischem Titel, Suchen-Button, "Neues Listing"-Button
|
||||
- **Dark Mode** (Toggle im Sidebar-Footer, localStorage-Persistenz, System-Praeferenz)
|
||||
- **Responsive** (Mobile: Sidebar versteckt, Hamburger-Menue, Overlay)
|
||||
- **Toast-Benachrichtigungen** (2,5s)
|
||||
- **Browser-Notifications** (Listing gespeichert, eBay Queue, Scheduled)
|
||||
- **Quill WYSIWYG-Editor** (Toolbar: H1-H3, Bold, Italic, Underline, Strike, Listen, Links, Farben)
|
||||
- **Live-HTML-Workbench**: WYSIWYG, editierbarer HTML-Code und Plattformvorschau gleichzeitig sichtbar
|
||||
|
||||
---
|
||||
|
||||
### 17. Installer (setup.bat + setup.ps1)
|
||||
|
||||
**Menue-Optionen:**
|
||||
1. Installieren (Node.js pruefen/installieren, npm install, .env erstellen, Ordner)
|
||||
2. Starten (Server + Browser oeffnen)
|
||||
3. Update (Dependencies aktualisieren, neue .env-Eintraege)
|
||||
4. Reparieren (node_modules loeschen + neu installieren)
|
||||
5. Deinstallieren (node_modules + optional Daten entfernen)
|
||||
6. Status (Node/npm/Dependencies/.env/DB/Uploads pruefen)
|
||||
|
||||
**CLI-Modus:** `setup.bat install`, `setup.bat update`, etc.
|
||||
|
||||
---
|
||||
|
||||
## API-Endpunkt-Uebersicht
|
||||
|
||||
### Auth
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| POST | /auth/setup | Erstes Admin-Konto erstellen |
|
||||
| POST | /auth/login | Anmelden |
|
||||
| POST | /auth/logout | Abmelden |
|
||||
| POST | /auth/invite | Einladung senden |
|
||||
| POST | /auth/accept-invite | Einladung annehmen |
|
||||
| GET | /auth/magic | Magic-Link-Redirect |
|
||||
| GET | /auth/setup-check | Setup-Modus pruefen |
|
||||
| GET | /auth/etsy | Etsy OAuth starten |
|
||||
| GET | /auth/etsy/callback | Etsy OAuth Callback |
|
||||
| GET | /auth/ebay | eBay OAuth starten |
|
||||
| GET | /auth/ebay/callback | eBay OAuth Callback |
|
||||
|
||||
### Listings
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| GET | /api/listings | Alle Listings (q, platform, status) |
|
||||
| GET | /api/listings/:id | Einzelnes Listing |
|
||||
| POST | /api/listings | Listing erstellen |
|
||||
| PUT | /api/listings/:id | Listing aktualisieren |
|
||||
| DELETE | /api/listings/:id | Listing loeschen (Soft-Delete) |
|
||||
| POST | /api/listings/:id/duplicate | Listing duplizieren |
|
||||
|
||||
### Generator & AI
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| POST | /api/generate | AI-Listing generieren |
|
||||
| POST | /api/upload | Fotos hochladen |
|
||||
| GET | /api/providers | AI-Provider + Modelle |
|
||||
| GET | /api/platforms | Marktplatz-Plattformen |
|
||||
| GET | /api/categories/:platform | Kategorie-Baum |
|
||||
| GET | /api/categories/:platform/match | Kategorie-Suche |
|
||||
| GET | /api/price-suggest | Preisvorschlag |
|
||||
|
||||
### Bilder
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| POST | /api/images/remove-bg | Hintergrund entfernen |
|
||||
| POST | /api/images/watermark | Wasserzeichen hinzufuegen |
|
||||
|
||||
### Templates
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| GET | /api/templates | Alle Vorlagen |
|
||||
| POST | /api/templates | Vorlage erstellen |
|
||||
| PUT | /api/templates/:id | Vorlage aktualisieren |
|
||||
| DELETE | /api/templates/:id | Vorlage loeschen |
|
||||
|
||||
### Publishing
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| GET | /api/publish/info/:id | Publish-Status + Verfuegbarkeit |
|
||||
| POST | /api/publish/smart-copy | Smart-Copy-Daten |
|
||||
| POST | /api/publish/mark | Als veroeffentlicht markieren |
|
||||
| GET | /api/publish/status | Batch-Status-Abfrage |
|
||||
| POST | /api/publish/etsy | Direkt auf Etsy veroeffentlichen |
|
||||
| POST | /api/ebay/publish/:id | Direkt auf eBay veroeffentlichen |
|
||||
|
||||
### eBay Queue
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| GET | /api/ebay/queue | Warteschlange anzeigen |
|
||||
| POST | /api/ebay/queue | Listings hinzufuegen |
|
||||
| DELETE | /api/ebay/queue/:id | Aus Queue entfernen |
|
||||
| DELETE | /api/ebay/queue | Queue leeren |
|
||||
| POST | /api/ebay/queue/process | Alle verarbeiten |
|
||||
| POST | /api/ebay/queue/retry | Fehlgeschlagene wiederholen |
|
||||
| GET | /api/ebay/policies | eBay-Richtlinien abrufen |
|
||||
|
||||
### Scheduling
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| GET | /api/schedule | Geplante Veroeffentlichungen |
|
||||
| POST | /api/schedule | Veroeffentlichung planen |
|
||||
| DELETE | /api/schedule/:id | Geplante entfernen |
|
||||
|
||||
### Inventory
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| GET | /api/inventory/locations | Lagerorte mit Statistiken |
|
||||
| GET | /api/inventory/unassigned | Artikel ohne Lagerort |
|
||||
| GET | /api/inventory/location/:loc | Artikel pro Lagerort |
|
||||
| PUT | /api/inventory/bulk-location | Bulk-Lagerort-Zuweisung |
|
||||
|
||||
### Papierkorb
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| GET | /api/trash | Geloeschte Listings |
|
||||
| GET | /api/trash/count | Anzahl im Papierkorb |
|
||||
| POST | /api/trash/:id/restore | Wiederherstellen |
|
||||
| DELETE | /api/trash/:id | Endgueltig loeschen |
|
||||
| DELETE | /api/trash | Papierkorb leeren |
|
||||
|
||||
### Gebuehren & Export
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| GET | /api/fees | Gebuehren berechnen |
|
||||
| GET | /api/export/csv | CSV-Export |
|
||||
| GET | /api/dashboard/stats | Dashboard-Statistiken |
|
||||
| GET | /api/html-templates | HTML-Beschreibungs-Templates |
|
||||
| POST | /api/html-wrap | Gespeichertes Listing in Plattform-HTML wrappen |
|
||||
| POST | /api/html-render | Nicht persistierende Live-HTML-Vorschau rendern |
|
||||
|
||||
### Mobiler Foto-Upload
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| POST | /api/mobile-upload-sessions | Zeitlich begrenzte Upload-Sitzung und QR-Link erzeugen |
|
||||
| GET | /api/mobile-upload-sessions/:id | Upload-Sitzung vom Desktop pollen |
|
||||
| DELETE | /api/mobile-upload-sessions/:id | Upload-Sitzung schließen |
|
||||
| GET | /mobile-upload/:token | Responsive Smartphone-Uploadseite |
|
||||
| GET | /mobile-upload-api/:token | Öffentlichen Sitzungsstatus abrufen |
|
||||
| POST | /mobile-upload-api/:token/photos | Fotos mit Bearer-Token aus dem QR-Link hochladen |
|
||||
|
||||
### Einstellungen & System
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| GET | /api/settings | Alle Einstellungen |
|
||||
| PUT | /api/settings | Einstellungen speichern |
|
||||
| GET | /api/me | Aktueller Benutzer |
|
||||
| PUT | /api/me | Profil aktualisieren |
|
||||
| GET | /api/backup | DB-Backup herunterladen |
|
||||
| POST | /api/restore | DB wiederherstellen |
|
||||
| GET | /api/etsy/status | Etsy-Verbindungsstatus |
|
||||
| GET | /api/ebay/status | eBay-Verbindungsstatus |
|
||||
|
||||
### Admin (erfordert Admin-Rolle)
|
||||
| Methode | Route | Beschreibung |
|
||||
|---|---|---|
|
||||
| GET | /api/admin/users | Alle Benutzer |
|
||||
| POST | /api/admin/users/invite | Benutzer einladen |
|
||||
| PUT | /api/admin/users/:id | Benutzer bearbeiten |
|
||||
| DELETE | /api/admin/users/:id | Benutzer loeschen |
|
||||
| GET | /api/admin/sessions | Alle Sessions |
|
||||
| DELETE | /api/admin/sessions/:id | Session beenden |
|
||||
| GET | /api/admin/audit | Audit-Log |
|
||||
| GET | /api/admin/system | System-Info |
|
||||
| GET/PUT | /api/admin/smtp | SMTP-Konfiguration |
|
||||
| POST | /api/admin/smtp/test | SMTP testen |
|
||||
|
||||
## Slice 07 – Cross-Browser Extensions & Mobile Upload
|
||||
|
||||
- `extensions/chrome`, `extensions/edge` und `extensions/firefox`
|
||||
- Vendoo Link Icon-Set in vier Browsergrößen
|
||||
- neues Popup und neues In-Page-Panel im freigegebenen Vendoo-Design
|
||||
- Plattformvorschau/HTML als Umschalter im Generator und Listing-Editor
|
||||
- responsive Layouts für Desktop, Notebook, iPad und Smartphone
|
||||
- QR-Code-Upload vom Smartphone mit 20 Minuten gültiger Upload-Sitzung
|
||||
- Handy-Fotos erscheinen automatisch im Generator beziehungsweise geöffneten Listing
|
||||
|
||||
## Extension UX 1.4.0
|
||||
|
||||
- Kompakte Queue-Karten mit Produktbild, Preis, SKU und Status
|
||||
- Direkte Einzelaktion „Nur ausfüllen“ ohne Entwurf und ohne Veröffentlichung
|
||||
- Option „Daten + Bilder ausfüllen“ ohne Abschlussaktion
|
||||
- Explizit getrennte Aktionen für Entwurf und Veröffentlichung
|
||||
- Gleichstand der Chrome-, Edge- und Firefox-Unterordner
|
||||
|
||||
|
||||
## Interaktives Dashboard und Studio Flow V2
|
||||
|
||||
- eigenständiger Dashboard-Menüpunkt mit realen Kennzahlen und interaktiven SVG/CSS-Diagrammen
|
||||
- Studio Flow verwendet die verständlichen Zustände Bereit, Geplant, Veröffentlicht, Verkauft und Benötigt Aufmerksamkeit
|
||||
- mehrere Fotodateien werden automatisch als ZIP heruntergeladen
|
||||
- Publish-Center verfügt über Retry- und Stale-Render-Schutz beim ersten Laden
|
||||
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -1,94 +0,0 @@
|
||||
## Neu in 1.12.0
|
||||
|
||||
Der mobile QR-Upload bietet nun zwei eindeutige Wege: **Kamera öffnen** und **Mediathek auswählen**. Dadurch erzwingt das Smartphone nicht mehr ausschließlich die Kamera. Mehrere vorhandene Bilder können direkt gewählt werden; HEIC/HEIF werden nach Möglichkeit serverseitig zu JPG konvertiert.
|
||||
|
||||
OpenRouter wurde zusätzlich gehärtet: Vendoo erkennt Free-Bildmodelle präziser, prüft Bildinput-Fähigkeit, versucht mehrere kompatible Request-Formen und wechselt bei Fehlern automatisch zum nächsten Modell. Sind aktuell keine kostenlosen Bildmodelle vorhanden, zeigt die Diagnose das ausdrücklich an.
|
||||
|
||||
## Neu in 1.11.0
|
||||
|
||||
Der QR-Handy-Upload erzeugt jetzt automatisch Links mit der erkannten LAN-IP des Vendoo-PCs. Unter **Einstellungen → Vendoo LAN-Adresse für QR-Upload** kann die Adresse erkannt, geprüft und dauerhaft gespeichert werden. Die mobile Navigation wurde für Smartphone und Tablet komplett stabilisiert. Bei OpenRouter-Bildgenerierung versucht Vendoo automatisch mehrere passende Free-Modelle nacheinander und nutzt im Auto-Modus anschließend OpenAI als Fallback.
|
||||
|
||||
## Neu in 1.10.0
|
||||
|
||||
Vendoo unterstützt jetzt eine **AI-Job-Queue** für Bildgenerierung. Kleidung kann mit **1 bis 3** Varianten als **Model**, **Ghost Mannequin** oder **Flatlay** generiert werden. Im Listing-Editor gibt es zusätzlich eine **Historie früherer AI-Generierungen**, damit erzeugte Bilder schnell wiederverwendet werden können.
|
||||
|
||||
## Neu in 1.9.0
|
||||
|
||||
Die Einstellungen wurden um ein eigenes Steuerzentrum für **AI-Model-Fotos** erweitert. Dort lassen sich Provider, Free-Filter, Safety-Blocker, Standard-Preset, Variantenanzahl und das OpenAI-Fallback-Modell pflegen. Zusätzlich kann die UI die aktuell verfügbaren OpenRouter-Bildmodelle direkt laden und anzeigen.
|
||||
|
||||
## Neu in 1.8.0
|
||||
|
||||
Vendoo kann jetzt neben OpenAI auch OpenRouter für AI-Model-Fotos verwenden. Wenn `ai_model_photo_provider=auto` gesetzt ist und `OPENROUTER_API_KEY` vorhanden ist, fragt Vendoo automatisch aktuelle Bildmodelle von OpenRouter ab, filtert auf Bildausgabe und – sofern aktiviert – nur kostenlose Varianten. Gibt es kein passendes freies Modell mehr, fällt Vendoo sauber auf OpenAI zurück.
|
||||
|
||||
### Neue relevante Settings
|
||||
|
||||
- `ai_model_photo_provider`: `auto`, `openrouter`, `openai`
|
||||
- `ai_model_photo_openrouter_free_only`: `1` oder `0`
|
||||
- `ai_model_photo_openrouter_model`: optional feste OpenRouter-Model-ID
|
||||
- `ai_model_photo_openai_model`: OpenAI-Fallback-Modell, Default `gpt-image-1`
|
||||
|
||||
### Neue ENV-Variablen
|
||||
|
||||
- `OPENROUTER_API_KEY`
|
||||
- optional `OPENROUTER_SITE_URL`
|
||||
- optional `OPENROUTER_APP_NAME`
|
||||
|
||||
## Neu in 1.7.0
|
||||
|
||||
Vendoo kann jetzt für geeignete Kleidungsstücke sichere AI-Model-Fotos erzeugen. Der Workflow prüft zuerst, ob ein Kleidungsartikel ohne Person vorliegt, blockiert sensible Kategorien und erstellt danach 1–3 zusätzliche Lifestyle-/Studio-Bilder mit fiktiven erwachsenen Personen oder alternative Ghost-Mannequin-/Flatlay-Varianten. Die Bilder lassen sich direkt in Generator und Listing-Editor übernehmen.
|
||||
|
||||
# Vendoo
|
||||
|
||||
Lokaler Multi-Plattform Listing-Generator mit AI, Lagerverwaltung und Publishing-System.
|
||||
|
||||
## Aktueller Entwicklungsstand
|
||||
|
||||
- Version: `1.5.0`
|
||||
- UI-Slice: `UI 2026 / Slice 07 – Cross-Browser Extensions, Responsive UI & Mobile Upload`
|
||||
- Startseite: `Studio Flow`
|
||||
- Tech Stack: Node.js, Express (ESM), Vanilla JS, SQLite (`better-sqlite3`)
|
||||
|
||||
Die vollständige Funktionsbeschreibung steht in [`FEATURES.md`](FEATURES.md). Die freigegebene Designrichtung liegt unter [`MockupDesign/`](MockupDesign/). Der vollständige Listing-Editor ist in [`docs/FULL_LISTING_EDITOR_2026.md`](docs/FULL_LISTING_EDITOR_2026.md) dokumentiert.
|
||||
|
||||
## Lokaler Start
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
Danach ist Vendoo standardmäßig unter `http://localhost:8124` erreichbar.
|
||||
|
||||
## Wichtige Sicherheitsregel
|
||||
|
||||
Nicht versionieren oder weitergeben:
|
||||
|
||||
- `.env`
|
||||
- `db/vendoo.db*`
|
||||
- `uploads/`
|
||||
- Backups und Logs
|
||||
- API-Schlüssel, OAuth-Tokens und SMTP-Passwörter
|
||||
|
||||
Die bereitgestellte Projektversion enthält ausschließlich `.env.example` und keine produktiven Laufzeitdaten.
|
||||
|
||||
## Browser-Erweiterungen
|
||||
|
||||
Die aktuellen Erweiterungen liegen getrennt unter `extensions/chrome`, `extensions/edge` und `extensions/firefox`. Der frühere Ordner `extension` ist nur noch ein Migrationshinweis.
|
||||
|
||||
## Mobiler Foto-Upload
|
||||
|
||||
Im Generator und im Listing-Editor kann ein zeitlich begrenzter QR-Code erzeugt werden. Nach dem Scan können Fotos direkt vom Smartphone aufgenommen und ohne Neuladen an Vendoo übertragen werden. Für andere Geräte muss Vendoo über eine erreichbare LAN-IP oder `PUBLIC_BASE_URL` geöffnet sein; `localhost` funktioniert nur auf demselben Gerät.
|
||||
|
||||
## Browser-Erweiterung 1.4.0
|
||||
|
||||
Die Erweiterungen für Chrome, Edge und Firefox verwenden jetzt eine kompakte, aktionsorientierte Oberfläche. Die primäre Aktion **Nur ausfüllen** trägt ein Listing in das geöffnete Vinted-Formular ein, ohne einen Entwurf zu speichern und ohne eine Veröffentlichung auszulösen. Abschlussaktionen sind davon deutlich getrennt.
|
||||
|
||||
|
||||
## Aktueller UI-Stand
|
||||
|
||||
Slice 10 ergänzt das interaktive Dashboard, den aufgeräumten Studio Flow, robuste Publish-Initialisierung, ZIP-Fotodownloads und erweiterte Responsive-Gates.
|
||||
|
||||
|
||||
## Branding
|
||||
|
||||
Logo, App-Icons, mobile Größen und Favicons liegen unter `public/brand/`. Die Browser-Erweiterungen für Chrome, Edge und Firefox verwenden dieselbe Icon-Familie.
|
||||
@@ -1,19 +0,0 @@
|
||||
# Sicherheit
|
||||
|
||||
## Vertrauliche Dateien
|
||||
|
||||
Folgende Dateien dürfen niemals in Git oder öffentliche Pakete gelangen:
|
||||
|
||||
- `.env`
|
||||
- `db/*.db`, `db/*.db-wal`, `db/*.db-shm`
|
||||
- `uploads/`
|
||||
- Backups, Logs und Session-Daten
|
||||
- private Schlüssel und Zertifikate
|
||||
|
||||
## Online-Betrieb
|
||||
|
||||
Vendoo darf nicht durch eine direkte Portfreigabe von Port 8124 veröffentlicht werden. Für öffentliche Nutzer ist eine getrennte Server-/VPS-Instanz hinter HTTPS und Reverse Proxy vorgesehen.
|
||||
|
||||
## Meldung von Sicherheitsproblemen
|
||||
|
||||
Sicherheitsprobleme nicht mit Zugangsdaten oder personenbezogenen Daten in öffentliche Issues schreiben.
|
||||
@@ -1,111 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS listings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform TEXT NOT NULL,
|
||||
ai_provider TEXT NOT NULL,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
description_html TEXT,
|
||||
tags TEXT,
|
||||
photos TEXT,
|
||||
seller_notes TEXT,
|
||||
brand TEXT,
|
||||
category TEXT,
|
||||
size TEXT,
|
||||
color TEXT,
|
||||
condition TEXT,
|
||||
price REAL,
|
||||
status TEXT DEFAULT 'active',
|
||||
language TEXT DEFAULT 'de',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS templates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
platform TEXT,
|
||||
seller_notes TEXT,
|
||||
language TEXT DEFAULT 'de',
|
||||
default_tags TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS publish_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
listing_id INTEGER NOT NULL,
|
||||
platform TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
external_id TEXT,
|
||||
external_url TEXT,
|
||||
published_at TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('default_platform', 'vinted');
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('default_ai', 'claude');
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('seller_notes', '');
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('default_language', 'de');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mobile_upload_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
user_id INTEGER,
|
||||
listing_id INTEGER,
|
||||
context TEXT NOT NULL DEFAULT 'generator',
|
||||
files TEXT NOT NULL DEFAULT '[]',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
expires_at TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_mobile_upload_token ON mobile_upload_sessions(token_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_mobile_upload_expires ON mobile_upload_sessions(expires_at);
|
||||
|
||||
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('ai_model_photos_enabled', '1');
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('ai_model_photo_default_variants', '3');
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('ai_model_photo_default_preset', 'mixed');
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('ai_model_photo_block_underwear', '1');
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('ai_model_photo_block_kids', '1');
|
||||
INSERT OR IGNORE INTO settings (key, value) VALUES ('ai_model_photo_provider', 'openai');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_model_generations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
listing_id INTEGER,
|
||||
context TEXT NOT NULL DEFAULT 'generator',
|
||||
preset TEXT NOT NULL,
|
||||
variants INTEGER NOT NULL DEFAULT 3,
|
||||
source_photos TEXT NOT NULL DEFAULT '[]',
|
||||
generated_photos TEXT NOT NULL DEFAULT '[]',
|
||||
prompt_summary TEXT,
|
||||
moderation_notes TEXT,
|
||||
safety_status TEXT NOT NULL DEFAULT 'pending',
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT,
|
||||
FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ai_model_generations_listing ON ai_model_generations(listing_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_model_photos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
generation_id INTEGER NOT NULL,
|
||||
listing_id INTEGER,
|
||||
variant_index INTEGER NOT NULL DEFAULT 1,
|
||||
preset TEXT NOT NULL,
|
||||
image_path TEXT NOT NULL,
|
||||
source_photo TEXT,
|
||||
prompt TEXT,
|
||||
moderation_result TEXT,
|
||||
safety_status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (generation_id) REFERENCES ai_model_generations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (listing_id) REFERENCES listings(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ai_model_photos_listing ON ai_model_photos(listing_id);
|
||||
@@ -1,23 +0,0 @@
|
||||
# Vendoo Branding
|
||||
|
||||
Die Anwendung verwendet ab Version 1.6.2 ein gemeinsames Vendoo-Branding für Web-App, Login, mobilen Foto-Upload und Browser-Erweiterungen.
|
||||
|
||||
## Web-App
|
||||
|
||||
Assets liegen unter `public/brand/`:
|
||||
|
||||
- `logo/` – horizontales Logo, transparente und zugeschnittene Varianten
|
||||
- `icons/` – App- und Mobilgrößen von 16 bis 512 Pixel
|
||||
- `favicons/` – ICO, PNG-Favicons, Apple Touch Icon, Android-Icons und Webmanifest
|
||||
|
||||
## Browser-Erweiterungen
|
||||
|
||||
Chrome, Edge und Firefox verwenden dieselbe Icon-Familie in 16, 32, 48 und 128 Pixeln. Popup, Browser-Toolbar, Vinted Assistant und minimierter Assistant verwenden das neue Markenzeichen.
|
||||
|
||||
## Farben
|
||||
|
||||
- Vendoo Orange: `#E63B17`
|
||||
- Graphit: `#22201D`
|
||||
- Warmes Off-White: `#FAF6F2`
|
||||
|
||||
Die Proportionen und Farben der bereitgestellten Assets sollen nicht manuell verändert werden.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Vendoo Full Listing Editor 2026
|
||||
|
||||
## Ziel
|
||||
|
||||
Bereits gespeicherte Artikel werden nicht mehr in einem reduzierten Detailformular, sondern in einem vollständigen Arbeitsbereich bearbeitet. Generator und Listing-Editor verwenden dieselbe Rich-Text- und HTML-Logik.
|
||||
|
||||
## Funktionsumfang
|
||||
|
||||
- vollständige Bearbeitung aller Listing-Felder
|
||||
- Plattform, AI-Provider, Sprache, Status, SKU und Lagerort
|
||||
- Titel, Tags, Verkäufernotizen und sämtliche erkannten Attribute
|
||||
- Preis und Kategorie
|
||||
- Foto hinzufügen, entfernen, sortieren und als Cover festlegen
|
||||
- vorhandene Fotos mit Helligkeit, Kontrast, Sättigung, Rotation, Spiegelung und Crop bearbeiten
|
||||
- WYSIWYG und HTML-Code gleichzeitig sichtbar
|
||||
- Änderungen im WYSIWYG erscheinen sofort im HTML-Code
|
||||
- Änderungen im HTML-Code werden in den WYSIWYG-Editor zurückgeführt
|
||||
- separate sandboxed Plattformvorschau
|
||||
- Editor-HTML und Plattform-HTML separat kopierbar
|
||||
|
||||
## Datenmodell
|
||||
|
||||
`listings.description_html` speichert das bereinigte Rich-Text-HTML. `listings.description` bleibt als Klartext-Fallback und für Plattformen beziehungsweise Exporte erhalten.
|
||||
|
||||
Die neue Spalte wird über das bestehende `MIGRATIONS`-Array in `lib/db.mjs` ergänzt.
|
||||
|
||||
## Sicherheit
|
||||
|
||||
HTML wird auf unterstützte Formatierungs-Tags reduziert. Skripte, eingebettete Frames, Formulare, Event-Handler und gefährliche URLs werden entfernt. Vorschauen werden in einem `sandbox`-iframe dargestellt.
|
||||
@@ -1,61 +0,0 @@
|
||||
# Vendoo Listing Studio 2026
|
||||
|
||||
## Ziel
|
||||
|
||||
Der Listing-Generator ist kein Formularstapel mehr, sondern ein zusammenhängender Studio-Arbeitsplatz mit drei klaren Zonen:
|
||||
|
||||
1. Fotos & Medien
|
||||
2. AI-Konfiguration
|
||||
3. Vorschau & Bearbeitung
|
||||
|
||||
Die bestehende Vanilla-JS-, Express- und SQLite-Architektur bleibt erhalten.
|
||||
|
||||
## Fotos & Medien
|
||||
|
||||
- großes Cover mit vollständiger Bilddarstellung über `object-fit: contain`
|
||||
- Klick auf ein Vorschaubild setzt es als Cover
|
||||
- Drag & Drop sortiert die Bildreihenfolge
|
||||
- maximal zehn Bilder pro Listing
|
||||
- sichtbarer Foto-Check mit Qualitätsindikatoren
|
||||
- bestehender Bildeditor, Hintergrundentfernung und Wasserzeichen bleiben integriert
|
||||
|
||||
## AI-Konfiguration
|
||||
|
||||
- Plattform, Provider und Modell
|
||||
- Sprache und Anzahl der Varianten
|
||||
- Vorlagenauswahl
|
||||
- Verkäufernotizen mit Zeichenzähler
|
||||
- Vorlagen übernehmen Plattform, Sprache, Hinweise und Standard-Tags
|
||||
- global hinterlegte Verkäuferhinweise, Vorlagenhinweise und Artikelhinweise werden kontrolliert zusammengeführt
|
||||
|
||||
## Vorschau & Bearbeitung
|
||||
|
||||
- permanenter, kontextbezogener Ergebnisbereich
|
||||
- Live-Zähler für Titel, Beschreibung und Tags
|
||||
- editierbare erkannte Attribute
|
||||
- Kategorie-Picker, Preis und Lagerort
|
||||
- dynamische Gebührenvorschau
|
||||
- nachvollziehbarer Qualitätswert
|
||||
- HTML- und Plattformvorschau als optional aufklappbarer Bereich
|
||||
- Speichern und Kopieren ohne Fensterwechsel
|
||||
|
||||
## API-Erweiterung
|
||||
|
||||
`POST /api/generate` akzeptiert zusätzlich:
|
||||
|
||||
```json
|
||||
{
|
||||
"template_id": 1,
|
||||
"seller_notes": "Artikelspezifische Hinweise"
|
||||
}
|
||||
```
|
||||
|
||||
Standard-Tags der gewählten Vorlage werden dedupliziert mit den AI-Tags zusammengeführt.
|
||||
|
||||
## Qualitätsregeln
|
||||
|
||||
- keine neuen Frameworks
|
||||
- keine neuen Datenbankspalten
|
||||
- bestehende IDs und Generatorfunktionen bleiben kompatibel
|
||||
- alle Farben und Zustände nutzen die Vendoo-CSS-Variablen
|
||||
- Dark Mode und responsive Darstellung sind Bestandteil des Slices
|
||||
@@ -1,16 +0,0 @@
|
||||
# Hotfix – Studio Flow Detailnavigation
|
||||
|
||||
## Fehler
|
||||
|
||||
Der Button **Prüfen & bearbeiten** lud die Listing-Detailansicht, ohne zuvor den Listings-Tab sichtbar zu schalten. Der Editor wurde dadurch in einem ausgeblendeten Bereich aufgebaut.
|
||||
|
||||
## Korrektur
|
||||
|
||||
`showDetail(id)` prüft nun zentral, ob der Listings-/Historie-Tab aktiv ist. Falls nicht, wird dieser vor dem Laden des Listings aktiviert. Damit funktionieren Detailaufrufe aus:
|
||||
|
||||
- Studio Flow
|
||||
- Attention Queue
|
||||
- Listings-Tabelle
|
||||
- weiteren internen Bereichen
|
||||
|
||||
Zusätzlich werden ungültige Listing-IDs abgefangen.
|
||||
@@ -1,31 +0,0 @@
|
||||
# Vendoo Listings UI 2026
|
||||
|
||||
## Ziel
|
||||
|
||||
Die Listings-/Historie-Seite bildet die operative Übersicht aller Artikel. Sie folgt der freigegebenen Vendoo-Designsprache und ersetzt die alte Kartenliste durch eine dichte, bildorientierte Arbeitsliste.
|
||||
|
||||
## Funktionsumfang
|
||||
|
||||
- Volltextsuche über bestehende Listing-Suche
|
||||
- Plattform- und Statusfilter
|
||||
- Zeiträume 7, 30 und 90 Tage, aktuelles Jahr, gesamter Zeitraum und benutzerdefinierte Daten
|
||||
- Spaltenkonfiguration mit Speicherung im Browser
|
||||
- Seitengrößen 10, 20, 50 oder alle
|
||||
- Seitennavigation mit direkten Seitenschaltflächen
|
||||
- Auswahl einzelner Listings und Auswahl der aktuellen Seite
|
||||
- Batch Publish, Nachbearbeiten, Preisänderung, Statusänderung, Duplizieren, Etikettendruck und Löschen
|
||||
- CSV- und JSON-Export
|
||||
- Direktänderung des Status pro Tabellenzeile
|
||||
- Zeilenaktionen Öffnen, Duplizieren und Löschen
|
||||
- Publish-Status-Badges je Plattform
|
||||
|
||||
## Technische Regeln
|
||||
|
||||
Die Umsetzung bleibt vollständig in der bestehenden Vanilla-Architektur:
|
||||
|
||||
- Markup: `public/index.html`
|
||||
- Verhalten: `public/app.js`
|
||||
- Design: `public/style.css`
|
||||
- keine neuen Frameworks
|
||||
- keine Änderungen am Datenbankschema erforderlich
|
||||
- vorhandene API-Endpunkte und CSRF-Absicherung werden weiterverwendet
|
||||
@@ -1,28 +0,0 @@
|
||||
# Vendoo Publish-Center 2026
|
||||
|
||||
## Ziel
|
||||
|
||||
Das Publish-Center bündelt alle Arbeitsschritte zwischen fertigem Listing und Veröffentlichung in einer einzigen, kontextstabilen Oberfläche.
|
||||
|
||||
## Bereiche
|
||||
|
||||
- **Smart Copy:** Felder, HTML/Text, Fotos und Plattformwechsel
|
||||
- **Direkt veröffentlichen:** Etsy- und eBay-API mit vorheriger Datenprüfung
|
||||
- **eBay Queue:** Status, Fehler, Wiederholungen und Verarbeitung
|
||||
- **Geplant:** zeitgesteuerte Veröffentlichungen mit Plattform und Zeitpunkt
|
||||
|
||||
## UX-Regeln
|
||||
|
||||
- keine internen Arbeitsabläufe in neuen Browserfenstern
|
||||
- Produktbild, Titel und SKU bleiben im Kontext sichtbar
|
||||
- Fehler und fehlende Pflichtfelder sind direkt handlungsfähig
|
||||
- externe Plattformen öffnen nur dann einen neuen Tab, wenn die Plattform selbst benötigt wird
|
||||
- alle Statusfarben verwenden die zentralen Vendoo-Tokens
|
||||
|
||||
## Technische Grundlage
|
||||
|
||||
- Vanilla JavaScript in `public/app.js`
|
||||
- Markup in `public/index.html`
|
||||
- Design-Tokens und responsive Komponenten in `public/style.css`
|
||||
- bestehende Endpunkte für Smart Copy, Etsy, eBay Queue und Scheduling
|
||||
- Session-, Rollen- und CSRF-Schutz bleiben unverändert aktiv
|
||||
@@ -1,29 +0,0 @@
|
||||
# Slice 02 – Listings / Historie Acceptance
|
||||
|
||||
## Abnahmeziele
|
||||
|
||||
- [x] freigegebene Vendoo-Designsprache übernommen
|
||||
- [x] bestehende Listings-API unverändert weiterverwendet
|
||||
- [x] Suche, Plattform-, Status- und Datumsfilter erhalten
|
||||
- [x] Zeitraumauswahl ergänzt
|
||||
- [x] CSV- und JSON-Export erhalten
|
||||
- [x] Einzel- und Seitenauswahl umgesetzt
|
||||
- [x] Batch Status, Preis, Duplizieren, Etiketten, Publish und Löschen umgesetzt
|
||||
- [x] Inline-Statusänderung pro Listing erhalten
|
||||
- [x] Detailansicht weiterhin erreichbar
|
||||
- [x] Pagination mit direkter Seitenauswahl umgesetzt
|
||||
- [x] Spaltenkonfiguration lokal gespeichert
|
||||
- [x] Dark-Mode-Tokens verwendet
|
||||
- [x] keine hardcodierten produktiven Secrets enthalten
|
||||
|
||||
## Technische Prüfungen
|
||||
|
||||
- `node --check public/app.js`
|
||||
- `node --check server.mjs`
|
||||
- `node --check lib/db.mjs`
|
||||
- keine doppelten HTML-IDs
|
||||
- ausgeglichene CSS-Klammerstruktur
|
||||
|
||||
## Runtime-Hinweis
|
||||
|
||||
Ein vollständiger Runtime-Test benötigt die plattformspezifische Installation von `better-sqlite3`. Das Paket enthält bewusst kein `node_modules`; Installation erfolgt über den gehärteten Installer beziehungsweise `npm ci`.
|
||||
@@ -1,28 +0,0 @@
|
||||
# Slice 05 – Abnahme Publish-Center
|
||||
|
||||
## Funktionsumfang
|
||||
|
||||
- [x] Smart Copy vollständig innerhalb Vendoo
|
||||
- [x] direkte Etsy-/eBay-Veröffentlichung mit Integrationsstatus
|
||||
- [x] Listing-Daten vor der Veröffentlichung inline bearbeitbar
|
||||
- [x] eBay Queue ohne separates Modal steuerbar
|
||||
- [x] Fehler, Wiederholungen und Queue-Status sichtbar
|
||||
- [x] geplante Veröffentlichungen inline verwaltbar
|
||||
- [x] Batch-Auswahl aus Listings wird übernommen
|
||||
- [x] Fotos werden vollständig ohne Beschnitt dargestellt
|
||||
- [x] responsive Layouts für Desktop, Tablet und Mobile
|
||||
- [x] Dark Mode ausschließlich über zentrale CSS-Variablen
|
||||
|
||||
## Technische Prüfungen
|
||||
|
||||
- [x] `node --check server.mjs`
|
||||
- [x] `node --check lib/db.mjs`
|
||||
- [x] `node --check public/app.js`
|
||||
- [x] keine doppelten HTML-IDs
|
||||
- [x] ausgeglichene CSS-Klammerstruktur
|
||||
- [x] keine `.env`, Datenbank, Uploads oder `node_modules` im Paket
|
||||
- [x] bestehende API-Routen, Authentifizierung und CSRF-Logik beibehalten
|
||||
|
||||
## Installation
|
||||
|
||||
Als Update über `setup.bat` → **3 Update** installieren. Danach Browser mit `Strg + F5` neu laden.
|
||||
@@ -1,25 +0,0 @@
|
||||
# Slice 06 – Abnahme
|
||||
|
||||
## Funktionsabnahme
|
||||
|
||||
- [x] Studio Flow öffnet den sichtbaren Listing-Editor.
|
||||
- [x] gespeicherte Listings können vollständig bearbeitet werden.
|
||||
- [x] Fotos können ergänzt, entfernt, sortiert und als Cover gesetzt werden.
|
||||
- [x] der vorhandene Fotoeditor funktioniert im Generator und im Listing-Editor.
|
||||
- [x] WYSIWYG-Änderungen erscheinen unmittelbar im HTML-Code.
|
||||
- [x] HTML-Codeänderungen werden in den WYSIWYG-Editor zurückgeführt.
|
||||
- [x] Generator und Listing-Editor rendern eine separate Plattformvorschau.
|
||||
- [x] Rich-Text-HTML wird dauerhaft in `description_html` gespeichert.
|
||||
- [x] Klartext bleibt in `description` als Fallback erhalten.
|
||||
- [x] Plattformvorschauen laufen in sandboxed iframes.
|
||||
|
||||
## Technische Abnahme
|
||||
|
||||
- [x] DB-Migration für `description_html` im `MIGRATIONS`-Array.
|
||||
- [x] `photos` und `tags` werden bei Updates als JSON gespeichert.
|
||||
- [x] Listing-Updates verwenden eine feste Feld-Allowlist.
|
||||
- [x] Client- und Server-Sanitizing für Rich-Text-HTML.
|
||||
- [x] JavaScript-/ESM-Syntaxprüfung bestanden.
|
||||
- [x] keine doppelten statischen HTML-IDs.
|
||||
- [x] CSS-Klammerstruktur vollständig.
|
||||
- [x] keine `.env`, Datenbanken, Uploads oder `node_modules` im Paket.
|
||||
@@ -1,36 +0,0 @@
|
||||
# Slice 07 – Cross-Browser Extensions, Responsive UI & Mobile Upload
|
||||
|
||||
## Browser-Erweiterungen
|
||||
|
||||
- Chrome: `extensions/chrome`
|
||||
- Edge: `extensions/edge`
|
||||
- Firefox: `extensions/firefox`
|
||||
- neues Vendoo-Link-Icon in 16/32/48/128 px
|
||||
- neues Popup nach freigegebenem Mockup
|
||||
- neuer Vendoo Assistant mit produktzentrierter Detailansicht und Auto-Fill-Flow
|
||||
|
||||
## HTML-/Plattformvorschau
|
||||
|
||||
Im Generator und Listing-Editor ist HTML nicht mehr als dauerhaft zweite Spalte sichtbar. Innerhalb der Plattformvorschau kann zwischen **Plattformvorschau** und **HTML-Code** umgeschaltet werden. WYSIWYG und HTML bleiben bidirektional synchronisiert.
|
||||
|
||||
## Responsive Layout
|
||||
|
||||
Abnahmepunkte:
|
||||
|
||||
- Desktop ab 1440 px
|
||||
- Notebook 1180–1439 px
|
||||
- iPad Querformat 1024 px
|
||||
- Tablet Hochformat 768 px
|
||||
- Smartphone 360–560 px
|
||||
|
||||
Navigation, Generator, Listing-Editor, Studio Flow, Tabellen, Modale und Kontextbereiche werden je Breakpoint neu priorisiert.
|
||||
|
||||
## Mobiler Foto-Upload
|
||||
|
||||
1. Nutzer erzeugt in Generator oder Listing-Editor einen QR-Code.
|
||||
2. Vendoo erstellt ein kryptografisch zufälliges, gehasht gespeichertes Upload-Token.
|
||||
3. Der Link ist 20 Minuten gültig und auf maximal zehn Bilder begrenzt.
|
||||
4. Smartphone lädt Bilder über eine eigenständige responsive Upload-Seite hoch.
|
||||
5. Desktop pollt die Sitzung und übernimmt neue Bilder automatisch.
|
||||
|
||||
`PUBLIC_BASE_URL` oder eine erreichbare LAN-/HTTPS-Adresse ist erforderlich, wenn der QR-Code von einem anderen Gerät geöffnet wird.
|
||||
@@ -1,33 +0,0 @@
|
||||
# Slice 08 – Extension Auto-Fill & UX
|
||||
|
||||
## Ziel
|
||||
|
||||
Die Vinted-Erweiterung soll kompakt, hochwertig und sicher bedienbar sein. Das bloße Ausfüllen eines Formulars darf niemals automatisch einen Entwurf erzeugen oder eine Veröffentlichung auslösen.
|
||||
|
||||
## Verbindliches Aktionsmodell
|
||||
|
||||
1. **Nur ausfüllen**
|
||||
- trägt Titel, Beschreibung, Preis, Tags und bestmöglich weitere Attribute ein
|
||||
- wählt die Kategorie, sofern möglich
|
||||
- speichert keinen Entwurf
|
||||
- veröffentlicht nicht
|
||||
- setzt keinen Publish-Status in Vendoo
|
||||
|
||||
2. **Daten + Bilder ausfüllen**
|
||||
- überträgt zusätzlich die Produktbilder
|
||||
- speichert keinen Entwurf
|
||||
- veröffentlicht nicht
|
||||
|
||||
3. **Als Entwurf** und **Veröffentlichen**
|
||||
- sind getrennte, explizite Abschlussaktionen
|
||||
- befinden sich im Detailbereich unter einer aufklappbaren Sicherheitssektion
|
||||
|
||||
## Browser
|
||||
|
||||
Identische UI- und Funktionsbasis unter:
|
||||
|
||||
- `extensions/chrome`
|
||||
- `extensions/edge`
|
||||
- `extensions/firefox`
|
||||
|
||||
Version: `1.4.0`
|
||||
@@ -1,17 +0,0 @@
|
||||
# Slice 11 – Dashboard-First & Responsive Hotfix
|
||||
|
||||
## Umfang
|
||||
|
||||
- Dashboard als erster Navigationspunkt und Standardansicht
|
||||
- fließende KPI-Kacheln mit Auto-Fit
|
||||
- responsive Sparklines und Diagramme
|
||||
- ResizeObserver für Dashboard-Visualisierungen
|
||||
- vollständige Produktbild-Darstellung im Studio Flow
|
||||
- automatische Kartenverteilung für Desktop, Tablet und Mobile
|
||||
|
||||
## Abnahme
|
||||
|
||||
- keine Text-/Diagrammüberlagerung in KPI-Karten
|
||||
- keine abgeschnittenen Bilder in „Bereit für den nächsten Schritt“
|
||||
- Dashboard öffnet nach dem Login zuerst
|
||||
- Diagramme reagieren auf Fenster- und Sidebar-Breitenänderungen
|
||||
@@ -1,41 +0,0 @@
|
||||
# Vendoo UI Foundation 2026
|
||||
|
||||
## Status
|
||||
|
||||
Die Designrichtung ist durch die freigegebenen Referenzen in `MockupDesign/` verbindlich festgelegt.
|
||||
|
||||
## Leitidee
|
||||
|
||||
Vendoo ist eine **Commerce Workbench**, kein generisches Statistik-Dashboard.
|
||||
|
||||
Der zentrale Arbeitsfluss lautet:
|
||||
|
||||
`Erfassen → AI-Prüfung → Bereit → Geplant → Veröffentlicht → Verkauft`
|
||||
|
||||
## Verbindliche Gestaltungsregeln
|
||||
|
||||
- warme Off-White-Grundflächen und Graphit-Typografie
|
||||
- Vendoo-Orange nur als gezielter Aktions- und Fokusakzent
|
||||
- Produktbilder bilden die wichtigste visuelle Ebene
|
||||
- Statusfarben transportieren Bedeutung statt Dekoration
|
||||
- schmale Hauptnavigation und kontextuelle Detailflächen
|
||||
- keine austauschbaren KPI-Kartenlandschaften
|
||||
- keine violett-blauen Standardverläufe
|
||||
- keine dekorativen Charts ohne operative Funktion
|
||||
- wenige, bewusst eingesetzte Rundungen und Schatten
|
||||
- Dark Mode wird eigenständig über CSS Custom Properties gestaltet
|
||||
|
||||
## Slice 01
|
||||
|
||||
Dieser Slice führt ein:
|
||||
|
||||
- neue App-Shell mit kompakter Navigation und Command Search
|
||||
- zentrale Design-Tokens für Light und Dark Mode
|
||||
- neue Startseite `Studio Flow`
|
||||
- Workflow-Dashboard-API unter `/api/dashboard/workflow`
|
||||
- Attention Queue, Activity Stream und Artikel-Inspector
|
||||
- unveränderte Vanilla-JavaScript-/Express-/SQLite-Architektur
|
||||
|
||||
## Noch nicht Teil dieses Slices
|
||||
|
||||
Die vollständige visuelle Umstellung von Listings, Generator, Publish, Lager, Vorlagen, Einstellungen und Admin erfolgt in nachfolgenden isolierten Slices auf derselben Foundation.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Vendoo UI 2026 – Slice 03
|
||||
|
||||
## Umfang
|
||||
|
||||
### Print Studio
|
||||
|
||||
Etiketten und Datenblätter werden innerhalb der Vendoo-Oberfläche vorbereitet. Das Print Studio bietet eine A4-Vorschau, Etikettenformate 90 × 50 mm und 62 × 29 mm, mehrere Exemplare sowie eine optionale Preisanzeige. Erst der abschließende Druckbefehl öffnet den normalen Systemdruckdialog. Es wird kein zusätzliches Browserfenster erzeugt.
|
||||
|
||||
### Studio Flow Media Fit
|
||||
|
||||
Produktbilder in Flow-Karten, Inspector und Attention Queue werden vollständig dargestellt. Die Medienflächen verwenden `object-fit: contain`, zentrierte Ausrichtung und neutrale Produktflächen.
|
||||
|
||||
### Vinted Extension
|
||||
|
||||
Die Extension verwendet die Vendoo-Designsprache mit warmen Flächen, Graphittypografie und Vendoo-Orange. Produktbilder werden nicht mehr direkt als HTTP-Bild in die Vinted-Seite eingebettet, sondern durch den Background-Service-Worker geladen und als Data-URL eingesetzt. Das vermeidet Blockaden durch Seiten-CSP oder gemischte Inhalte.
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
- `public/app.js`
|
||||
- `public/style.css`
|
||||
- `extension/background.js`
|
||||
- `extension/content.js`
|
||||
- `extension/content.css`
|
||||
- `extension/popup.html`
|
||||
- `extension/popup.js`
|
||||
- `extension/manifest.json`
|
||||
@@ -1,66 +0,0 @@
|
||||
# AutoLister Feature-Erweiterungen
|
||||
|
||||
## 1. Kategorie-Fixes
|
||||
|
||||
### a) Auto-Matching nach AI-Generierung
|
||||
- `showMeta()` ruft `/api/categories/:platform/match?q=<ai_category>` auf
|
||||
- Bestes Ergebnis wird im Picker vorausgewählt
|
||||
- Freitext-Fallback wenn kein Match (Score < 50)
|
||||
|
||||
### b) Plattform-Wechsel aktualisiert Picker
|
||||
- Plattform-Button-Click prüft ob Meta-Section sichtbar ist
|
||||
- Wenn ja: Kategorie-Picker wird mit neuer Plattform neu initialisiert
|
||||
- Bisheriger Wert wird als Suchquery gegen neue Plattform gematcht
|
||||
|
||||
### c) Extension nutzt Server-API
|
||||
- `content.js` lädt Kategorien via `/api/categories/vinted` statt lokaler Datei
|
||||
- `vinted-categories.js` wird aus manifest.json entfernt (Datei bleibt als Fallback)
|
||||
- Match-Funktion nutzt `/api/categories/vinted/match?q=`
|
||||
|
||||
## 2. WYSIWYG ↔ HTML Sync
|
||||
|
||||
- Quill `text-change` Event → HTML-Source-Textarea wird aktualisiert (`editor.root.innerHTML`)
|
||||
- HTML-Source `input` Event → Quill übernimmt via `editor.root.innerHTML = textarea.value`
|
||||
- Toolbar-Icons bekommen `title`-Attribute: Bold→"Fett", Italic→"Kursiv", Underline→"Unterstrichen", Strike→"Durchgestrichen", List Ordered→"Nummerierte Liste", List Bullet→"Aufzählung", Blockquote→"Zitat", Link→"Link einfügen", Clean→"Formatierung entfernen", Color→"Textfarbe", Background→"Hintergrundfarbe", Align→"Ausrichtung", Header→"Überschrift"
|
||||
|
||||
## 3. Veröffentlichungsstatus in Historie
|
||||
|
||||
- `loadHistory()` ruft zusätzlich `/api/publish/status?ids=` auf
|
||||
- Pro Listing in der Liste: farbige Badges für jede Plattform
|
||||
- Grün: veröffentlicht
|
||||
- Orange: kopiert
|
||||
- Kein Badge: nicht veröffentlicht
|
||||
- Badges erscheinen neben dem Status-Badge in der History-Liste
|
||||
|
||||
## 4. SKU/Artikelnummer-System
|
||||
|
||||
### DB
|
||||
- Neues Feld: `sku TEXT` in listings-Tabelle
|
||||
- Auto-generiert: `AL-XXXX` (fortlaufend, 4-stellig zero-padded)
|
||||
- Jederzeit editierbar auf eigene Bezeichnung
|
||||
|
||||
### UI-Integration
|
||||
- Generator Meta-Section: SKU-Feld (readonly, auto-generiert beim Erstellen)
|
||||
- Historie-Liste: SKU als erste Info nach Thumbnail
|
||||
- Detail-View: editierbares SKU-Feld
|
||||
- Publish-Liste: SKU angezeigt
|
||||
|
||||
### QR-Code
|
||||
- Client-seitig via qrcode.js (CDN)
|
||||
- QR-Inhalt: `{"sku":"AL-0001","id":42,"url":"http://localhost:8124/#listing/42"}`
|
||||
- Angezeigt im Detail-View
|
||||
|
||||
### Druck-Formate
|
||||
**Kompakt-Etikett (~90×50mm):**
|
||||
- QR links, rechts: SKU, Titel (max 40 Zeichen), Preis, Plattform
|
||||
- 4 Etiketten pro A4-Seite bei Batch-Druck
|
||||
|
||||
**Produkt-Datenblatt (A4):**
|
||||
- Header: QR + SKU groß
|
||||
- Foto-Thumbnails (max 4)
|
||||
- Titel, Beschreibung (max 200 Zeichen), Marke, Kategorie, Größe, Farbe, Zustand, Preis
|
||||
- Plattform, Erstelldatum
|
||||
|
||||
### Druck-Buttons
|
||||
- Detail-View: "Etikett drucken" + "Datenblatt drucken"
|
||||
- Historie Batch-Bar: "Etiketten drucken" (mehrere auf einem Blatt)
|
||||
@@ -1,8 +0,0 @@
|
||||
VERALTETER ORDNER
|
||||
|
||||
Die Browser-Erweiterungen befinden sich ab Version 1.3.0 unter:
|
||||
- extensions/chrome
|
||||
- extensions/edge
|
||||
- extensions/firefox
|
||||
|
||||
Bitte die bisher geladene Erweiterung entfernen und den passenden neuen Unterordner laden.
|
||||
@@ -1,60 +0,0 @@
|
||||
const ext = globalThis.browser || globalThis.chrome;
|
||||
const csrfCache = new Map();
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const chunkSize = 0x8000;
|
||||
let binary = '';
|
||||
for (let offset = 0; offset < bytes.length; offset += chunkSize) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
async function getCsrf(serverUrl) {
|
||||
if (csrfCache.has(serverUrl)) return csrfCache.get(serverUrl);
|
||||
const response = await fetch(`${serverUrl}/api/me`, { credentials: 'include', cache: 'no-store' });
|
||||
if (!response.ok) return '';
|
||||
const data = await response.json();
|
||||
const token = data.csrf || '';
|
||||
csrfCache.set(serverUrl, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
if (msg.type === 'api') {
|
||||
(async () => {
|
||||
try {
|
||||
const { serverUrl, path, method = 'GET', body } = msg;
|
||||
const opts = { method, headers: {}, credentials: 'include', cache: 'no-store' };
|
||||
if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
const csrf = await getCsrf(serverUrl);
|
||||
if (csrf) opts.headers['X-CSRF-Token'] = csrf;
|
||||
}
|
||||
let response = await fetch(`${serverUrl}${path}`, opts);
|
||||
if (response.status === 403 && !['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
csrfCache.delete(serverUrl);
|
||||
const csrf = await getCsrf(serverUrl);
|
||||
if (csrf) opts.headers['X-CSRF-Token'] = csrf;
|
||||
response = await fetch(`${serverUrl}${path}`, opts);
|
||||
}
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
const data = contentType.includes('application/json') ? await response.json() : { error: await response.text() };
|
||||
sendResponse({ ok: response.ok, status: response.status, data });
|
||||
} catch (error) { sendResponse({ ok: false, status: 0, error: error.message }); }
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
if (msg.type === 'fetch-image') {
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch(msg.url, { cache: 'no-store', credentials: 'include' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const blob = await response.blob();
|
||||
if (!blob.type.startsWith('image/')) throw new Error('Antwort ist kein Bild');
|
||||
const buffer = await blob.arrayBuffer();
|
||||
sendResponse({ ok: true, dataUrl: `data:${blob.type};base64,${arrayBufferToBase64(buffer)}`, mime: blob.type, size: blob.size });
|
||||
} catch (error) { sendResponse({ ok: false, error: error.message }); }
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,10 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<rect width="128" height="128" rx="10" fill="#fffdf9"/>
|
||||
<g transform="translate(26 22) rotate(-25 17 42)">
|
||||
<rect x="0" y="0" width="28" height="84" rx="10" fill="#ff4a12"/>
|
||||
</g>
|
||||
<path d="M76 24h21l11 12v21L91 70 72 61V39z" fill="#ff4a12"/>
|
||||
<circle cx="94" cy="38" r="6" fill="#fffdf9"/>
|
||||
<circle cx="84" cy="91" r="21" fill="#24262a"/>
|
||||
<path d="m74 91 7 7 14-16" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 542 B |
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Vendoo Link – Vinted Assistant",
|
||||
"version": "1.6.0",
|
||||
"description": "Vendoo Link mit integriertem Vendoo-Branding, Queue, sicherem Auto-Fill, Bildtransfer und Publishing für Vinted.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://localhost:8124/*",
|
||||
"http://127.0.0.1:8124/*",
|
||||
"https://www.vinted.de/*",
|
||||
"https://www.vinted.fr/*",
|
||||
"https://www.vinted.co.uk/*",
|
||||
"https://www.vinted.com/*",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://www.vinted.de/*",
|
||||
"https://www.vinted.fr/*",
|
||||
"https://www.vinted.co.uk/*",
|
||||
"https://www.vinted.com/*"
|
||||
],
|
||||
"js": [
|
||||
"vinted-categories.js",
|
||||
"content.js"
|
||||
],
|
||||
"css": [
|
||||
"content.css"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Vendoo Assistant",
|
||||
"default_icon": {
|
||||
"16": "icon-16.png",
|
||||
"32": "icon-32.png",
|
||||
"48": "icon-48.png",
|
||||
"128": "icon-128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icon-16.png",
|
||||
"32": "icon-32.png",
|
||||
"48": "icon-48.png",
|
||||
"128": "icon-128.png"
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Vendoo Link</title>
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="popup-shell">
|
||||
<header class="popup-header">
|
||||
<div class="brand-lockup"><img src="icon-48.png" alt=""><div><strong>Vendoo Link</strong><span>Vinted Assistant</span></div></div>
|
||||
<div id="connection-chip" class="connection-chip"><i></i><span>Prüfe …</span></div>
|
||||
<button id="settings-toggle" class="icon-button" type="button" title="Einstellungen">⚙</button>
|
||||
</header>
|
||||
|
||||
<section id="dashboard-view" class="popup-content">
|
||||
<article class="queue-card">
|
||||
<div class="queue-icon">↗</div>
|
||||
<div><span>Vinted Workflow</span><strong id="queue-state">Wird geladen …</strong><small>Listings kontrolliert in Vinted übernehmen</small><div class="queue-progress"><i id="queue-progress-fill"></i></div></div>
|
||||
<div class="queue-count"><b id="queue-count">0 / 0</b><button id="open-queue" type="button">Öffnen</button></div>
|
||||
</article>
|
||||
|
||||
<article id="listing-card" class="listing-card hidden">
|
||||
<header><div><span>Aktuelles Listing</span><b>Bereit</b></div><button id="listing-menu" type="button">•••</button></header>
|
||||
<div class="listing-overview">
|
||||
<div class="listing-photo"><img id="listing-image" alt="Produktbild"></div>
|
||||
<div class="listing-copy"><h2 id="listing-title">–</h2><p id="listing-meta">–</p><code id="listing-sku">–</code><dl><dt>Plattform</dt><dd><span class="platform-chip">Vinted</span></dd><dt>Preis</dt><dd id="listing-price">–</dd><dt>Erstellt</dt><dd id="listing-created">–</dd></dl></div>
|
||||
</div>
|
||||
<div class="safe-notice"><b>Nur ausfüllen</b><span>Öffnet Vinted und trägt die Daten ein – ohne Entwurf und ohne Veröffentlichung.</span></div>
|
||||
<button id="fill-listing" class="primary-action" type="button">Auf Vinted ausfüllen <span>↗</span></button>
|
||||
<div class="action-row"><button id="next-listing" class="secondary-action" type="button">Nächstes Listing</button><button id="prepare-images" class="secondary-action" type="button">Bilder vorbereiten</button></div>
|
||||
</article>
|
||||
|
||||
<article id="empty-card" class="empty-card hidden"><strong>Keine offenen Vinted-Listings</strong><p>Neue Listings kannst du direkt in Vendoo erstellen.</p><button id="empty-open-app" type="button">Vendoo öffnen</button></article>
|
||||
</section>
|
||||
|
||||
<section id="settings-view" class="settings-view hidden">
|
||||
<div class="settings-heading"><button id="back-dashboard" type="button">←</button><div><strong>Einstellungen</strong><span>Verbindung und Standardwerte</span></div></div>
|
||||
<div class="setting"><label for="server-url">Vendoo Server</label><input id="server-url" type="url" spellcheck="false"></div>
|
||||
<div class="setting-row"><div class="setting"><label for="provider">AI Provider</label><select id="provider"><option value="openrouter">OpenRouter</option><option value="claude">Claude</option><option value="openai">OpenAI</option><option value="local">Ollama</option></select></div><div class="setting"><label for="language">Sprache</label><select id="language"><option value="de">Deutsch</option><option value="en">English</option><option value="fr">Français</option><option value="nl">Nederlands</option></select></div></div>
|
||||
<button id="save-settings" class="primary-action" type="button">Einstellungen speichern</button>
|
||||
</section>
|
||||
|
||||
<footer><span>Vendoo Link v1.6.0</span><button id="open-app" type="button">Vendoo öffnen ↗</button></footer>
|
||||
</main>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,129 +0,0 @@
|
||||
const browserApi = globalThis.browser || null;
|
||||
const chromeApi = globalThis.chrome || null;
|
||||
const $ = selector => document.querySelector(selector);
|
||||
const state = { queue: [], current: 0, serverUrl: 'http://localhost:8124' };
|
||||
|
||||
function sendMessage(message) {
|
||||
if (browserApi) return browserApi.runtime.sendMessage(message);
|
||||
return new Promise((resolve, reject) => {
|
||||
chromeApi.runtime.sendMessage(message, response => {
|
||||
const error = chromeApi.runtime.lastError;
|
||||
if (error) reject(new Error(error.message)); else resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
function storageGet(keys) {
|
||||
if (browserApi) return browserApi.storage.local.get(keys);
|
||||
return new Promise(resolve => chromeApi.storage.local.get(keys, resolve));
|
||||
}
|
||||
function storageSet(values) {
|
||||
if (browserApi) return browserApi.storage.local.set(values);
|
||||
return new Promise(resolve => chromeApi.storage.local.set(values, resolve));
|
||||
}
|
||||
function openTab(url) {
|
||||
if (browserApi) return browserApi.tabs.create({ url });
|
||||
return chromeApi.tabs.create({ url });
|
||||
}
|
||||
async function ensureServerPermission(url) {
|
||||
const parsed = new URL(url);
|
||||
const origin = `${parsed.protocol}//${parsed.host}/*`;
|
||||
const permissions = browserApi?.permissions || chromeApi?.permissions;
|
||||
if (!permissions) return true;
|
||||
if (browserApi) {
|
||||
if (await permissions.contains({ origins: [origin] })) return true;
|
||||
return permissions.request({ origins: [origin] });
|
||||
}
|
||||
return new Promise(resolve => permissions.contains({ origins: [origin] }, has => has ? resolve(true) : permissions.request({ origins: [origin] }, resolve)));
|
||||
}
|
||||
|
||||
async function api(path, method = 'GET', body) {
|
||||
const response = await sendMessage({ type: 'api', serverUrl: state.serverUrl, path, method, body });
|
||||
if (!response?.ok) throw new Error(response?.data?.error || response?.error || 'Server nicht erreichbar');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function setConnection(ok, text) {
|
||||
const chip = $('#connection-chip');
|
||||
chip.className = `connection-chip ${ok ? 'ok' : 'error'}`;
|
||||
chip.querySelector('span').textContent = text;
|
||||
}
|
||||
|
||||
async function imageData(filename) {
|
||||
if (!filename) return '';
|
||||
const url = `${state.serverUrl}/uploads/${String(filename).split('/').map(encodeURIComponent).join('/')}`;
|
||||
const response = await sendMessage({ type: 'fetch-image', url });
|
||||
return response?.ok ? response.dataUrl : '';
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
const date = new Date(String(value || '').replace(' ', 'T') + 'Z');
|
||||
return Number.isNaN(date.getTime()) ? '–' : date.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
async function renderCurrent() {
|
||||
const listing = state.queue[state.current];
|
||||
$('#listing-card').classList.toggle('hidden', !listing);
|
||||
$('#empty-card').classList.toggle('hidden', !!listing);
|
||||
if (!listing) return;
|
||||
$('#listing-title').textContent = listing.title || 'Ohne Titel';
|
||||
$('#listing-meta').textContent = [listing.size, listing.color, listing.condition].filter(Boolean).join(' · ') || 'Noch keine Eigenschaften';
|
||||
$('#listing-sku').textContent = listing.sku || `VD-${String(listing.id).padStart(4, '0')}`;
|
||||
$('#listing-price').textContent = listing.price ? `${Number(listing.price).toFixed(2).replace('.', ',')} €` : '–';
|
||||
$('#listing-created').textContent = formatDate(listing.created_at);
|
||||
const src = await imageData(listing.photos?.[0]);
|
||||
const image = $('#listing-image');
|
||||
if (src) { image.src = src; image.classList.remove('hidden'); } else { image.removeAttribute('src'); image.classList.add('hidden'); }
|
||||
}
|
||||
|
||||
async function loadQueue() {
|
||||
try {
|
||||
const data = await api('/api/extension/queue?platform=vinted');
|
||||
state.queue = data.queue || [];
|
||||
state.current = Math.min(state.current, Math.max(0, state.queue.length - 1));
|
||||
setConnection(true, 'Verbunden');
|
||||
$('#queue-state').textContent = state.queue.length ? 'Bereit zur Übertragung' : 'Queue leer';
|
||||
$('#queue-count').textContent = `${state.queue.length} / ${data.total || state.queue.length}`;
|
||||
$('#queue-progress-fill').style.width = `${data.total ? ((data.total - state.queue.length) / data.total) * 100 : 0}%`;
|
||||
await renderCurrent();
|
||||
} catch (error) {
|
||||
setConnection(false, 'Offline');
|
||||
$('#queue-state').textContent = error.message;
|
||||
$('#listing-card').classList.add('hidden');
|
||||
$('#empty-card').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const settings = await storageGet(['serverUrl', 'provider', 'language']);
|
||||
state.serverUrl = String(settings.serverUrl || 'http://localhost:8124').replace(/\/$/, '');
|
||||
$('#server-url').value = state.serverUrl;
|
||||
if (settings.provider) $('#provider').value = settings.provider;
|
||||
if (settings.language) $('#language').value = settings.language;
|
||||
await loadQueue();
|
||||
}
|
||||
|
||||
function openUrl(url) { openTab(url); }
|
||||
$('#settings-toggle').addEventListener('click', () => { $('#dashboard-view').classList.add('hidden'); $('#settings-view').classList.remove('hidden'); });
|
||||
$('#back-dashboard').addEventListener('click', () => { $('#settings-view').classList.add('hidden'); $('#dashboard-view').classList.remove('hidden'); });
|
||||
$('#save-settings').addEventListener('click', async () => {
|
||||
state.serverUrl = $('#server-url').value.trim().replace(/\/$/, '');
|
||||
if (!await ensureServerPermission(state.serverUrl)) throw new Error('Zugriff auf die Serveradresse wurde nicht erlaubt.');
|
||||
await storageSet({ serverUrl: state.serverUrl, provider: $('#provider').value, language: $('#language').value });
|
||||
$('#settings-view').classList.add('hidden'); $('#dashboard-view').classList.remove('hidden');
|
||||
await loadQueue();
|
||||
});
|
||||
$('#next-listing').addEventListener('click', async () => { if (state.queue.length) state.current = (state.current + 1) % state.queue.length; await renderCurrent(); });
|
||||
$('#fill-listing').addEventListener('click', async () => {
|
||||
const listing = state.queue[state.current];
|
||||
if (!listing) return;
|
||||
await storageSet({ pendingFillListingId: listing.id });
|
||||
openUrl('https://www.vinted.de/items/new');
|
||||
window.close();
|
||||
});
|
||||
|
||||
$('#prepare-images').addEventListener('click', () => openUrl(`${state.serverUrl}/#publish`));
|
||||
$('#open-queue').addEventListener('click', () => openUrl(`${state.serverUrl}/#publish`));
|
||||
$('#open-app').addEventListener('click', () => openUrl(state.serverUrl));
|
||||
$('#empty-open-app').addEventListener('click', () => openUrl(state.serverUrl));
|
||||
|
||||
init();
|
||||
@@ -1,337 +0,0 @@
|
||||
/* eslint-disable no-var */
|
||||
var VINTED_CATEGORIES = [
|
||||
{
|
||||
id: 5, name: 'Damen', children: [
|
||||
{ id: 1904, name: 'Kleidung', children: [
|
||||
{ id: 1, name: 'Tops & T-Shirts' },
|
||||
{ id: 2, name: 'Pullover & Strickjacken' },
|
||||
{ id: 3, name: 'Blusen & Hemden' },
|
||||
{ id: 4, name: 'Kleider' },
|
||||
{ id: 6, name: 'Röcke' },
|
||||
{ id: 7, name: 'Hosen & Leggings' },
|
||||
{ id: 8, name: 'Jeans' },
|
||||
{ id: 9, name: 'Jacken & Mäntel' },
|
||||
{ id: 10, name: 'Anzüge & Blazer' },
|
||||
{ id: 11, name: 'Sportbekleidung' },
|
||||
{ id: 12, name: 'Bademode' },
|
||||
{ id: 13, name: 'Unterwäsche & Nachtwäsche' },
|
||||
{ id: 14, name: 'Umstandsmode' },
|
||||
{ id: 15, name: 'Overalls & Jumpsuits' },
|
||||
{ id: 16, name: 'Kostüme & Verkleidungen' },
|
||||
{ id: 1226, name: 'Shorts' },
|
||||
{ id: 1904, name: 'Sonstige Kleidung' },
|
||||
]},
|
||||
{ id: 1905, name: 'Schuhe', children: [
|
||||
{ id: 17, name: 'Sneaker' },
|
||||
{ id: 18, name: 'Stiefel & Stiefeletten' },
|
||||
{ id: 19, name: 'Pumps & High Heels' },
|
||||
{ id: 20, name: 'Sandalen' },
|
||||
{ id: 21, name: 'Ballerinas' },
|
||||
{ id: 22, name: 'Schnürschuhe & Slipper' },
|
||||
{ id: 23, name: 'Sportschuhe' },
|
||||
{ id: 24, name: 'Hausschuhe' },
|
||||
{ id: 1905, name: 'Sonstige Schuhe' },
|
||||
]},
|
||||
{ id: 1906, name: 'Taschen', children: [
|
||||
{ id: 25, name: 'Handtaschen' },
|
||||
{ id: 26, name: 'Umhängetaschen' },
|
||||
{ id: 27, name: 'Rucksäcke' },
|
||||
{ id: 28, name: 'Clutches & Abendtaschen' },
|
||||
{ id: 29, name: 'Geldbörsen & Etuis' },
|
||||
{ id: 30, name: 'Shopper & Tragetaschen' },
|
||||
{ id: 1906, name: 'Sonstige Taschen' },
|
||||
]},
|
||||
{ id: 1907, name: 'Accessoires', children: [
|
||||
{ id: 31, name: 'Schmuck' },
|
||||
{ id: 32, name: 'Uhren' },
|
||||
{ id: 33, name: 'Sonnenbrillen' },
|
||||
{ id: 34, name: 'Schals & Tücher' },
|
||||
{ id: 35, name: 'Gürtel' },
|
||||
{ id: 36, name: 'Mützen & Hüte' },
|
||||
{ id: 37, name: 'Handschuhe' },
|
||||
{ id: 1907, name: 'Sonstige Accessoires' },
|
||||
]},
|
||||
{ id: 1919, name: 'Pflege & Kosmetik', children: [
|
||||
{ id: 38, name: 'Makeup' },
|
||||
{ id: 39, name: 'Hautpflege' },
|
||||
{ id: 40, name: 'Haarpflege' },
|
||||
{ id: 41, name: 'Parfum' },
|
||||
{ id: 42, name: 'Nagelpflege' },
|
||||
{ id: 1919, name: 'Sonstige Pflege' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1206, name: 'Herren', children: [
|
||||
{ id: 1908, name: 'Kleidung', children: [
|
||||
{ id: 50, name: 'T-Shirts & Poloshirts' },
|
||||
{ id: 51, name: 'Pullover & Hoodies' },
|
||||
{ id: 52, name: 'Hemden' },
|
||||
{ id: 53, name: 'Hosen' },
|
||||
{ id: 54, name: 'Jeans' },
|
||||
{ id: 55, name: 'Shorts' },
|
||||
{ id: 56, name: 'Jacken & Mäntel' },
|
||||
{ id: 57, name: 'Anzüge & Sakkos' },
|
||||
{ id: 58, name: 'Sportbekleidung' },
|
||||
{ id: 59, name: 'Bademode' },
|
||||
{ id: 60, name: 'Unterwäsche & Nachtwäsche' },
|
||||
{ id: 61, name: 'Kostüme & Verkleidungen' },
|
||||
{ id: 1908, name: 'Sonstige Kleidung' },
|
||||
]},
|
||||
{ id: 1909, name: 'Schuhe', children: [
|
||||
{ id: 62, name: 'Sneaker' },
|
||||
{ id: 63, name: 'Stiefel & Stiefeletten' },
|
||||
{ id: 64, name: 'Business-Schuhe' },
|
||||
{ id: 65, name: 'Sandalen' },
|
||||
{ id: 66, name: 'Sportschuhe' },
|
||||
{ id: 67, name: 'Hausschuhe' },
|
||||
{ id: 1909, name: 'Sonstige Schuhe' },
|
||||
]},
|
||||
{ id: 1910, name: 'Taschen', children: [
|
||||
{ id: 68, name: 'Rucksäcke' },
|
||||
{ id: 69, name: 'Umhängetaschen' },
|
||||
{ id: 70, name: 'Aktentaschen' },
|
||||
{ id: 71, name: 'Geldbörsen & Etuis' },
|
||||
{ id: 1910, name: 'Sonstige Taschen' },
|
||||
]},
|
||||
{ id: 1911, name: 'Accessoires', children: [
|
||||
{ id: 72, name: 'Uhren' },
|
||||
{ id: 73, name: 'Sonnenbrillen' },
|
||||
{ id: 74, name: 'Krawatten & Fliegen' },
|
||||
{ id: 75, name: 'Gürtel' },
|
||||
{ id: 76, name: 'Mützen & Hüte' },
|
||||
{ id: 77, name: 'Schals' },
|
||||
{ id: 78, name: 'Schmuck' },
|
||||
{ id: 1911, name: 'Sonstige Accessoires' },
|
||||
]},
|
||||
{ id: 1920, name: 'Pflege', children: [
|
||||
{ id: 79, name: 'Rasur & Bartpflege' },
|
||||
{ id: 80, name: 'Hautpflege' },
|
||||
{ id: 81, name: 'Parfum' },
|
||||
{ id: 1920, name: 'Sonstige Pflege' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1207, name: 'Kinder', children: [
|
||||
{ id: 1912, name: 'Mädchen-Kleidung', children: [
|
||||
{ id: 100, name: 'Tops & T-Shirts' },
|
||||
{ id: 101, name: 'Pullover & Strickjacken' },
|
||||
{ id: 102, name: 'Kleider & Röcke' },
|
||||
{ id: 103, name: 'Hosen & Jeans' },
|
||||
{ id: 104, name: 'Jacken & Mäntel' },
|
||||
{ id: 1912, name: 'Sonstige Mädchen-Kleidung' },
|
||||
]},
|
||||
{ id: 1913, name: 'Jungen-Kleidung', children: [
|
||||
{ id: 110, name: 'Tops & T-Shirts' },
|
||||
{ id: 111, name: 'Pullover & Hoodies' },
|
||||
{ id: 112, name: 'Hosen & Jeans' },
|
||||
{ id: 113, name: 'Jacken & Mäntel' },
|
||||
{ id: 1913, name: 'Sonstige Jungen-Kleidung' },
|
||||
]},
|
||||
{ id: 1914, name: 'Baby-Kleidung', children: [
|
||||
{ id: 120, name: 'Strampler & Bodies' },
|
||||
{ id: 121, name: 'Sets & Outfits' },
|
||||
{ id: 122, name: 'Jacken' },
|
||||
{ id: 1914, name: 'Sonstige Baby-Kleidung' },
|
||||
]},
|
||||
{ id: 1915, name: 'Kinderschuhe', children: [
|
||||
{ id: 130, name: 'Sneaker' },
|
||||
{ id: 131, name: 'Stiefel' },
|
||||
{ id: 132, name: 'Sandalen' },
|
||||
{ id: 133, name: 'Sportschuhe' },
|
||||
{ id: 1915, name: 'Sonstige Kinderschuhe' },
|
||||
]},
|
||||
{ id: 1916, name: 'Spielzeug', children: [
|
||||
{ id: 140, name: 'Puppen & Zubehör' },
|
||||
{ id: 141, name: 'Fahrzeuge' },
|
||||
{ id: 142, name: 'Bausteine & Baukästen' },
|
||||
{ id: 143, name: 'Kuscheltiere' },
|
||||
{ id: 144, name: 'Brettspiele & Puzzles' },
|
||||
{ id: 145, name: 'Lernspielzeug' },
|
||||
{ id: 1916, name: 'Sonstiges Spielzeug' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1208, name: 'Haus & Wohnen', children: [
|
||||
{ id: 1917, name: 'Dekoration', children: [
|
||||
{ id: 200, name: 'Bilder & Poster' },
|
||||
{ id: 201, name: 'Kerzen & Windlichter' },
|
||||
{ id: 202, name: 'Vasen' },
|
||||
{ id: 203, name: 'Spiegel' },
|
||||
{ id: 204, name: 'Figuren & Skulpturen' },
|
||||
{ id: 1917, name: 'Sonstige Dekoration' },
|
||||
]},
|
||||
{ id: 245, name: 'Küche & Essen', children: [
|
||||
{ id: 210, name: 'Geschirr' },
|
||||
{ id: 211, name: 'Gläser & Tassen' },
|
||||
{ id: 212, name: 'Besteck' },
|
||||
{ id: 213, name: 'Küchengeräte' },
|
||||
{ id: 214, name: 'Aufbewahrung' },
|
||||
]},
|
||||
{ id: 246, name: 'Textilien', children: [
|
||||
{ id: 220, name: 'Bettwäsche' },
|
||||
{ id: 221, name: 'Handtücher' },
|
||||
{ id: 222, name: 'Kissen & Decken' },
|
||||
{ id: 223, name: 'Teppiche' },
|
||||
{ id: 224, name: 'Vorhänge' },
|
||||
]},
|
||||
{ id: 247, name: 'Möbel', children: [
|
||||
{ id: 230, name: 'Regale' },
|
||||
{ id: 231, name: 'Tische' },
|
||||
{ id: 232, name: 'Stühle' },
|
||||
{ id: 233, name: 'Schränke & Kommoden' },
|
||||
{ id: 234, name: 'Betten' },
|
||||
]},
|
||||
{ id: 248, name: 'Garten', children: [
|
||||
{ id: 240, name: 'Gartenmöbel' },
|
||||
{ id: 241, name: 'Pflanzen & Töpfe' },
|
||||
{ id: 242, name: 'Grill & Outdoor' },
|
||||
{ id: 243, name: 'Gartengeräte' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1209, name: 'Unterhaltung', children: [
|
||||
{ id: 249, name: 'Bücher', children: [
|
||||
{ id: 300, name: 'Romane & Erzählungen' },
|
||||
{ id: 301, name: 'Sachbücher' },
|
||||
{ id: 302, name: 'Kinder- & Jugendbücher' },
|
||||
{ id: 303, name: 'Comics & Manga' },
|
||||
{ id: 304, name: 'Fach- & Schulbücher' },
|
||||
]},
|
||||
{ id: 250, name: 'Musik & Film', children: [
|
||||
{ id: 310, name: 'CDs' },
|
||||
{ id: 311, name: 'Vinyl & Schallplatten' },
|
||||
{ id: 312, name: 'DVDs & Blu-ray' },
|
||||
]},
|
||||
{ id: 251, name: 'Spiele & Konsolen', children: [
|
||||
{ id: 320, name: 'Videospiele' },
|
||||
{ id: 321, name: 'Konsolen' },
|
||||
{ id: 322, name: 'Zubehör' },
|
||||
{ id: 323, name: 'Brettspiele' },
|
||||
]},
|
||||
{ id: 252, name: 'Sport & Freizeit', children: [
|
||||
{ id: 330, name: 'Fitnessgeräte' },
|
||||
{ id: 331, name: 'Fahrräder & Zubehör' },
|
||||
{ id: 332, name: 'Camping & Wandern' },
|
||||
{ id: 333, name: 'Wintersport' },
|
||||
{ id: 334, name: 'Ballsport' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1210, name: 'Elektronik & Technik', children: [
|
||||
{ id: 253, name: 'Handys & Smartphones', children: [
|
||||
{ id: 400, name: 'iPhones' },
|
||||
{ id: 401, name: 'Samsung' },
|
||||
{ id: 402, name: 'Sonstige Smartphones' },
|
||||
{ id: 403, name: 'Handyhüllen & -zubehör' },
|
||||
]},
|
||||
{ id: 254, name: 'Computer & Tablets', children: [
|
||||
{ id: 410, name: 'Laptops' },
|
||||
{ id: 411, name: 'Tablets' },
|
||||
{ id: 412, name: 'PCs & Desktops' },
|
||||
{ id: 413, name: 'Monitore' },
|
||||
{ id: 414, name: 'Netzwerkspeicher (NAS)' },
|
||||
{ id: 415, name: 'Server' },
|
||||
{ id: 416, name: 'Drucker & Scanner' },
|
||||
{ id: 417, name: 'PC-Zubehör' },
|
||||
]},
|
||||
{ id: 255, name: 'Foto & Kamera', children: [
|
||||
{ id: 420, name: 'Digitalkameras' },
|
||||
{ id: 421, name: 'Objektive' },
|
||||
{ id: 422, name: 'Kamerazubehör' },
|
||||
]},
|
||||
{ id: 256, name: 'Audio & HiFi', children: [
|
||||
{ id: 430, name: 'Kopfhörer' },
|
||||
{ id: 431, name: 'Lautsprecher' },
|
||||
{ id: 432, name: 'HiFi-Anlagen' },
|
||||
]},
|
||||
{ id: 257, name: 'TV & Video', children: [
|
||||
{ id: 440, name: 'Fernseher' },
|
||||
{ id: 441, name: 'Streaming-Geräte' },
|
||||
{ id: 442, name: 'Beamer' },
|
||||
]},
|
||||
{ id: 258, name: 'Kabel & Zubehör', children: [
|
||||
{ id: 450, name: 'Kabel' },
|
||||
{ id: 451, name: 'Adapter' },
|
||||
{ id: 452, name: 'Speicherkarten & USB-Sticks' },
|
||||
{ id: 453, name: 'Ladegeräte & Akkus' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1211, name: 'Haustiere', children: [
|
||||
{ id: 259, name: 'Hunde', children: [
|
||||
{ id: 500, name: 'Hundeleine & Halsband' },
|
||||
{ id: 501, name: 'Hundebett & Decke' },
|
||||
{ id: 502, name: 'Hundespielzeug' },
|
||||
{ id: 503, name: 'Hundebekleidung' },
|
||||
]},
|
||||
{ id: 260, name: 'Katzen', children: [
|
||||
{ id: 510, name: 'Kratzbäume' },
|
||||
{ id: 511, name: 'Katzenspielzeug' },
|
||||
{ id: 512, name: 'Katzenbett' },
|
||||
]},
|
||||
{ id: 261, name: 'Sonstige Haustiere' },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
function flattenCategories(tree, path) {
|
||||
const result = [];
|
||||
path = path || [];
|
||||
for (const cat of tree) {
|
||||
const currentPath = [...path, cat.name];
|
||||
if (cat.children) {
|
||||
for (const sub of cat.children) {
|
||||
const subPath = [...currentPath, sub.name];
|
||||
if (sub.children) {
|
||||
for (const leaf of sub.children) {
|
||||
result.push({
|
||||
id: leaf.id,
|
||||
name: leaf.name,
|
||||
path: [...subPath, leaf.name],
|
||||
pathStr: [...subPath, leaf.name].join(' > '),
|
||||
search: [...subPath, leaf.name].join(' ').toLowerCase(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result.push({
|
||||
id: sub.id,
|
||||
name: sub.name,
|
||||
path: subPath,
|
||||
pathStr: subPath.join(' > '),
|
||||
search: subPath.join(' ').toLowerCase(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var VINTED_CATEGORIES_FLAT = flattenCategories(VINTED_CATEGORIES);
|
||||
|
||||
function matchCategory(query) {
|
||||
if (!query) return [];
|
||||
const q = query.toLowerCase().trim();
|
||||
const words = q.split(/\s+/);
|
||||
const scored = VINTED_CATEGORIES_FLAT.map(cat => {
|
||||
let score = 0;
|
||||
const s = cat.search;
|
||||
if (s.includes(q)) score += 100;
|
||||
for (const w of words) {
|
||||
if (w.length < 2) continue;
|
||||
if (s.includes(w)) score += 20;
|
||||
if (cat.name.toLowerCase().includes(w)) score += 30;
|
||||
}
|
||||
// Bonus for exact leaf match
|
||||
if (cat.name.toLowerCase() === q) score += 200;
|
||||
return { ...cat, score };
|
||||
}).filter(c => c.score > 0);
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
return scored.slice(0, 15);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
# Vendoo Browser Extensions
|
||||
|
||||
- `chrome/` – Chrome, Manifest V3
|
||||
- `edge/` – Microsoft Edge, Manifest V3
|
||||
- `firefox/` – Firefox, WebExtension-Paket
|
||||
|
||||
## Lokal laden
|
||||
|
||||
### Chrome
|
||||
`chrome://extensions` öffnen → Entwicklermodus → **Entpackte Erweiterung laden** → Ordner `extensions/chrome` auswählen.
|
||||
|
||||
### Edge
|
||||
`edge://extensions` öffnen → Entwicklermodus → **Entpackte Erweiterung laden** → Ordner `extensions/edge` auswählen.
|
||||
|
||||
### Firefox
|
||||
`about:debugging#/runtime/this-firefox` öffnen → **Temporäres Add-on laden** → `extensions/firefox/manifest.json` auswählen.
|
||||
|
||||
Die Serveradresse kann im Popup geändert werden. Für öffentliche Server muss die Vendoo-Adresse per HTTPS erreichbar sein.
|
||||
|
||||
## Serverzugriff
|
||||
|
||||
Die Erweiterung kann mit `localhost`, einer LAN-Adresse oder einer öffentlichen HTTPS-Adresse verbunden werden. Da die Serveradresse frei konfigurierbar ist, verlangen die Browser beim Laden Zugriff auf HTTP-/HTTPS-Seiten. Die Erweiterung verwendet diesen Zugriff ausschließlich für die eingetragene Vendoo-Instanz und die unterstützten Vinted-Seiten.
|
||||
|
||||
## Alte Erweiterung ersetzen
|
||||
|
||||
Der frühere Ordner `extension/` wird nicht mehr verwendet. Eine bereits geladene Altversion zuerst entfernen und danach den passenden Ordner unter `extensions/` laden.
|
||||
@@ -1,60 +0,0 @@
|
||||
const ext = globalThis.browser || globalThis.chrome;
|
||||
const csrfCache = new Map();
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const chunkSize = 0x8000;
|
||||
let binary = '';
|
||||
for (let offset = 0; offset < bytes.length; offset += chunkSize) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
async function getCsrf(serverUrl) {
|
||||
if (csrfCache.has(serverUrl)) return csrfCache.get(serverUrl);
|
||||
const response = await fetch(`${serverUrl}/api/me`, { credentials: 'include', cache: 'no-store' });
|
||||
if (!response.ok) return '';
|
||||
const data = await response.json();
|
||||
const token = data.csrf || '';
|
||||
csrfCache.set(serverUrl, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
if (msg.type === 'api') {
|
||||
(async () => {
|
||||
try {
|
||||
const { serverUrl, path, method = 'GET', body } = msg;
|
||||
const opts = { method, headers: {}, credentials: 'include', cache: 'no-store' };
|
||||
if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
const csrf = await getCsrf(serverUrl);
|
||||
if (csrf) opts.headers['X-CSRF-Token'] = csrf;
|
||||
}
|
||||
let response = await fetch(`${serverUrl}${path}`, opts);
|
||||
if (response.status === 403 && !['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
csrfCache.delete(serverUrl);
|
||||
const csrf = await getCsrf(serverUrl);
|
||||
if (csrf) opts.headers['X-CSRF-Token'] = csrf;
|
||||
response = await fetch(`${serverUrl}${path}`, opts);
|
||||
}
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
const data = contentType.includes('application/json') ? await response.json() : { error: await response.text() };
|
||||
sendResponse({ ok: response.ok, status: response.status, data });
|
||||
} catch (error) { sendResponse({ ok: false, status: 0, error: error.message }); }
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
if (msg.type === 'fetch-image') {
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch(msg.url, { cache: 'no-store', credentials: 'include' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const blob = await response.blob();
|
||||
if (!blob.type.startsWith('image/')) throw new Error('Antwort ist kein Bild');
|
||||
const buffer = await blob.arrayBuffer();
|
||||
sendResponse({ ok: true, dataUrl: `data:${blob.type};base64,${arrayBufferToBase64(buffer)}`, mime: blob.type, size: blob.size });
|
||||
} catch (error) { sendResponse({ ok: false, error: error.message }); }
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
@@ -1,60 +0,0 @@
|
||||
const ext = globalThis.browser || globalThis.chrome;
|
||||
const csrfCache = new Map();
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const chunkSize = 0x8000;
|
||||
let binary = '';
|
||||
for (let offset = 0; offset < bytes.length; offset += chunkSize) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
async function getCsrf(serverUrl) {
|
||||
if (csrfCache.has(serverUrl)) return csrfCache.get(serverUrl);
|
||||
const response = await fetch(`${serverUrl}/api/me`, { credentials: 'include', cache: 'no-store' });
|
||||
if (!response.ok) return '';
|
||||
const data = await response.json();
|
||||
const token = data.csrf || '';
|
||||
csrfCache.set(serverUrl, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
if (msg.type === 'api') {
|
||||
(async () => {
|
||||
try {
|
||||
const { serverUrl, path, method = 'GET', body } = msg;
|
||||
const opts = { method, headers: {}, credentials: 'include', cache: 'no-store' };
|
||||
if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
const csrf = await getCsrf(serverUrl);
|
||||
if (csrf) opts.headers['X-CSRF-Token'] = csrf;
|
||||
}
|
||||
let response = await fetch(`${serverUrl}${path}`, opts);
|
||||
if (response.status === 403 && !['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
csrfCache.delete(serverUrl);
|
||||
const csrf = await getCsrf(serverUrl);
|
||||
if (csrf) opts.headers['X-CSRF-Token'] = csrf;
|
||||
response = await fetch(`${serverUrl}${path}`, opts);
|
||||
}
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
const data = contentType.includes('application/json') ? await response.json() : { error: await response.text() };
|
||||
sendResponse({ ok: response.ok, status: response.status, data });
|
||||
} catch (error) { sendResponse({ ok: false, status: 0, error: error.message }); }
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
if (msg.type === 'fetch-image') {
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch(msg.url, { cache: 'no-store', credentials: 'include' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const blob = await response.blob();
|
||||
if (!blob.type.startsWith('image/')) throw new Error('Antwort ist kein Bild');
|
||||
const buffer = await blob.arrayBuffer();
|
||||
sendResponse({ ok: true, dataUrl: `data:${blob.type};base64,${arrayBufferToBase64(buffer)}`, mime: blob.type, size: blob.size });
|
||||
} catch (error) { sendResponse({ ok: false, error: error.message }); }
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 698 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,10 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<rect width="128" height="128" rx="10" fill="#fffdf9"/>
|
||||
<g transform="translate(26 22) rotate(-25 17 42)">
|
||||
<rect x="0" y="0" width="28" height="84" rx="10" fill="#ff4a12"/>
|
||||
</g>
|
||||
<path d="M76 24h21l11 12v21L91 70 72 61V39z" fill="#ff4a12"/>
|
||||
<circle cx="94" cy="38" r="6" fill="#fffdf9"/>
|
||||
<circle cx="84" cy="91" r="21" fill="#24262a"/>
|
||||
<path d="m74 91 7 7 14-16" fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 542 B |
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Vendoo Link – Vinted Assistant",
|
||||
"version": "1.6.0",
|
||||
"description": "Vendoo Link mit integriertem Vendoo-Branding, Queue, sicherem Auto-Fill, Bildtransfer und Publishing für Vinted.",
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"http://localhost:8124/*",
|
||||
"http://127.0.0.1:8124/*",
|
||||
"https://www.vinted.de/*",
|
||||
"https://www.vinted.fr/*",
|
||||
"https://www.vinted.co.uk/*",
|
||||
"https://www.vinted.com/*",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://www.vinted.de/*",
|
||||
"https://www.vinted.fr/*",
|
||||
"https://www.vinted.co.uk/*",
|
||||
"https://www.vinted.com/*"
|
||||
],
|
||||
"js": [
|
||||
"vinted-categories.js",
|
||||
"content.js"
|
||||
],
|
||||
"css": [
|
||||
"content.css"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "Vendoo Assistant",
|
||||
"default_icon": {
|
||||
"16": "icon-16.png",
|
||||
"32": "icon-32.png",
|
||||
"48": "icon-48.png",
|
||||
"128": "icon-128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icon-16.png",
|
||||
"32": "icon-32.png",
|
||||
"48": "icon-48.png",
|
||||
"128": "icon-128.png"
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
:root{
|
||||
--bg:#f4f0e9;
|
||||
--surface:#fffdf9;
|
||||
--surface-2:#f8f5ef;
|
||||
--line:#ddd6cb;
|
||||
--ink:#22201d;
|
||||
--muted:#736d64;
|
||||
--brand:#e63b17;
|
||||
--brand-dark:#c92f0d;
|
||||
--success:#218356;
|
||||
--danger:#bd4339;
|
||||
--radius:5px;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{width:420px;min-height:600px;margin:0;background:var(--bg);color:var(--ink);font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
||||
button,input,select{font:inherit}
|
||||
.popup-shell{min-height:600px;display:grid;grid-template-rows:auto 1fr auto;background:var(--bg)}
|
||||
.popup-header{display:flex;align-items:center;gap:8px;padding:12px 12px 10px;border-bottom:1px solid var(--line);background:var(--surface)}
|
||||
.brand-lockup{display:flex;align-items:center;gap:8px;min-width:0}.brand-lockup img{width:28px;height:28px}.brand-lockup>div{display:grid;gap:1px}.brand-lockup strong{font-size:14px;letter-spacing:-.03em}.brand-lockup span{font-size:10px;color:var(--muted)}
|
||||
.connection-chip{margin-left:auto;display:flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid var(--line);background:#efebe4;color:var(--muted);font-size:10px;font-weight:700}.connection-chip i{width:7px;height:7px;background:#999}.connection-chip.ok{background:#edf7f2;color:var(--success);border-color:#b9dec8}.connection-chip.ok i{background:var(--success)}.connection-chip.error{background:#fbeceb;color:var(--danger);border-color:#eac0bc}.connection-chip.error i{background:var(--danger)}
|
||||
.icon-button,.back-button{width:28px;height:28px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);color:var(--muted);cursor:pointer}.icon-button:hover,.back-button:hover{background:var(--surface-2);color:var(--ink)}
|
||||
.popup-content,.settings-view{padding:12px;overflow:auto}
|
||||
.summary-card{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;align-items:center;padding:10px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}
|
||||
.summary-main{display:grid;gap:2px}.eyebrow{color:var(--brand);font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.08em}.summary-main strong{font-size:13px}.summary-main small{color:var(--muted);font-size:10px;line-height:1.4}.summary-side{min-width:52px;padding:8px 7px;border:1px solid var(--line);background:var(--surface-2);text-align:center}.summary-side b{display:block;font-size:18px;color:var(--brand);line-height:1}.summary-side small{display:block;margin-top:4px;font-size:9px;color:var(--muted)}
|
||||
.toolbar-row{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:10px}.tool-button{min-height:30px;padding:0 8px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);color:var(--ink);font-size:11px;font-weight:700;cursor:pointer}.tool-button:hover{border-color:var(--brand);color:var(--brand)}
|
||||
.list-section{margin-top:10px}.section-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px}.section-head strong{font-size:12px}.section-head span{font-size:10px;color:var(--muted)}
|
||||
.queue-list{display:grid;gap:7px}
|
||||
.queue-item{display:grid;grid-template-columns:56px minmax(0,1fr);gap:8px;padding:8px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}
|
||||
.queue-item-image{width:56px;height:64px;border:1px solid var(--line);background:linear-gradient(145deg,#fff,#ece8e0);display:grid;place-items:center;overflow:hidden}.queue-item-image img{width:100%;height:100%;object-fit:contain;padding:3px}
|
||||
.queue-item-main{min-width:0;display:grid;gap:5px}.queue-item-top{display:flex;align-items:start;justify-content:space-between;gap:8px}.queue-item-title{font-size:12px;font-weight:750;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.queue-item-price{white-space:nowrap;font-size:11px;font-weight:800}.queue-item-meta{font-size:10px;color:var(--muted);line-height:1.35}.queue-item-sku{font-size:9px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Consolas,monospace}
|
||||
.queue-item-actions{display:grid;grid-template-columns:repeat(4,1fr);gap:5px}.item-action{min-height:26px;padding:0 6px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);color:var(--ink);font-size:10px;font-weight:700;cursor:pointer}.item-action:hover{border-color:var(--brand);color:var(--brand)}.item-action.primary{border-color:var(--brand);background:var(--brand);color:#fff}.item-action.primary:hover{background:var(--brand-dark)}
|
||||
.empty-card{padding:28px 18px;text-align:center;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}.empty-card strong{font-size:14px}.empty-card p{color:var(--muted);font-size:11px;line-height:1.5}.empty-card button{border:0;background:transparent;color:var(--brand);font-size:11px;font-weight:800;cursor:pointer}
|
||||
.settings-view{display:grid;align-content:start;gap:10px}.settings-heading{display:flex;align-items:center;gap:8px}.settings-heading>div{display:grid}.settings-heading strong{font-size:14px}.settings-heading span{font-size:10px;color:var(--muted)}.setting{display:grid;gap:5px}.setting label{color:var(--muted);font-size:10px;font-weight:700}.setting input,.setting select{width:100%;height:34px;padding:0 9px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);color:var(--ink);font-size:11px}.setting-row{display:grid;grid-template-columns:1fr 1fr;gap:8px}
|
||||
.primary-action{min-height:34px;border:1px solid var(--brand);border-radius:var(--radius);background:var(--brand);color:#fff;font-size:11px;font-weight:800;cursor:pointer}.primary-action:hover{background:var(--brand-dark)}
|
||||
footer{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-top:1px solid var(--line);background:var(--surface);color:var(--muted);font-size:10px}
|
||||
.hidden{display:none!important}
|
||||
@@ -1,96 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Vendoo Link</title>
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="popup-shell">
|
||||
<header class="popup-header">
|
||||
<div class="brand-lockup">
|
||||
<img src="icon-48.png" alt="Vendoo">
|
||||
<div>
|
||||
<strong>Vendoo Link</strong>
|
||||
<span>Vinted Assistant</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="connection-chip" class="connection-chip"><i></i><span>Prüfe …</span></div>
|
||||
<button id="settings-toggle" class="icon-button" type="button" title="Einstellungen">⚙</button>
|
||||
</header>
|
||||
|
||||
<section id="dashboard-view" class="popup-content">
|
||||
<article class="summary-card">
|
||||
<div class="summary-main">
|
||||
<span class="eyebrow">Vinted Queue</span>
|
||||
<strong id="queue-state">Wird geladen …</strong>
|
||||
<small id="queue-summary-text">Offene Artikel werden geladen.</small>
|
||||
</div>
|
||||
<div class="summary-side">
|
||||
<b id="queue-count">0</b>
|
||||
<small>offen</small>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="toolbar-row">
|
||||
<button id="open-queue" class="tool-button" type="button">Queue</button>
|
||||
<button id="open-app" class="tool-button" type="button">Vendoo</button>
|
||||
<button id="reload-list" class="tool-button" type="button">Neu laden</button>
|
||||
</div>
|
||||
|
||||
<section class="list-section">
|
||||
<div class="section-head">
|
||||
<strong>Offene Listings</strong>
|
||||
<span id="list-count">0 Einträge</span>
|
||||
</div>
|
||||
<div id="queue-list" class="queue-list"></div>
|
||||
</section>
|
||||
|
||||
<article id="empty-card" class="empty-card hidden">
|
||||
<strong>Keine offenen Vinted-Listings</strong>
|
||||
<p>Erstelle neue Listings in Vendoo oder stelle weitere Artikel in die Queue.</p>
|
||||
<button id="empty-open-app" type="button">Vendoo öffnen</button>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section id="settings-view" class="settings-view hidden">
|
||||
<div class="settings-heading">
|
||||
<button id="back-dashboard" class="back-button" type="button">←</button>
|
||||
<div><strong>Einstellungen</strong><span>Verbindung und Standardwerte</span></div>
|
||||
</div>
|
||||
<div class="setting">
|
||||
<label for="server-url">Vendoo Server</label>
|
||||
<input id="server-url" type="url" spellcheck="false">
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting">
|
||||
<label for="provider">AI Provider</label>
|
||||
<select id="provider">
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
<option value="claude">Claude</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="local">Ollama</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="setting">
|
||||
<label for="language">Sprache</label>
|
||||
<select id="language">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="nl">Nederlands</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button id="save-settings" class="primary-action" type="button">Einstellungen speichern</button>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<span>Vendoo Link v1.6.0</span>
|
||||
<span>Nur ausfüllen = kein Entwurf</span>
|
||||
</footer>
|
||||
</main>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,178 +0,0 @@
|
||||
const browserApi = globalThis.browser || null;
|
||||
const chromeApi = globalThis.chrome || null;
|
||||
const $ = selector => document.querySelector(selector);
|
||||
const state = { queue: [], serverUrl: 'http://localhost:8124' };
|
||||
|
||||
function sendMessage(message) {
|
||||
if (browserApi) return browserApi.runtime.sendMessage(message);
|
||||
return new Promise((resolve, reject) => {
|
||||
chromeApi.runtime.sendMessage(message, response => {
|
||||
const error = chromeApi.runtime.lastError;
|
||||
if (error) reject(new Error(error.message)); else resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
function storageGet(keys) {
|
||||
if (browserApi) return browserApi.storage.local.get(keys);
|
||||
return new Promise(resolve => chromeApi.storage.local.get(keys, resolve));
|
||||
}
|
||||
function storageSet(values) {
|
||||
if (browserApi) return browserApi.storage.local.set(values);
|
||||
return new Promise(resolve => chromeApi.storage.local.set(values, resolve));
|
||||
}
|
||||
function openTab(url) {
|
||||
if (browserApi) return browserApi.tabs.create({ url });
|
||||
return chromeApi.tabs.create({ url });
|
||||
}
|
||||
async function ensureServerPermission(url) {
|
||||
const parsed = new URL(url);
|
||||
const origin = `${parsed.protocol}//${parsed.host}/*`;
|
||||
const permissions = browserApi?.permissions || chromeApi?.permissions;
|
||||
if (!permissions) return true;
|
||||
if (browserApi) {
|
||||
if (await permissions.contains({ origins: [origin] })) return true;
|
||||
return permissions.request({ origins: [origin] });
|
||||
}
|
||||
return new Promise(resolve => permissions.contains({ origins: [origin] }, has => has ? resolve(true) : permissions.request({ origins: [origin] }, resolve)));
|
||||
}
|
||||
|
||||
async function api(path, method = 'GET', body) {
|
||||
const response = await sendMessage({ type: 'api', serverUrl: state.serverUrl, path, method, body });
|
||||
if (!response?.ok) throw new Error(response?.data?.error || response?.error || 'Server nicht erreichbar');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function setConnection(ok, text) {
|
||||
const chip = $('#connection-chip');
|
||||
chip.className = `connection-chip ${ok ? 'ok' : 'error'}`;
|
||||
chip.querySelector('span').textContent = text;
|
||||
}
|
||||
|
||||
async function imageData(filename) {
|
||||
if (!filename) return '';
|
||||
const url = `${state.serverUrl}/uploads/${String(filename).split('/').map(encodeURIComponent).join('/')}`;
|
||||
const response = await sendMessage({ type: 'fetch-image', url });
|
||||
return response?.ok ? response.dataUrl : '';
|
||||
}
|
||||
|
||||
function formatPrice(value) {
|
||||
const num = Number(value);
|
||||
return Number.isFinite(num) ? `${num.toFixed(2).replace('.', ',')} €` : 'Preis fehlt';
|
||||
}
|
||||
|
||||
async function renderQueue() {
|
||||
const list = $('#queue-list');
|
||||
const empty = $('#empty-card');
|
||||
$('#list-count').textContent = `${state.queue.length} Einträge`;
|
||||
if (!state.queue.length) {
|
||||
list.innerHTML = '';
|
||||
empty.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
empty.classList.add('hidden');
|
||||
list.innerHTML = state.queue.map(item => `
|
||||
<article class="queue-item" data-id="${item.id}">
|
||||
<div class="queue-item-image"><img data-photo="${item.photos?.[0] || ''}" alt="${escapeHtml(item.title || 'Produktbild')}"></div>
|
||||
<div class="queue-item-main">
|
||||
<div class="queue-item-top">
|
||||
<div class="queue-item-title">${escapeHtml(item.title || 'Ohne Titel')}</div>
|
||||
<div class="queue-item-price">${formatPrice(item.price)}</div>
|
||||
</div>
|
||||
<div class="queue-item-meta">${escapeHtml([item.brand, item.size, item.condition].filter(Boolean).join(' · ') || 'Noch keine Eigenschaften')}</div>
|
||||
<div class="queue-item-sku">${escapeHtml(item.sku || `VD-${String(item.id).padStart(4, '0')}`)}</div>
|
||||
<div class="queue-item-actions">
|
||||
<button class="item-action primary" data-action="fill" data-id="${item.id}">Daten</button>
|
||||
<button class="item-action" data-action="fill-all" data-id="${item.id}">Alles</button>
|
||||
<button class="item-action" data-action="detail" data-id="${item.id}">Prüfen</button>
|
||||
<button class="item-action" data-action="app" data-id="${item.id}">Vendoo</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`).join('');
|
||||
|
||||
const imgNodes = [...list.querySelectorAll('img[data-photo]')];
|
||||
for (const img of imgNodes) {
|
||||
const photo = img.dataset.photo;
|
||||
if (!photo) { img.removeAttribute('src'); continue; }
|
||||
const src = await imageData(photo);
|
||||
if (src) img.src = src;
|
||||
}
|
||||
|
||||
list.querySelectorAll('[data-action]').forEach(button => button.addEventListener('click', async event => {
|
||||
const { action, id } = event.currentTarget.dataset;
|
||||
const listing = state.queue.find(item => item.id === Number(id));
|
||||
if (!listing) return;
|
||||
if (action === 'fill') {
|
||||
await storageSet({ pendingFillListingId: listing.id, pendingFillIncludePhotos: false });
|
||||
openTab('https://www.vinted.de/items/new');
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
if (action === 'fill-all') {
|
||||
await storageSet({ pendingFillListingId: listing.id, pendingFillIncludePhotos: true });
|
||||
openTab('https://www.vinted.de/items/new');
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
if (action === 'detail') {
|
||||
await storageSet({ pendingOpenListingId: listing.id });
|
||||
openTab('https://www.vinted.de/items/new');
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
if (action === 'app') {
|
||||
openTab(`${state.serverUrl}/#publish`);
|
||||
window.close();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async function loadQueue() {
|
||||
try {
|
||||
const data = await api('/api/extension/queue?platform=vinted');
|
||||
state.queue = data.queue || [];
|
||||
setConnection(true, 'Verbunden');
|
||||
$('#queue-state').textContent = state.queue.length ? 'Bereit zum Ausfüllen' : 'Queue leer';
|
||||
$('#queue-summary-text').textContent = state.queue.length
|
||||
? 'Sichere Autofill-Aktionen ohne versteckte Veröffentlichung.'
|
||||
: 'Neue Vinted-Listings erscheinen automatisch hier.';
|
||||
$('#queue-count').textContent = String(state.queue.length);
|
||||
await renderQueue();
|
||||
} catch (error) {
|
||||
setConnection(false, 'Offline');
|
||||
$('#queue-state').textContent = 'Server nicht erreichbar';
|
||||
$('#queue-summary-text').textContent = error.message;
|
||||
state.queue = [];
|
||||
await renderQueue();
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const settings = await storageGet(['serverUrl', 'provider', 'language']);
|
||||
state.serverUrl = String(settings.serverUrl || 'http://localhost:8124').replace(/\/$/, '');
|
||||
$('#server-url').value = state.serverUrl;
|
||||
if (settings.provider) $('#provider').value = settings.provider;
|
||||
if (settings.language) $('#language').value = settings.language;
|
||||
await loadQueue();
|
||||
}
|
||||
|
||||
$('#settings-toggle').addEventListener('click', () => { $('#dashboard-view').classList.add('hidden'); $('#settings-view').classList.remove('hidden'); });
|
||||
$('#back-dashboard').addEventListener('click', () => { $('#settings-view').classList.add('hidden'); $('#dashboard-view').classList.remove('hidden'); });
|
||||
$('#save-settings').addEventListener('click', async () => {
|
||||
state.serverUrl = $('#server-url').value.trim().replace(/\/$/, '');
|
||||
if (!await ensureServerPermission(state.serverUrl)) throw new Error('Zugriff auf die Serveradresse wurde nicht erlaubt.');
|
||||
await storageSet({ serverUrl: state.serverUrl, provider: $('#provider').value, language: $('#language').value });
|
||||
$('#settings-view').classList.add('hidden');
|
||||
$('#dashboard-view').classList.remove('hidden');
|
||||
await loadQueue();
|
||||
});
|
||||
$('#reload-list').addEventListener('click', loadQueue);
|
||||
$('#open-queue').addEventListener('click', () => openTab(`${state.serverUrl}/#publish`));
|
||||
$('#open-app').addEventListener('click', () => openTab(state.serverUrl));
|
||||
$('#empty-open-app').addEventListener('click', () => openTab(state.serverUrl));
|
||||
|
||||
function escapeHtml(input) {
|
||||
return String(input ?? '').replace(/[&<>"']/g, ch => ({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[ch]));
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -1,337 +0,0 @@
|
||||
/* eslint-disable no-var */
|
||||
var VINTED_CATEGORIES = [
|
||||
{
|
||||
id: 5, name: 'Damen', children: [
|
||||
{ id: 1904, name: 'Kleidung', children: [
|
||||
{ id: 1, name: 'Tops & T-Shirts' },
|
||||
{ id: 2, name: 'Pullover & Strickjacken' },
|
||||
{ id: 3, name: 'Blusen & Hemden' },
|
||||
{ id: 4, name: 'Kleider' },
|
||||
{ id: 6, name: 'Röcke' },
|
||||
{ id: 7, name: 'Hosen & Leggings' },
|
||||
{ id: 8, name: 'Jeans' },
|
||||
{ id: 9, name: 'Jacken & Mäntel' },
|
||||
{ id: 10, name: 'Anzüge & Blazer' },
|
||||
{ id: 11, name: 'Sportbekleidung' },
|
||||
{ id: 12, name: 'Bademode' },
|
||||
{ id: 13, name: 'Unterwäsche & Nachtwäsche' },
|
||||
{ id: 14, name: 'Umstandsmode' },
|
||||
{ id: 15, name: 'Overalls & Jumpsuits' },
|
||||
{ id: 16, name: 'Kostüme & Verkleidungen' },
|
||||
{ id: 1226, name: 'Shorts' },
|
||||
{ id: 1904, name: 'Sonstige Kleidung' },
|
||||
]},
|
||||
{ id: 1905, name: 'Schuhe', children: [
|
||||
{ id: 17, name: 'Sneaker' },
|
||||
{ id: 18, name: 'Stiefel & Stiefeletten' },
|
||||
{ id: 19, name: 'Pumps & High Heels' },
|
||||
{ id: 20, name: 'Sandalen' },
|
||||
{ id: 21, name: 'Ballerinas' },
|
||||
{ id: 22, name: 'Schnürschuhe & Slipper' },
|
||||
{ id: 23, name: 'Sportschuhe' },
|
||||
{ id: 24, name: 'Hausschuhe' },
|
||||
{ id: 1905, name: 'Sonstige Schuhe' },
|
||||
]},
|
||||
{ id: 1906, name: 'Taschen', children: [
|
||||
{ id: 25, name: 'Handtaschen' },
|
||||
{ id: 26, name: 'Umhängetaschen' },
|
||||
{ id: 27, name: 'Rucksäcke' },
|
||||
{ id: 28, name: 'Clutches & Abendtaschen' },
|
||||
{ id: 29, name: 'Geldbörsen & Etuis' },
|
||||
{ id: 30, name: 'Shopper & Tragetaschen' },
|
||||
{ id: 1906, name: 'Sonstige Taschen' },
|
||||
]},
|
||||
{ id: 1907, name: 'Accessoires', children: [
|
||||
{ id: 31, name: 'Schmuck' },
|
||||
{ id: 32, name: 'Uhren' },
|
||||
{ id: 33, name: 'Sonnenbrillen' },
|
||||
{ id: 34, name: 'Schals & Tücher' },
|
||||
{ id: 35, name: 'Gürtel' },
|
||||
{ id: 36, name: 'Mützen & Hüte' },
|
||||
{ id: 37, name: 'Handschuhe' },
|
||||
{ id: 1907, name: 'Sonstige Accessoires' },
|
||||
]},
|
||||
{ id: 1919, name: 'Pflege & Kosmetik', children: [
|
||||
{ id: 38, name: 'Makeup' },
|
||||
{ id: 39, name: 'Hautpflege' },
|
||||
{ id: 40, name: 'Haarpflege' },
|
||||
{ id: 41, name: 'Parfum' },
|
||||
{ id: 42, name: 'Nagelpflege' },
|
||||
{ id: 1919, name: 'Sonstige Pflege' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1206, name: 'Herren', children: [
|
||||
{ id: 1908, name: 'Kleidung', children: [
|
||||
{ id: 50, name: 'T-Shirts & Poloshirts' },
|
||||
{ id: 51, name: 'Pullover & Hoodies' },
|
||||
{ id: 52, name: 'Hemden' },
|
||||
{ id: 53, name: 'Hosen' },
|
||||
{ id: 54, name: 'Jeans' },
|
||||
{ id: 55, name: 'Shorts' },
|
||||
{ id: 56, name: 'Jacken & Mäntel' },
|
||||
{ id: 57, name: 'Anzüge & Sakkos' },
|
||||
{ id: 58, name: 'Sportbekleidung' },
|
||||
{ id: 59, name: 'Bademode' },
|
||||
{ id: 60, name: 'Unterwäsche & Nachtwäsche' },
|
||||
{ id: 61, name: 'Kostüme & Verkleidungen' },
|
||||
{ id: 1908, name: 'Sonstige Kleidung' },
|
||||
]},
|
||||
{ id: 1909, name: 'Schuhe', children: [
|
||||
{ id: 62, name: 'Sneaker' },
|
||||
{ id: 63, name: 'Stiefel & Stiefeletten' },
|
||||
{ id: 64, name: 'Business-Schuhe' },
|
||||
{ id: 65, name: 'Sandalen' },
|
||||
{ id: 66, name: 'Sportschuhe' },
|
||||
{ id: 67, name: 'Hausschuhe' },
|
||||
{ id: 1909, name: 'Sonstige Schuhe' },
|
||||
]},
|
||||
{ id: 1910, name: 'Taschen', children: [
|
||||
{ id: 68, name: 'Rucksäcke' },
|
||||
{ id: 69, name: 'Umhängetaschen' },
|
||||
{ id: 70, name: 'Aktentaschen' },
|
||||
{ id: 71, name: 'Geldbörsen & Etuis' },
|
||||
{ id: 1910, name: 'Sonstige Taschen' },
|
||||
]},
|
||||
{ id: 1911, name: 'Accessoires', children: [
|
||||
{ id: 72, name: 'Uhren' },
|
||||
{ id: 73, name: 'Sonnenbrillen' },
|
||||
{ id: 74, name: 'Krawatten & Fliegen' },
|
||||
{ id: 75, name: 'Gürtel' },
|
||||
{ id: 76, name: 'Mützen & Hüte' },
|
||||
{ id: 77, name: 'Schals' },
|
||||
{ id: 78, name: 'Schmuck' },
|
||||
{ id: 1911, name: 'Sonstige Accessoires' },
|
||||
]},
|
||||
{ id: 1920, name: 'Pflege', children: [
|
||||
{ id: 79, name: 'Rasur & Bartpflege' },
|
||||
{ id: 80, name: 'Hautpflege' },
|
||||
{ id: 81, name: 'Parfum' },
|
||||
{ id: 1920, name: 'Sonstige Pflege' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1207, name: 'Kinder', children: [
|
||||
{ id: 1912, name: 'Mädchen-Kleidung', children: [
|
||||
{ id: 100, name: 'Tops & T-Shirts' },
|
||||
{ id: 101, name: 'Pullover & Strickjacken' },
|
||||
{ id: 102, name: 'Kleider & Röcke' },
|
||||
{ id: 103, name: 'Hosen & Jeans' },
|
||||
{ id: 104, name: 'Jacken & Mäntel' },
|
||||
{ id: 1912, name: 'Sonstige Mädchen-Kleidung' },
|
||||
]},
|
||||
{ id: 1913, name: 'Jungen-Kleidung', children: [
|
||||
{ id: 110, name: 'Tops & T-Shirts' },
|
||||
{ id: 111, name: 'Pullover & Hoodies' },
|
||||
{ id: 112, name: 'Hosen & Jeans' },
|
||||
{ id: 113, name: 'Jacken & Mäntel' },
|
||||
{ id: 1913, name: 'Sonstige Jungen-Kleidung' },
|
||||
]},
|
||||
{ id: 1914, name: 'Baby-Kleidung', children: [
|
||||
{ id: 120, name: 'Strampler & Bodies' },
|
||||
{ id: 121, name: 'Sets & Outfits' },
|
||||
{ id: 122, name: 'Jacken' },
|
||||
{ id: 1914, name: 'Sonstige Baby-Kleidung' },
|
||||
]},
|
||||
{ id: 1915, name: 'Kinderschuhe', children: [
|
||||
{ id: 130, name: 'Sneaker' },
|
||||
{ id: 131, name: 'Stiefel' },
|
||||
{ id: 132, name: 'Sandalen' },
|
||||
{ id: 133, name: 'Sportschuhe' },
|
||||
{ id: 1915, name: 'Sonstige Kinderschuhe' },
|
||||
]},
|
||||
{ id: 1916, name: 'Spielzeug', children: [
|
||||
{ id: 140, name: 'Puppen & Zubehör' },
|
||||
{ id: 141, name: 'Fahrzeuge' },
|
||||
{ id: 142, name: 'Bausteine & Baukästen' },
|
||||
{ id: 143, name: 'Kuscheltiere' },
|
||||
{ id: 144, name: 'Brettspiele & Puzzles' },
|
||||
{ id: 145, name: 'Lernspielzeug' },
|
||||
{ id: 1916, name: 'Sonstiges Spielzeug' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1208, name: 'Haus & Wohnen', children: [
|
||||
{ id: 1917, name: 'Dekoration', children: [
|
||||
{ id: 200, name: 'Bilder & Poster' },
|
||||
{ id: 201, name: 'Kerzen & Windlichter' },
|
||||
{ id: 202, name: 'Vasen' },
|
||||
{ id: 203, name: 'Spiegel' },
|
||||
{ id: 204, name: 'Figuren & Skulpturen' },
|
||||
{ id: 1917, name: 'Sonstige Dekoration' },
|
||||
]},
|
||||
{ id: 245, name: 'Küche & Essen', children: [
|
||||
{ id: 210, name: 'Geschirr' },
|
||||
{ id: 211, name: 'Gläser & Tassen' },
|
||||
{ id: 212, name: 'Besteck' },
|
||||
{ id: 213, name: 'Küchengeräte' },
|
||||
{ id: 214, name: 'Aufbewahrung' },
|
||||
]},
|
||||
{ id: 246, name: 'Textilien', children: [
|
||||
{ id: 220, name: 'Bettwäsche' },
|
||||
{ id: 221, name: 'Handtücher' },
|
||||
{ id: 222, name: 'Kissen & Decken' },
|
||||
{ id: 223, name: 'Teppiche' },
|
||||
{ id: 224, name: 'Vorhänge' },
|
||||
]},
|
||||
{ id: 247, name: 'Möbel', children: [
|
||||
{ id: 230, name: 'Regale' },
|
||||
{ id: 231, name: 'Tische' },
|
||||
{ id: 232, name: 'Stühle' },
|
||||
{ id: 233, name: 'Schränke & Kommoden' },
|
||||
{ id: 234, name: 'Betten' },
|
||||
]},
|
||||
{ id: 248, name: 'Garten', children: [
|
||||
{ id: 240, name: 'Gartenmöbel' },
|
||||
{ id: 241, name: 'Pflanzen & Töpfe' },
|
||||
{ id: 242, name: 'Grill & Outdoor' },
|
||||
{ id: 243, name: 'Gartengeräte' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1209, name: 'Unterhaltung', children: [
|
||||
{ id: 249, name: 'Bücher', children: [
|
||||
{ id: 300, name: 'Romane & Erzählungen' },
|
||||
{ id: 301, name: 'Sachbücher' },
|
||||
{ id: 302, name: 'Kinder- & Jugendbücher' },
|
||||
{ id: 303, name: 'Comics & Manga' },
|
||||
{ id: 304, name: 'Fach- & Schulbücher' },
|
||||
]},
|
||||
{ id: 250, name: 'Musik & Film', children: [
|
||||
{ id: 310, name: 'CDs' },
|
||||
{ id: 311, name: 'Vinyl & Schallplatten' },
|
||||
{ id: 312, name: 'DVDs & Blu-ray' },
|
||||
]},
|
||||
{ id: 251, name: 'Spiele & Konsolen', children: [
|
||||
{ id: 320, name: 'Videospiele' },
|
||||
{ id: 321, name: 'Konsolen' },
|
||||
{ id: 322, name: 'Zubehör' },
|
||||
{ id: 323, name: 'Brettspiele' },
|
||||
]},
|
||||
{ id: 252, name: 'Sport & Freizeit', children: [
|
||||
{ id: 330, name: 'Fitnessgeräte' },
|
||||
{ id: 331, name: 'Fahrräder & Zubehör' },
|
||||
{ id: 332, name: 'Camping & Wandern' },
|
||||
{ id: 333, name: 'Wintersport' },
|
||||
{ id: 334, name: 'Ballsport' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1210, name: 'Elektronik & Technik', children: [
|
||||
{ id: 253, name: 'Handys & Smartphones', children: [
|
||||
{ id: 400, name: 'iPhones' },
|
||||
{ id: 401, name: 'Samsung' },
|
||||
{ id: 402, name: 'Sonstige Smartphones' },
|
||||
{ id: 403, name: 'Handyhüllen & -zubehör' },
|
||||
]},
|
||||
{ id: 254, name: 'Computer & Tablets', children: [
|
||||
{ id: 410, name: 'Laptops' },
|
||||
{ id: 411, name: 'Tablets' },
|
||||
{ id: 412, name: 'PCs & Desktops' },
|
||||
{ id: 413, name: 'Monitore' },
|
||||
{ id: 414, name: 'Netzwerkspeicher (NAS)' },
|
||||
{ id: 415, name: 'Server' },
|
||||
{ id: 416, name: 'Drucker & Scanner' },
|
||||
{ id: 417, name: 'PC-Zubehör' },
|
||||
]},
|
||||
{ id: 255, name: 'Foto & Kamera', children: [
|
||||
{ id: 420, name: 'Digitalkameras' },
|
||||
{ id: 421, name: 'Objektive' },
|
||||
{ id: 422, name: 'Kamerazubehör' },
|
||||
]},
|
||||
{ id: 256, name: 'Audio & HiFi', children: [
|
||||
{ id: 430, name: 'Kopfhörer' },
|
||||
{ id: 431, name: 'Lautsprecher' },
|
||||
{ id: 432, name: 'HiFi-Anlagen' },
|
||||
]},
|
||||
{ id: 257, name: 'TV & Video', children: [
|
||||
{ id: 440, name: 'Fernseher' },
|
||||
{ id: 441, name: 'Streaming-Geräte' },
|
||||
{ id: 442, name: 'Beamer' },
|
||||
]},
|
||||
{ id: 258, name: 'Kabel & Zubehör', children: [
|
||||
{ id: 450, name: 'Kabel' },
|
||||
{ id: 451, name: 'Adapter' },
|
||||
{ id: 452, name: 'Speicherkarten & USB-Sticks' },
|
||||
{ id: 453, name: 'Ladegeräte & Akkus' },
|
||||
]},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 1211, name: 'Haustiere', children: [
|
||||
{ id: 259, name: 'Hunde', children: [
|
||||
{ id: 500, name: 'Hundeleine & Halsband' },
|
||||
{ id: 501, name: 'Hundebett & Decke' },
|
||||
{ id: 502, name: 'Hundespielzeug' },
|
||||
{ id: 503, name: 'Hundebekleidung' },
|
||||
]},
|
||||
{ id: 260, name: 'Katzen', children: [
|
||||
{ id: 510, name: 'Kratzbäume' },
|
||||
{ id: 511, name: 'Katzenspielzeug' },
|
||||
{ id: 512, name: 'Katzenbett' },
|
||||
]},
|
||||
{ id: 261, name: 'Sonstige Haustiere' },
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
function flattenCategories(tree, path) {
|
||||
const result = [];
|
||||
path = path || [];
|
||||
for (const cat of tree) {
|
||||
const currentPath = [...path, cat.name];
|
||||
if (cat.children) {
|
||||
for (const sub of cat.children) {
|
||||
const subPath = [...currentPath, sub.name];
|
||||
if (sub.children) {
|
||||
for (const leaf of sub.children) {
|
||||
result.push({
|
||||
id: leaf.id,
|
||||
name: leaf.name,
|
||||
path: [...subPath, leaf.name],
|
||||
pathStr: [...subPath, leaf.name].join(' > '),
|
||||
search: [...subPath, leaf.name].join(' ').toLowerCase(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result.push({
|
||||
id: sub.id,
|
||||
name: sub.name,
|
||||
path: subPath,
|
||||
pathStr: subPath.join(' > '),
|
||||
search: subPath.join(' ').toLowerCase(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var VINTED_CATEGORIES_FLAT = flattenCategories(VINTED_CATEGORIES);
|
||||
|
||||
function matchCategory(query) {
|
||||
if (!query) return [];
|
||||
const q = query.toLowerCase().trim();
|
||||
const words = q.split(/\s+/);
|
||||
const scored = VINTED_CATEGORIES_FLAT.map(cat => {
|
||||
let score = 0;
|
||||
const s = cat.search;
|
||||
if (s.includes(q)) score += 100;
|
||||
for (const w of words) {
|
||||
if (w.length < 2) continue;
|
||||
if (s.includes(w)) score += 20;
|
||||
if (cat.name.toLowerCase().includes(w)) score += 30;
|
||||
}
|
||||
// Bonus for exact leaf match
|
||||
if (cat.name.toLowerCase() === q) score += 200;
|
||||
return { ...cat, score };
|
||||
}).filter(c => c.score > 0);
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
return scored.slice(0, 15);
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
const ext = globalThis.browser || globalThis.chrome;
|
||||
const csrfCache = new Map();
|
||||
|
||||
function arrayBufferToBase64(buffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const chunkSize = 0x8000;
|
||||
let binary = '';
|
||||
for (let offset = 0; offset < bytes.length; offset += chunkSize) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
async function getCsrf(serverUrl) {
|
||||
if (csrfCache.has(serverUrl)) return csrfCache.get(serverUrl);
|
||||
const response = await fetch(`${serverUrl}/api/me`, { credentials: 'include', cache: 'no-store' });
|
||||
if (!response.ok) return '';
|
||||
const data = await response.json();
|
||||
const token = data.csrf || '';
|
||||
csrfCache.set(serverUrl, token);
|
||||
return token;
|
||||
}
|
||||
|
||||
ext.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||
if (msg.type === 'api') {
|
||||
(async () => {
|
||||
try {
|
||||
const { serverUrl, path, method = 'GET', body } = msg;
|
||||
const opts = { method, headers: {}, credentials: 'include', cache: 'no-store' };
|
||||
if (body !== undefined) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); }
|
||||
if (!['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
const csrf = await getCsrf(serverUrl);
|
||||
if (csrf) opts.headers['X-CSRF-Token'] = csrf;
|
||||
}
|
||||
let response = await fetch(`${serverUrl}${path}`, opts);
|
||||
if (response.status === 403 && !['GET', 'HEAD', 'OPTIONS'].includes(method)) {
|
||||
csrfCache.delete(serverUrl);
|
||||
const csrf = await getCsrf(serverUrl);
|
||||
if (csrf) opts.headers['X-CSRF-Token'] = csrf;
|
||||
response = await fetch(`${serverUrl}${path}`, opts);
|
||||
}
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
const data = contentType.includes('application/json') ? await response.json() : { error: await response.text() };
|
||||
sendResponse({ ok: response.ok, status: response.status, data });
|
||||
} catch (error) { sendResponse({ ok: false, status: 0, error: error.message }); }
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
if (msg.type === 'fetch-image') {
|
||||
(async () => {
|
||||
try {
|
||||
const response = await fetch(msg.url, { cache: 'no-store', credentials: 'include' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const blob = await response.blob();
|
||||
if (!blob.type.startsWith('image/')) throw new Error('Antwort ist kein Bild');
|
||||
const buffer = await blob.arrayBuffer();
|
||||
sendResponse({ ok: true, dataUrl: `data:${blob.type};base64,${arrayBufferToBase64(buffer)}`, mime: blob.type, size: blob.size });
|
||||
} catch (error) { sendResponse({ ok: false, error: error.message }); }
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 698 B |