Compare commits
No commits in common. "de5f5330963cbe9ca0dc7f7af3e438fd8d0e5c89" and "3ebadf08f34cf66ee61bed58f1808a0cc4ab8e80" have entirely different histories.
de5f533096
...
3ebadf08f3
17 changed files with 115 additions and 1105 deletions
|
|
@ -14,15 +14,8 @@
|
|||
| FünfVorAcht / Telegram KI-Poster | fuenfvoracht/STATE.md |
|
||||
| Server / Container / Proxmox | infrastructure/STATE.md |
|
||||
| Telegram Bots allgemein | infrastructure/STATE.md |
|
||||
| TODOs / Aufgaben / Was steht an | Forgejo Issues (siehe unten) |
|
||||
| Alle Projekte / Übersicht | MASTER_INDEX.md |
|
||||
|
||||
## TODO-Liste (Forgejo)
|
||||
- Lesen: `curl -s -H "Authorization: token b874766bdf357bd4c32fa4369d0c588fc6193336" http://100.89.246.60:3000/api/v1/repos/orbitalo/homelab-brain/issues?state=open`
|
||||
- Web: http://100.89.246.60:3000/orbitalo/homelab-brain/issues
|
||||
- Neues TODO: POST an `/api/v1/repos/orbitalo/homelab-brain/issues` mit `{"title":"...","body":"...","labels":[ID]}`
|
||||
- Erledigt: PATCH mit `{"state":"closed"}`
|
||||
|
||||
## Server-Zugang (immer verfügbar)
|
||||
- pve-hetzner: `ssh root@100.88.230.59` | PW: Astral-Proxmox!2026
|
||||
- pve1 Kambodscha: `ssh root@192.168.0.197` | PW: astral66
|
||||
|
|
|
|||
|
|
@ -8,32 +8,20 @@
|
|||
|---|---|---|---|
|
||||
| **Arakava News** (WordPress + RSS + KI) | Orbitalo/Wordpress-V3-MCP-Projekt | arakava-news/STATE.md | arakava-news/src/ |
|
||||
| **Edelmetall Dashboard** (Gold/Silber) | — (in diesem Repo) | edelmetall/STATE.md | edelmetall/src/ |
|
||||
| **Smart Home** (ioBroker, Grafana) | — (in diesem Repo) | smart-home/STATE.md | smart-home/scripts/ |
|
||||
| **Smart Home Muldenstein** (ioBroker, Grafana) | — (in diesem Repo) | smart-home/STATE.md | smart-home/scripts/ |
|
||||
| **ESP32 Projekte** (Heizung, Sensor) | — (in diesem Repo) | esp32/PLAN.md | — |
|
||||
| **FünfVorAcht** (Telegram KI-Poster) | — (in diesem Repo) | fuenfvoracht/STATE.md | fuenfvoracht/src/ |
|
||||
| **Redakteur** (WordPress KI-Autor) | git.orbitalo.net/orbitalo/redakteur | redakteur/STATE.md | redakteur/src/ |
|
||||
| **Flugpreisscanner** (FRA→PNH, Selenium, KI) | git.orbitalo.net/orbitalo/flugpreisscanner | flugpreisscanner/STATE.md | flugpreisscanner/src/ |
|
||||
| **Infrastruktur** (alle Server + CTs) | — (in diesem Repo) | infrastructure/STATE.md | — |
|
||||
|
||||
## Prioritäten
|
||||
## Server
|
||||
|
||||
1. **Arakava News** (WordPress + RSS-Manager) — Prio 1
|
||||
2. **FünfVorAcht** (Telegram KI-Poster) — Prio 1
|
||||
3. Rest — bei Bedarf
|
||||
|
||||
## Physische Standorte
|
||||
|
||||
| Standort | Server | Hardware | Tailscale IP | Funktion |
|
||||
|---|---|---|---|---|
|
||||
| Hetzner Cloud | pve-hetzner | Dedicated Server | 100.88.230.59 | Hauptserver, alle Projekte |
|
||||
| Kambodscha | pve1 | Dell Optiplex Mini | 100.122.56.60 | Heimserver, Edelmetall, Smart Home |
|
||||
| Bei Helmut (Kumpel) | helmut-pve | Dell Optiplex Mini | 100.87.235.11 | Backup-Agent, Filebrowser |
|
||||
| Muldenstein | pve2-1 | Dell Optiplex Mini | 100.99.101.37 | Pizza-Shops, PC-Shops, Taxi, Tools (22 CTs aktiv) |
|
||||
| Muldenstein | pve3 | Dell Optiplex Mini | 100.109.101.12 | Syncthing, WireGuard, Flugscanner-Node, MQTT |
|
||||
| Muldenstein | pbs-1 | PBS Server | 100.99.139.22 | Proxmox Backup Server (aktiv, 20 GB Traffic) |
|
||||
| Muldenstein | KI-Tower | Tower + RTX 3090 | — | Geplant: Lokaler KI-Server (Ollama) |
|
||||
|
||||
**Aktueller Aufenthalt:** Kambodscha (bis ca. Ende März 2026)
|
||||
| Server | Standort | Tailscale IP | Funktion |
|
||||
|---|---|---|---|
|
||||
| pve-hetzner | Deutschland | 100.88.230.59 | Hauptserver (CT 100-110, 144, 999) |
|
||||
| pve1 | Kambodscha | 192.168.0.197 (lokal) / 100.122.56.60 (TS) | Heimserver (CT 136, 888, 999-Mirror) |
|
||||
| pve3 | Muldenstein, DE | 100.109.101.12 | Smart Home (CT 143, 134) |
|
||||
|
||||
## Wichtigste Zugangsdaten
|
||||
|
||||
|
|
@ -41,28 +29,21 @@
|
|||
|---|---|
|
||||
| pve-hetzner SSH | root / Astral-Proxmox!2026 |
|
||||
| pve1 SSH | root / astral66 |
|
||||
| helmut-pve SSH | root / astral66 |
|
||||
| Alle lokalen CTs | root / astral66 |
|
||||
| WordPress Admin | admin / eJIyhW0p5PFacjvvKGufKeXS |
|
||||
| WordPress Admin | admin / astral66 |
|
||||
| Seafile | admin@orbitalo.net / astral66 |
|
||||
| Forgejo | orbitalo / astral66 |
|
||||
| n8n | wuttig@gmx.de / Astral66 |
|
||||
| Dify | admin@orbitalo.net / astral66 |
|
||||
| Grafana | admin / astral66 |
|
||||
|
||||
## Telegram Bots
|
||||
|
||||
| Bot | Token | Chat-ID | Projekt |
|
||||
|---|---|---|---|
|
||||
| @MutterbotAI_bot | 8551565940:AAHIUpZND-tCNGv9yEoNPRyPt4GxEPYBJdE | 674951792 | RSS-Manager / Allgemein |
|
||||
| @Diendemleben_bot | 8799990587:AAEoQuohGdoJ2WudoOHs_j5Ns3iwft6OlFc | 674951792 | FünfVorAcht |
|
||||
| Edelmetall Bot | 8262992299:AAEf8YHPsz42ZdP85DV7JqC4822Ts75GqF4 | 674951792 | Gold/Silber Preise |
|
||||
|
||||
## TODO-Liste
|
||||
|
||||
**Zentral in Forgejo (Repo `orbitalo/homelab-brain`):**
|
||||
- Web: http://100.89.246.60:3000/orbitalo/homelab-brain/issues
|
||||
- API-Token: `b874766bdf357bd4c32fa4369d0c588fc6193336`
|
||||
|
||||
**Labels:** prio-1, wordpress, fuenfvoracht, infrastruktur, flugscanner, ki-tower, wartung, nice-to-have
|
||||
| @MutterbotAI_bot | (in infrastructure/STATE.md) | 674951792 | Moltbot allgemein |
|
||||
| @DifyRagBot | 8390483455:AAEUyRWkvESSGQBtvjzAIQ5UKqmpoMTQZ00 | 674951792 | Dify RAG / Grafana Alerts |
|
||||
| Arakava Comments | 8551565940:AAHIUpZND-tCNGv9yEoNPRyPt4GxEPYBJdE | 674951792 | WordPress Kommentare |
|
||||
| Edelmetall Bot | 8262992299:AAEf8YHPsz42ZdP85DV7JqC4822Ts75GqF4 | 674951792 | Gold/Silber Preise (CT 136) |
|
||||
|
||||
## Auto-Sync
|
||||
Die STATE.md Dateien werden täglich um 03:00 Uhr automatisch aktualisiert.
|
||||
|
|
|
|||
|
|
@ -1,20 +1,5 @@
|
|||
# Infrastruktur — Live State
|
||||
> Aktualisiert: 2026-03-01
|
||||
|
||||
## Physische Standorte
|
||||
|
||||
| Standort | Hardware | Tailscale IP | Funktion | Status |
|
||||
|----------|----------|-------------|----------|--------|
|
||||
| **Hetzner Cloud** | Dedicated Server | 100.88.230.59 | pve-hetzner — Hauptserver, alle Projekte | ✅ Läuft |
|
||||
| **Kambodscha** | Dell Optiplex Mini | 100.122.56.60 | pve1 — Heimserver, Edelmetall, Smart Home | ✅ Läuft |
|
||||
| **Bei Helmut** (Kumpel) | Dell Optiplex Mini | 100.87.235.11 | helmut-pve — Backup-Agent, Filebrowser | ✅ Läuft |
|
||||
| **Muldenstein** | Dell Optiplex Minis | 100.99.101.37 / 100.109.101.12 | pve2-1 + pve3 — Pizza-Shops, Scraper, Tools | ✅ Läuft |
|
||||
| **Muldenstein** | PBS Server | 100.99.139.22 | pbs-1 — Proxmox Backup Server | ✅ Läuft (20 GB Traffic) |
|
||||
| **Muldenstein** | Tower, RTX 3090 | — | KI-Tower — geplant als lokaler KI-Server | ⚠️ Windows, Netzwerkprobleme, Neuaufsetzen geplant |
|
||||
|
||||
**Aktueller Aufenthalt:** Kambodscha (noch ~4 Wochen, bis ca. Ende März 2026)
|
||||
|
||||
---
|
||||
> Auto-generiert: 2026-03-03 10:15
|
||||
|
||||
## pve-hetzner Disk
|
||||
| Mount | Belegt |
|
||||
|
|
@ -23,109 +8,17 @@
|
|||
| /var/lib/vz (VMs/CTs) | 2% von 2.9T |
|
||||
|
||||
## Aktive Container auf pve-hetzner
|
||||
|
||||
| CT | Name | Tailscale IP | Dienste |
|
||||
|---|---|---|---|
|
||||
| 101 | wordpress-v2 | 100.91.212.19 | WordPress + MySQL (Docker) + **CF Tunnel** |
|
||||
| 101 | wordpress-v2 | 100.91.212.19 | WordPress + MySQL (Docker) |
|
||||
| 103 | seafile | 100.75.247.60 | Seafile (seafile.orbitalo.net) |
|
||||
| 109 | rss-manager | 100.113.244.101 | RSS Manager + Matomo + **CF Tunnel** |
|
||||
| 109 | rss-manager | 100.113.244.101 | RSS Manager + Matomo |
|
||||
| 110 | portainer | 100.109.206.43 | Portainer Docker UI |
|
||||
| 111 | forgejo | 100.89.246.60 | Forgejo Git (http://100.89.246.60:3000) |
|
||||
| 112 | fuenfvoracht | 100.73.171.62 | FünfVorAcht Telegram KI-Poster + **CF Tunnel** |
|
||||
| 113 | redax-wp | 100.69.243.16 | Redax-WP KI-Redakteur |
|
||||
| 115 | flugscanner-hub | 100.92.161.97 | Flugscanner Scheduler + Web-Dashboard |
|
||||
| 144 | muldenstein-backup | — | Backup-Archiv |
|
||||
| 999 | cluster-docu | 100.79.8.49 | Dokumentation (http://100.79.8.49:8080) |
|
||||
|
||||
## Container auf pve1 (Kambodscha)
|
||||
|
||||
| CT | Name | Tailscale IP | Dienste |
|
||||
|---|---|---|---|
|
||||
| 115 | flugscanner-asia | 100.112.190.22 | Flugscanner Scraping-Node Asia |
|
||||
| 136 | gold-silber-v3 | 100.72.230.87 | Edelmetall-Bot |
|
||||
| 143 | smart-home | — | ioBroker + Grafana + InfluxDB |
|
||||
|
||||
## Container auf helmut-pve (bei Kumpel)
|
||||
|
||||
| CT | Name | Tailscale IP | Dienste |
|
||||
|---|---|---|---|
|
||||
| 145 | flugscanner-mu | 100.75.182.15 | Flugscanner Scraping-Node DE (derzeit inaktiv) |
|
||||
| — | — | — | Backup-Agent + Filebrowser |
|
||||
|
||||
## Container auf pve2-1 (Muldenstein) — Pizza-Shops & Tools
|
||||
|
||||
| CT | Name | Status | Tailscale IP | Dienste |
|
||||
|---|---|---|---|---|
|
||||
| 111 | uptimekuma | running | — | Uptime Monitoring |
|
||||
| 112 | myspeed | running | — | Speedtest Tracker |
|
||||
| 113 | pve-scripts-local | running | — | Lokale Scripts |
|
||||
| 114 | djangoadmin | running | — | Django Admin Tools |
|
||||
| 115 | Takeo-PC-Shop-Engl | running | — | PC-Shop Takeo (englisch) |
|
||||
| 116 | Pulse | running | — | **Pulse Monitoring + Cloudflare Tunnel** |
|
||||
| 117 | Intercity-Taxi | running | — | Taxi-Buchung |
|
||||
| 123 | Kofi-Shop-PP | running | — | Kofi Shop Phnom Penh |
|
||||
| 128 | rustdeskserver | running | — | RustDesk Remote Desktop |
|
||||
| 129 | debian | running | — | Allgemein |
|
||||
| 130 | PC-Shop-Takeo | running | 100.70.158.12 | PC-Shop Takeo |
|
||||
| 131 | PC-Shopp-PP | running | 100.98.199.9 | PC-Shop Phnom Penh |
|
||||
| 136 | Seleniumbase | running | — | SeleniumBase Scraper |
|
||||
| 140 | Alfredo-Pizza | running | 100.118.43.100 | Django Pizza-Shop |
|
||||
| 150 | Pizza-Express-Wolfen | running | 100.105.246.18 | Django Pizza-Shop |
|
||||
| 160 | Red-Pizza | running | 100.69.66.101 | Django Pizza-Shop |
|
||||
| 180 | Mellensa-Pizza | running | 100.76.173.1 | Django Pizza-Shop |
|
||||
| 190 | Ali-Baba | running | 100.126.45.101 | Django Pizza-Shop |
|
||||
| 200 | Pizza-Di-Angelo | running | 100.66.182.58 | Django Pizza-Shop |
|
||||
| 500 | Test-Shop | running | 100.98.217.121 | Test-Umgebung |
|
||||
| 501 | Test-Shop-Prod | running | — | Test-Umgebung |
|
||||
| 502 | Test-Shop-2 | running | — | Test-Umgebung |
|
||||
|
||||
**Gestoppt:** CT 110, 118, 119, 120, 121, 122, 124, 125, 126, 132, 133 (alte Klone/Templates)
|
||||
|
||||
**Stack aller Shops:** Django 5.2 + PostgreSQL + Gunicorn + Nginx + Telegram Bot (projektscan2000)
|
||||
|
||||
**Cloudflare Tunnel:** CT 116 (Pulse) — Tunnel-ID `f98f666c-73b8-487b-8327-9aa1edc2145e`
|
||||
- Aktive Routes:
|
||||
- `pulse.orbitalo.info` → `http://192.168.178.200:7655` (Pulse Monitoring)
|
||||
- `pve2-muldenstein.orbitalo.net` → `https://192.168.178.123:8006` (Proxmox pve2-1, TLS Verify: OFF)
|
||||
- Alle Container auf pve2-1 können über diesen Tunnel erreichbar gemacht werden
|
||||
|
||||
## Container auf pve3 (Muldenstein)
|
||||
|
||||
| CT | Name | Status | Tailscale IP | Dienste |
|
||||
|---|---|---|---|---|
|
||||
| 139 | Syncthing-Muldenstein | running | — | Datei-Synchronisation |
|
||||
| 141 | syncthing | running | — | Datei-Synchronisation |
|
||||
| 142 | WG-easy | running | — | WireGuard VPN |
|
||||
| 143 | Raspi-Broker | running | 100.66.78.56 | MQTT Broker (Smart Home) |
|
||||
| 145 | flugscanner-mu | running | 100.75.182.15 | Flugscanner Scraping-Node DE |
|
||||
| 504 | projektscan-template | running | — | Shop-Template |
|
||||
| 144 | BT-Bridge | running (VM) | — | Bluetooth Bridge |
|
||||
|
||||
**Gestoppt:** CT 137 (Template), 138 (SeleniumBase2), 503 (Schawarma-Cursor)
|
||||
|
||||
## Proxmox Backup Server
|
||||
|
||||
| Server | Tailscale IP | Standort | Version | Status |
|
||||
|---|---|---|---|---|
|
||||
| pbs-1 | 100.99.139.22 | Muldenstein | PBS 3.4.0 | ✅ Aktiv (20 GB Traffic) |
|
||||
| pbs | 100.82.175.23 | ? | PBS 3.4.0 | ✅ Online |
|
||||
| pbs-hetzner | 100.126.237.22 | Hetzner | ? | ⚠️ Offline/Auth |
|
||||
|
||||
## KI-Tower (Muldenstein) — geplant
|
||||
|
||||
| Eigenschaft | Wert |
|
||||
|---|---|
|
||||
| Hardware | Tower-Gehäuse, ~1.200€ (ohne GPU) |
|
||||
| GPU | NVIDIA RTX 3090 (24 GB VRAM) |
|
||||
| OS aktuell | Windows (Netzwerkprobleme) |
|
||||
| OS geplant | Neu aufsetzen (Ubuntu Server oder Proxmox) |
|
||||
| Ziel | Lokaler KI-Server (Ollama), Ersatz für OpenRouter/GPT-4o-mini |
|
||||
| Status | ⚠️ Wartet auf Rückkehr aus Kambodscha |
|
||||
|
||||
---
|
||||
|
||||
## Gelöschte Container (24.02.2026)
|
||||
|
||||
| CT | Name | Grund |
|
||||
|---|---|---|
|
||||
| 100 | traefik | Abgelöst durch Cloudflare Tunnel |
|
||||
|
|
@ -133,62 +26,27 @@
|
|||
| 104 | n8n | Nicht aktiv genutzt |
|
||||
| 105 | debian-12 | Nicht genutzt |
|
||||
| 106 | wordpress-news | Abgelöst durch CT 101 |
|
||||
| 113 | matomo | Integriert in CT 109 |
|
||||
|
||||
---
|
||||
|
||||
## Cloudflare Tunnels & Routing
|
||||
|
||||
### pve-hetzner
|
||||
| CT | Tunnel-ID | Public Hostnames |
|
||||
## Container auf pve1 (Kambodscha)
|
||||
| CT | Name | Dienste |
|
||||
|---|---|---|
|
||||
| 101 | 0231beb8-193e-46df-a6ef-4154cf04f374 | arakava-news-2.orbitalo.net → localhost:80 |
|
||||
| 109 | 486454a9-4812-4422-b30b-abd5ada71ce1 | matomo.orbitalo.net → localhost:80 |
|
||||
| 112 | ba4f6f84-45db-4369-a588-c231f9d559ce | fuenfvoracht.orbitalo.net → localhost:8080 |
|
||||
| 136 | gold-silber-v3 | Edelmetall-Bot (Tailscale: 100.72.230.87) |
|
||||
| 143 | smart-home | ioBroker + Grafana + InfluxDB |
|
||||
|
||||
### pve2-1 (Muldenstein)
|
||||
| CT | Tunnel-ID | Public Hostnames |
|
||||
|---|---|---|
|
||||
| 116 (Pulse) | f98f666c-73b8-487b-8327-9aa1edc2145e | pulse.orbitalo.info → http://192.168.178.200:7655<br>pve2-muldenstein.orbitalo.net → https://192.168.178.123:8006 (TLS Verify: OFF) |
|
||||
|
||||
**Lokale IPs:**
|
||||
- pve2-1: 192.168.178.123
|
||||
- pve3: 192.168.178.250
|
||||
|
||||
**Best Practice:**
|
||||
- Tunnel immer auf Host oder dediziertem Tunnel-CT, nie in Dienst-Containern
|
||||
- Ein Tunnel pro Proxmox-Host kann alle Container bedienen
|
||||
- Service-URLs immer mit lokaler IP + Port: `http://192.168.178.xxx:port`
|
||||
- Bei HTTPS-Services: "No TLS Verify" aktivieren (Self-Signed Certificates)
|
||||
|
||||
**Wichtige URLs:**
|
||||
- Pulse Monitoring: https://pulse.orbitalo.info
|
||||
- pve2-1 Proxmox GUI: https://pve2-muldenstein.orbitalo.net
|
||||
- pve3 Proxmox GUI: https://100.109.101.12:8006 (Tailscale, kein Tunnel)
|
||||
## Routing
|
||||
- Cloudflare Tunnel CT 101: arakava-news-2.orbitalo.net → :80
|
||||
- Cloudflare Tunnel CT 109: matomo.orbitalo.net → :80
|
||||
- Kein Traefik, kein PBS-Gateway mehr
|
||||
|
||||
## Zugangsdaten
|
||||
|
||||
| System | Login | Zugang |
|
||||
|---|---|---|
|
||||
| pve-hetzner | root / Astral-Proxmox!2026 | SSH: 100.88.230.59<br>GUI: https://100.88.230.59:8006 |
|
||||
| pve1 (Kambodscha) | root / astral66 | SSH: 100.122.56.60<br>GUI: https://100.122.56.60:8006 |
|
||||
| pve2-1 (Muldenstein) | root / astral66 | SSH: 100.99.101.37<br>GUI: https://pve2-muldenstein.orbitalo.net |
|
||||
| pve3 (Muldenstein) | root / astral66 | SSH: 100.109.101.12<br>GUI: https://100.109.101.12:8006 (Tailscale) |
|
||||
| helmut-pve (Kumpel) | root / astral66 | SSH: 100.87.235.11<br>GUI: https://100.87.235.11:8006 |
|
||||
| Alle CTs | root / astral66 | — |
|
||||
| Seafile | admin@orbitalo.net / astral66 | https://seafile.orbitalo.net |
|
||||
| Forgejo | orbitalo / astral66 | http://100.89.246.60:3000 |
|
||||
- pve-hetzner: root / Astral-Proxmox!2026
|
||||
- pve1: root / astral66
|
||||
- Alle CTs: root / astral66
|
||||
- Seafile: admin@orbitalo.net / astral66
|
||||
- Forgejo: orbitalo / astral66
|
||||
|
||||
## Telegram Bots
|
||||
|
||||
| Bot | Token (Auszug) | Chat-ID | Projekt |
|
||||
|---|---|---|---|
|
||||
| @MutterbotAI_bot | 8551565940:... | 674951792 | RSS-Manager / Allgemein |
|
||||
| @Diendemleben_bot | 8799990587:... | 674951792 | FünfVorAcht |
|
||||
| Edelmetall Bot | 8262992299:... | 674951792 | Gold/Silber Preise |
|
||||
|
||||
## KI-API Kosten
|
||||
|
||||
| Service | Kosten | Verbrauch |
|
||||
| Bot | Token (Auszug) | Chat-ID |
|
||||
|---|---|---|
|
||||
| OpenRouter (GPT-4o-mini) | ~$0,35/Tag | RSS-Manager + Flugscanner Vision |
|
||||
| Cursor Ultra | $200/Monat | Entwicklung |
|
||||
| Mutter (@MutterbotAI_bot) | 8551565940:... | 674951792 |
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# STATE: Redax-WP
|
||||
**Stand: 03.03.2026**
|
||||
**Stand: 28.02.2026**
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
✅ **Vollständig in Betrieb — 03.03.2026**
|
||||
✅ **Vollständig in Betrieb — 28.02.2026**
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -87,40 +87,18 @@ Docker Container:
|
|||
| Feature | Beschreibung |
|
||||
|---------|-------------|
|
||||
| Artikel-Studio | KI-Generierung via OpenRouter (Ton wählbar) |
|
||||
| Artikel-Studio | KI-Generierung via OpenRouter (Ton wählbar) |
|
||||
| **Text-Import** | Quelle-Feld als Textarea — komplette Artikel einfügbar |
|
||||
| **Markdown→HTML** | KI-Output zu WordPress-HTML konvertiert |
|
||||
| **YouTube-Bilder** | YouTube-URL in Artikel → Thumbnail + Featured Image |
|
||||
| **KI-Chat** | Freie Texteingabe mit Artikelkontext |
|
||||
| WP-Entwurf | Artikel als Draft pushen |
|
||||
| Redaktionsplan | 7-Tage-Kalender mit Drag & Drop |
|
||||
| Multi-Publish | Primary + alle aktiven Mirrors |
|
||||
| Publish-Ziele | Einklappbar mit Links zu Website & WP-Admin |
|
||||
| Mirror-Status | Pro Artikel: welche Sites bespielt (✅/❌) |
|
||||
| WP-Entwurf | Artikel direkt als Draft auf Primary pushen + Vorschau-Link |
|
||||
| Redaktionsplan | 7-Tage-Kalender mit Status, Umplanen, Löschen |
|
||||
| Multi-Publish | Beim Veröffentlichen: Primary + alle aktiven Mirrors |
|
||||
| Publish-Ziele | Checkboxen zum Ein-/Ausschalten pro Mirror + Links zu Website & WP-Admin + Zugangsdaten |
|
||||
| Mirror-Status | Pro Artikel: welche Sites wurden bespielt (✅/❌) |
|
||||
| RSS-Queue | Feed-Artikel verwalten, KI-Rewrite, Auto-Publish |
|
||||
| Duplikat-Schutz | Mirror überspringt existierende Artikel |
|
||||
| Duplikat-Schutz | Mirror überspringt Artikel die bereits vorhanden sind |
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
### 03.03.2026 (Latest)
|
||||
- **Text-Import**: Quelle-Feld von `<input>` zu `<textarea>` — ganze Artikel einfügbar
|
||||
- **Neuer Default-Prompt**: „Artikel formatieren & SEO" — formatiert Text UND URLs zu WP-tauglichem Markdown
|
||||
- **Markdown→HTML**: `_to_html()` in `app.py` konvertiert KI-Output automatisch zu HTML beim WP-Push
|
||||
- **YouTube-Thumbnail im Content**: YouTube-URL alleine in eigenem Absatz → anklickbares `<img>` Vorschaubild
|
||||
- **YouTube als Featured Image**: YouTube-Thumbnail wird automatisch als Beitragsbild gesetzt (wenn kein anderes Bild vorhanden)
|
||||
- **Gunicorn-Fix**: `generate()` + `generate_chat()` in `openrouter.py` auf `requests` umgestellt (kein asyncio-Timeout)
|
||||
- `markdown==3.10.2` zu `requirements.txt` hinzugefügt
|
||||
- `docker-compose.yml`: `openrouter.py` + `rss_fetcher.py` als Volume-mounts (persistente Änderungen)
|
||||
|
||||
### 02.03.2026
|
||||
- **KI-Chat** implementiert (freie Promptwahl, Artikelkontext, Übernehmen-Button)
|
||||
- Layout: Redaktionsplan unten, Studio volle Breite
|
||||
- Publish-Ziele einklappbar
|
||||
- Entwurf: WP-Editor-Link + Vorschau-Link
|
||||
- Editor + Vorschau größer
|
||||
|
||||
### 28.02.2026
|
||||
- Multi-Publish implementiert: `WordPressMirrorClient` in `wordpress.py`
|
||||
- `mirror_posts` Tabelle in SQLite für Mirror-Tracking
|
||||
|
|
|
|||
|
|
@ -1,148 +0,0 @@
|
|||
# Plan: Chat-Fenster mit freier Promptwahl
|
||||
|
||||
**Stand:** 02.03.2026
|
||||
**Status:** ✅ **Umgesetzt**
|
||||
**Ziel:** Schnelle, direkte KI-Interaktion ohne Umweg über feste Prompts
|
||||
|
||||
---
|
||||
|
||||
## 1. Übersicht
|
||||
|
||||
| Element | Beschreibung |
|
||||
|--------|--------------|
|
||||
| **Chatfenster** | Unter dem Editor, freie Texteingabe an die KI |
|
||||
| **Kontext** | Aktueller Artikel (Titel, Inhalt, SEO) wird automatisch mitgegeben |
|
||||
| **Prompts** | Feste Prompts bleiben optional (für "KI generieren" aus Quelle) |
|
||||
| **Priorität** | Chat = Hauptweg, feste Prompts = Nebenweg |
|
||||
|
||||
---
|
||||
|
||||
## 2. Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Nav, Status, Publish-Ziele │
|
||||
├──────────────────────────────┬──────────────────────────────┤
|
||||
│ Inhalt (Editor) │ Vorschau │
|
||||
│ │ │
|
||||
├──────────────────────────────┴──────────────────────────────┤
|
||||
│ 💬 KI-Chat (freie Eingabe) │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ Nachrichtenverlauf (User/KI) │ │
|
||||
│ ├───────────────────────────────────────────────────────┤ │
|
||||
│ │ [Eingabefeld] [Senden] │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ SEO, Medien, Buttons │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Redaktionsplan (nach Scrollen) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- Chat direkt unter Editor + Vorschau
|
||||
- **Standard: offen** (schnelle Interaktion), einklappbar mit Chevron „▸ KI-Chat“
|
||||
- Eingabefeld groß genug für längere Anweisungen
|
||||
|
||||
---
|
||||
|
||||
## 3. API
|
||||
|
||||
### Endpoint: `POST /api/chat`
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"message": "Mach den ersten Absatz knackiger",
|
||||
"history": [
|
||||
{"role": "user", "content": "..."},
|
||||
{"role": "assistant", "content": "..."}
|
||||
],
|
||||
"context": {
|
||||
"title": "Aktueller Titel",
|
||||
"content": "Aktueller Inhalt...",
|
||||
"seo_title": "...",
|
||||
"seo_description": "...",
|
||||
"focus_keyword": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"reply": "Antwort der KI...",
|
||||
"suggested_content": "Optional: überarbeiteter Text zum Übernehmen"
|
||||
}
|
||||
```
|
||||
|
||||
- Backend: OpenRouter wie bei `/api/polish`, aber mit Chat-System-Prompt
|
||||
- System-Prompt: „Du bist ein Redakteurs-Assistent. Der User arbeitet an einem Artikel. Kontext: [Titel, Inhalt, SEO]. Antworte kurz und handlungsorientiert. Wenn der User Änderungen will, gib den überarbeiteten Text als `suggested_content` zurück.“
|
||||
|
||||
---
|
||||
|
||||
## 4. Chat-UI (Frontend)
|
||||
|
||||
| Element | Beschreibung |
|
||||
|---------|--------------|
|
||||
| Nachrichtenliste | User-Nachrichten rechts/unterschiedlich, KI links/anders |
|
||||
| Eingabefeld | Textarea oder input, Enter zum Senden |
|
||||
| Senden-Button | Expliziter Klick |
|
||||
| Spinner | Während die KI antwortet |
|
||||
| Übernehmen-Button | Bei Antworten mit `suggested_content` – übernimmt in Editor/SEO |
|
||||
| History | Nur Session (im Speicher), max. 6 Nachrichtenpaare, kein DB-Persistenz (v1) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Ablauf
|
||||
|
||||
1. User tippt im Chat: „Kürze den zweiten Absatz“
|
||||
2. Frontend sendet: message + context (aktueller Artikel) + history (falls vorhanden)
|
||||
3. Backend ruft OpenRouter auf, KI antwortet
|
||||
4. Antwort erscheint im Chat
|
||||
5. Optional: KI liefert `suggested_content` → Button „In Editor übernehmen“
|
||||
|
||||
---
|
||||
|
||||
## 6. Feste Prompts (Anpassung)
|
||||
|
||||
| Änderung | Beschreibung |
|
||||
|----------|--------------|
|
||||
| Quelle + Ton + Prompt | Bleiben für „KI generieren“ (Artikel aus URL/Text erzeugen) |
|
||||
| Position | Können kompakter werden (z.B. eine Zeile), Chat ist prominenter |
|
||||
| Optional | Wenn User nur chatten will: Quelle/Prompt ignorierbar |
|
||||
|
||||
---
|
||||
|
||||
## 7. Aufwand (grobe Schätzung)
|
||||
|
||||
| Teil | Zeilen / Aufwand |
|
||||
|------|------------------|
|
||||
| `POST /api/chat` in app.py | ~40 Zeilen |
|
||||
| Chat-UI in index.html | ~80 Zeilen HTML/JS |
|
||||
| Integration Kontext (getArticleData) | ~5 Zeilen |
|
||||
| **Gesamt** | ~2–3 Stunden |
|
||||
|
||||
---
|
||||
|
||||
## 8. Entscheidungen (festgelegt)
|
||||
|
||||
| Punkt | Entscheidung |
|
||||
|-------|--------------|
|
||||
| **Chat Standard** | Offen (schnelle Interaktion), einklappbar optional |
|
||||
| **History persistieren** | Nein – nur Session, bei Artikel-Wechsel/Reload neu |
|
||||
| **Übernehmen-Button** | Zeigen, wenn `suggested_content` vorhanden |
|
||||
| **Max. History** | 6 Nachrichtenpaare (12 Nachrichten) |
|
||||
|
||||
---
|
||||
|
||||
## 9. Reihenfolge (bei Umsetzung)
|
||||
|
||||
1. API-Endpoint `/api/chat`
|
||||
2. Chat-UI (Eingabe + Senden + Anzeige)
|
||||
3. Kontext-Anbindung (Artikel mitschicken)
|
||||
4. Übernehmen-Button (optional)
|
||||
5. Feinschliff (Spinner, Fehlerbehandlung, Einklappbar)
|
||||
|
||||
---
|
||||
|
||||
*Plan erstellt für Redax-WP. Umsetzung nur nach explizitem OK.*
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
# Redax Prompt-Experimente
|
||||
|
||||
Prompts unter **https://redax.orbitalo.net/prompts** bearbeiten. Neue erstellen, testen, als Standard setzen.
|
||||
|
||||
---
|
||||
|
||||
## Variablen (werden ersetzt)
|
||||
|
||||
- `{tone}` — informativ | meinungsstark | reportage
|
||||
- `{date}` — aktuelles Datum (dd.mm.yyyy)
|
||||
|
||||
**Quelle** kommt als User-Eingabe (URL oder Text).
|
||||
|
||||
---
|
||||
|
||||
## A) Standard (aktuell)
|
||||
|
||||
```
|
||||
Du bist ein erfahrener Redakteur. Schreibe einen vollständigen, gut strukturierten Artikel auf Basis der folgenden Quelle.
|
||||
|
||||
Ton: {tone}
|
||||
Datum: {date}
|
||||
|
||||
Formatierung:
|
||||
- Titel als erste Zeile (ohne #)
|
||||
- Dann den Artikeltext in HTML (H2, H3, <p>, <ul>, <strong>)
|
||||
- Am Ende: SEO_TITLE: [max 60 Zeichen]
|
||||
- SEO_DESC: [max 155 Zeichen]
|
||||
- KEYWORD: [1 Fokus-Keyword]
|
||||
|
||||
Quelle:
|
||||
{source}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## B) Mit Links & Quellen
|
||||
|
||||
```
|
||||
Du bist ein erfahrener Redakteur. Schreibe einen vollständigen, gut strukturierten Artikel auf Basis der folgenden Quelle.
|
||||
|
||||
Ton: {tone}
|
||||
Datum: {date}
|
||||
|
||||
WICHTIG – Quellen und Links:
|
||||
- Behalte wichtige Quellen-Links im Text als <a href="URL">Linktext</a>
|
||||
- Wenn die Quelle eine URL hat: am Ende des Artikels einen Absatz „Weiterlesen beim Original“ mit Link einbauen
|
||||
- Externe Links mit target="_blank" rel="noopener"
|
||||
|
||||
Formatierung:
|
||||
- Titel als erste Zeile (ohne #)
|
||||
- Dann den Artikeltext in HTML (H2, H3, <p>, <ul>, <strong>)
|
||||
- Am Ende: SEO_TITLE: [max 60 Zeichen]
|
||||
- SEO_DESC: [max 155 Zeichen]
|
||||
- KEYWORD: [1 Fokus-Keyword]
|
||||
|
||||
Quelle:
|
||||
{source}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## C) Mit Links & Videos
|
||||
|
||||
```
|
||||
Du bist ein erfahrener Redakteur. Schreibe einen vollständigen, gut strukturierten Artikel auf Basis der folgenden Quelle.
|
||||
|
||||
Ton: {tone}
|
||||
Datum: {date}
|
||||
|
||||
WICHTIG:
|
||||
- Links: Wichtige Quellen als <a href="URL" target="_blank" rel="noopener">Linktext</a> einbauen. Am Ende: „Weiterlesen“-Link zur Originalquelle wenn URL vorhanden.
|
||||
- Videos: Wenn die Quelle ein YouTube- oder Vimeo-Video enthält, den passenden Embed einbauen:
|
||||
YouTube: <iframe src="https://www.youtube.com/embed/VIDEO_ID" width="560" height="315" frameborder="0" allowfullscreen></iframe>
|
||||
Vimeo: <iframe src="https://player.vimeo.com/video/VIDEO_ID" width="560" height="315" frameborder="0" allowfullscreen></iframe>
|
||||
- Nur einbinden wenn das Video den Artikel sinnvoll ergänzt.
|
||||
|
||||
Formatierung:
|
||||
- Titel als erste Zeile (ohne #)
|
||||
- Dann den Artikeltext in HTML (H2, H3, <p>, <ul>, <strong>)
|
||||
- Am Ende: SEO_TITLE: [max 60 Zeichen]
|
||||
- SEO_DESC: [max 155 Zeichen]
|
||||
- KEYWORD: [1 Fokus-Keyword]
|
||||
|
||||
Quelle:
|
||||
{source}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## D) Kompakt & klar
|
||||
|
||||
```
|
||||
Schreibe einen Artikel basierend auf der Quelle. Ton: {tone}. Datum: {date}.
|
||||
|
||||
Output-Format:
|
||||
1. Erste Zeile = Titel (ohne #)
|
||||
2. Artikel in HTML: <p>, <h2>, <h3>, <ul>, <strong>, <a href="...">
|
||||
3. Quellen-Links im Text beibehalten; am Ende „Weiterlesen“-Link wenn URL vorhanden
|
||||
4. Bei Video-Quellen: YouTube/Vimeo iframe einbetten
|
||||
5. Abschluss: SEO_TITLE: [60 Zeichen] / SEO_DESC: [155 Zeichen] / KEYWORD: [1 Wort]
|
||||
|
||||
Quelle:
|
||||
{source}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testablauf
|
||||
|
||||
1. https://redax.orbitalo.net/prompts öffnen
|
||||
2. „+ Neuer Prompt“ → Name z.B. „Mit Links“
|
||||
3. Prompt B oder C einfügen → Speichern → „Standard“ klicken
|
||||
4. Zurück zum Studio, Quelle eingeben (z.B. Artikel-URL), „KI generieren“
|
||||
5. Ergebnis prüfen: Sind Links/Videos drin?
|
||||
6. Bei Bedarf Prompt anpassen und erneut testen
|
||||
|
||||
---
|
||||
|
||||
## Tipp: Quelle = was du eingibst
|
||||
|
||||
Redax fetcht **keinen** Web-Inhalt. Die KI bekommt exakt das, was du in „Quelle“ eintippst.
|
||||
|
||||
- **Nur URL**: KI hat wenig Kontext (kennt evtl. bekannte Seiten, aber unzuverlässig)
|
||||
- **Besser**: Artikeltext kopieren und einfügen — dann hat die KI den vollen Inhalt
|
||||
- Bei RSS: Redax nutzt Titel + URL + Summary aus dem Feed (mehr Kontext)
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
# Redax: Angepasster Prompt + Kambodscha-Quelle zum Selbsttesten
|
||||
|
||||
---
|
||||
|
||||
## 1. Prompt (unter https://redax.orbitalo.net/prompts einfügen)
|
||||
|
||||
**Name:** Sachlich mit SEO
|
||||
|
||||
```
|
||||
Du bist ein erfahrener Redakteur. Schreibe einen vollständigen, gut strukturierten Artikel auf Basis der folgenden Quelle.
|
||||
|
||||
Ton: {tone}
|
||||
Datum: {date}
|
||||
|
||||
REGELN:
|
||||
- Keine konkreten Agenten-, Firmen- oder Personennamen nennen (nur allgemein: "eine Agentur", "Anbieter im Riverside")
|
||||
- Keine nummerierten Listen (1. 2. 3.) und keine Bulletpoints im Fließtext – stattdessen Absätze
|
||||
- Keine Tabellen – Unterschiede im Fließtext erklären
|
||||
- Keine Zusatzinhalte wie "Grafik-Ideen", "Tipps" oder Ähnliches am Ende
|
||||
- Output nur: Titel, Artikel, SEO-Zeilen
|
||||
|
||||
FORMATIERUNG:
|
||||
- Erste Zeile = Titel (ohne #)
|
||||
- Artikel in HTML: <p>, <h2>, <h3>, <strong> – keine <ul>/<ol>
|
||||
- Am Ende des Artikels exakt diese drei Zeilen (ohne ### oder andere Präfixe):
|
||||
SEO_TITLE: [max 60 Zeichen]
|
||||
SEO_DESC: [max 155 Zeichen]
|
||||
KEYWORD: [1 Fokus-Keyword]
|
||||
|
||||
Quelle:
|
||||
{source}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Quelle (ins Feld "Quelle" im Studio einfügen)
|
||||
|
||||
```
|
||||
Titel: Ohne Visum kein Kambodscha – warum dein erster Schritt nicht Hausbau oder Business, sondern Immigration sein sollte
|
||||
|
||||
Zielgruppe: Auswanderungsinteressierte, die länger als touristisch in Kambodscha bleiben wollen.
|
||||
|
||||
Ton: nüchtern, erfahren, realistisch. Keine Romantisierung. Kein Werbestil. Keine Rechtsberatung. Keine Preisgarantien. Keine konkreten Agentennamen.
|
||||
|
||||
Inhalt: Einstieg über stabiles Aufenthaltsmodell. Viele Visa-Agenturen im Riverside-Bereich Phnom Penh. Unterschied: Tourist Visa, Ordinary (Business) Visa, Jahres-Extension. Praxisbeispiel: Eine Visa-Agentur im Riverside organisierte ca. 10 Tage eine saubere Jahresverlängerung. Ablauf: Passabgabe, Bearbeitungszeit, Rückgabe mit gültigem Eintrag. System: Rolle von Agenten, persönliche Netzwerke, Regeln können sich ändern. Strategisch: Aufenthaltsstatus Grundlage für Mietverträge, Geschäftsaufbau, Planung, Verhandlungsposition. Abschluss: Visa-Regelungen ändern sich, jeder prüft selbst aktuelle Infos.
|
||||
|
||||
Länge: 900–1200 Wörter. Zwischenüberschriften. Sachliche Sprache. Keine Dramatisierung.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Schritte zum Testen
|
||||
|
||||
1. https://redax.orbitalo.net öffnen und einloggen
|
||||
2. Zu **Prompts** gehen → "+ Neuer Prompt"
|
||||
3. Prompt aus Abschnitt 1 einfügen → Speichern → "Standard" klicken
|
||||
4. Zurück zum **Studio**
|
||||
5. Quelle aus Abschnitt 2 ins Feld "Quelle (URL oder Text)" einfügen
|
||||
6. Ton: **Informativ**
|
||||
7. "🤖 KI generieren" klicken
|
||||
8. Vorschau erscheint rechts; bei Bedarf "💾 Entwurf" für WordPress-Vorschau
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
# STATE: Redax-WP (Redakteur)
|
||||
**Stand: 02.03.2026**
|
||||
**Stand: 28.02.2026**
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
✅ **Vollständig in Betrieb — Multi-Publish, KI-Chat, KI-Serie**
|
||||
✅ **Vollständig in Betrieb — Multi-Publish, KI-Serie, animierte Grafiken**
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
│ ├── app.py Flask-App, Scheduler, alle Routes
|
||||
│ ├── wordpress.py WordPressClient + WordPressMirrorClient
|
||||
│ ├── database.py SQLite Schema + Helpers
|
||||
│ ├── openrouter.py OpenRouter API (generate + generate_chat für KI-Chat)
|
||||
│ ├── openrouter.py OpenRouter API (sync wrapper)
|
||||
│ ├── rss_fetcher.py RSS Feed Parser
|
||||
│ ├── logger.py JSON Logging
|
||||
│ ├── Dockerfile.web Docker Image Build
|
||||
|
|
@ -63,8 +63,7 @@ redax-db MySQL 8
|
|||
|
||||
### KI-Artikel
|
||||
- Quelle eingeben → Ton wählen → KI generiert Artikel + SEO-Felder automatisch
|
||||
- Zwei-Spalten-Editor: Markdown links / WordPress-Vorschau rechts (große Designfläche 50vh/75vh)
|
||||
- **KI-Chat** unter Editor: Freie Texteingabe an die KI, Artikelkontext wird automatisch mitgegeben, max. 6 Nachrichtenpaare History, Button „In Editor übernehmen“ bei Änderungsvorschlägen
|
||||
- Zwei-Spalten-Editor: Markdown links / WordPress-Vorschau rechts
|
||||
- Featured Image aus og:image der Quelle automatisch
|
||||
- Kategorie + Tags aus WordPress live geladen
|
||||
- Publish / Entwurf / Einplanen (15-Minuten-Slots)
|
||||
|
|
@ -80,8 +79,7 @@ redax-db MySQL 8
|
|||
- Credentials (User/PW) direkt im Dashboard sichtbar
|
||||
- WP-Admin Direktzugang via socat-Proxy (bypass Cloudflare WAF)
|
||||
|
||||
### Redaktionsplan
|
||||
- **Layout:** Unten (volle Breite), nach Scrollen sichtbar — Studio nimmt oben gesamte Breite
|
||||
### Redaktionsplan (verbessert)
|
||||
- 7-Tage-Kalender mit KI + RSS gemeinsam
|
||||
- Badge: 🤖 KI / 📡 RSS
|
||||
- **Drag & Drop** zum Umplanen zwischen Tagen
|
||||
|
|
@ -90,10 +88,6 @@ redax-db MySQL 8
|
|||
- **Entwürfe ohne Datum** in separater Sektion sichtbar
|
||||
- WP-Editor-Link für Arakava News via socat-Proxy: `http://100.88.230.59:8101/wp-admin/`
|
||||
|
||||
### Entwurf-Speicher
|
||||
- Zwei Links nach Push: **Im WP-Editor bearbeiten** (WP_ADMIN_DIRECT_URL) + **Vorschau ansehen**
|
||||
- Publish-Ziele (WP-Targets) einklappbar, Standard eingeklappt
|
||||
|
||||
### RSS-Feeds
|
||||
- Beliebig viele Feeds konfigurierbar
|
||||
- Modi: Manuell / Auto-Publish (Teaser) / KI-Rewrite + Publish
|
||||
|
|
@ -140,7 +134,7 @@ Direkt-URL: `http://100.88.230.59:8101/wp-admin/`
|
|||
| Was | Pfad |
|
||||
|-----|------|
|
||||
| App | /opt/redax-wp/ |
|
||||
| Datenbank | /opt/redax-wp/data/db/redax.db |
|
||||
| Datenbank | /opt/redax-wp/data/redax.db |
|
||||
| WordPress-Dateien | /opt/redax-wp/data/wordpress/ |
|
||||
| MySQL-Daten | /opt/redax-wp/data/mysql/ |
|
||||
| Logs | /opt/redax-wp/logs/ |
|
||||
|
|
@ -190,14 +184,6 @@ TELEGRAM_CHANNEL_ID=...
|
|||
|
||||
## Changelog
|
||||
|
||||
### 02.03.2026 — KI-Chat + Layout
|
||||
- **KI-Chat** unter Editor: Freie Texteingabe, Artikelkontext, History (6 Paare), „In Editor übernehmen“
|
||||
- **API** `/api/chat` + `openrouter.generate_chat(messages)`
|
||||
- **Layout:** Redaktionsplan nach unten (volle Breite), Studio oben volle Breite
|
||||
- **Publish-Ziele** einklappbar
|
||||
- **Editor + Vorschau** größer (50vh / 75vh)
|
||||
- **Entwurf:** Zwei Links (WP-Editor bearbeiten + Vorschau)
|
||||
|
||||
### 28.02.2026 — ESP32-Serie Teil 2 + Animiertes Hydraulikschema
|
||||
- **ESP32-Serie Teil 2** als WP-Entwurf erstellt (Post 1340 auf Arakava News)
|
||||
- Titel: "70 Euro gegen Heizungschaos: Die Hardware für mein Smart-Home-Projekt"
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Prüft ob Redax-Entwürfe auf Arakava News 2 landen
|
||||
# Auf pve-hetzner: ssh root@100.88.230.59, dann:
|
||||
# pct exec 113 -- bash -c 'cd /opt/redax-wp && source .env 2>/dev/null; echo "WP_URL=$WP_URL"'
|
||||
|
||||
echo "=== 1. WP_URL in .env (CT 113) ==="
|
||||
sshpass -p 'Astral-Proxmox!2026' ssh -o StrictHostKeyChecking=no root@100.88.230.59 \
|
||||
"pct exec 113 -- bash -c 'grep -E \"^WP_URL=\" /opt/redax-wp/.env 2>/dev/null || echo \"Keine .env gefunden\"'"
|
||||
|
||||
echo ""
|
||||
echo "=== 2. Letzte Redax-Logs (draft_push) ==="
|
||||
sshpass -p 'Astral-Proxmox!2026' ssh -o StrictHostKeyChecking=no root@100.88.230.59 \
|
||||
"pct exec 113 -- tail -20 /opt/redax-wp/logs/redax.log 2>/dev/null | grep -E 'draft|push|article'" 2>/dev/null || echo "Keine passenden Log-Einträge"
|
||||
|
||||
echo ""
|
||||
echo "=== 3. Letzter Artikel in Redax-DB (wp_post_id) ==="
|
||||
sshpass -p 'Astral-Proxmox!2026' ssh -o StrictHostKeyChecking=no root@100.88.230.59 \
|
||||
"pct exec 113 -- sqlite3 /opt/redax-wp/data/db/redax.db 'SELECT id, title, wp_post_id, status FROM articles ORDER BY id DESC LIMIT 3'" 2>/dev/null || echo "DB-Zugriff fehlgeschlagen"
|
||||
|
|
@ -46,9 +46,6 @@ services:
|
|||
volumes:
|
||||
- ./data/db:/data
|
||||
- ./logs:/logs
|
||||
- ./src/app.py:/app/app.py
|
||||
- ./src/database.py:/app/database.py
|
||||
- ./src/templates:/app/templates
|
||||
ports:
|
||||
- "8080:8080"
|
||||
networks:
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"title": "Ohne Visum kein Kambodscha – warum dein erster Schritt nicht Hausbau oder Business, sondern Immigration sein sollte",
|
||||
"content": "In der Überlegung, nach Kambodscha auszuwandern, gibt es eine zentrale Fragestellung: Wie stellt man sicher, dass der Aufenthalt rechtlich abgesichert ist? Bevor du über den Bau eines Hauses oder die Gründung eines Unternehmens nachdenkst, ist es unerlässlich, die verschiedenen Möglichkeiten des Aufenthalts rechtlich zu klären. In diesem Artikel beleuchte ich die verschiedenen Visa-Optionen sowie den Ablauf der Beantragung und die Rolle von Agenturen in Phnom Penh.\n\n## Stabiles Aufenthaltsmodell\n\nKambodscha bietet eine Vielzahl von Aufenthaltsmodellen, die auf verschiedene Bedürfnisse abgestimmt sind. Die gängigsten Visa sind das Touristenvisum, das Ordinary Business Visa (EB-Visa) und die Jahresverlängerung. Je nach deinem Vorhaben in Kambodscha kann die Wahl des richtigen Visas entscheidend für die Planung deiner Zukunft hier sein.\n\n### Die Visa-Optionen im Detail\n\nTouristenvisum: Bis zu 30 Tage, 1 Monat einmal verlängerbar, touristische Aufenthalte.\nOrdinary Business Visa: 30 Tage bis 1 Jahr, Jahresverlängerung möglich, geschäftliche Aktivitäten.\nJahresverlängerung: 1 Jahr, kann jährlich erneuert werden, langfristiger Aufenthalt.\n\n### Agenturen im Riverside Phnom Penh\n\nIn Phnom Penh, besonders im Riverside-Gebiet, gibt es zahlreiche Visa-Agenturen, die den Prozess der Visa-Beantragung und -Verlängerung erleichtern. Diese Agenturen sind oft mit dem lokalen System vertraut und können helfen, die Formalitäten schnell und unkompliziert zu erledigen. Im Riverside-Bereich finden sich mehrere Anbieter.\n\n## Praxisbeispiel: Ablauf einer Jahresverlängerung\n\nEine Visa-Agentur im Riverside-Bereich organisierte innerhalb von etwa 10 Tagen eine saubere Jahresverlängerung im Reisepass. Der Ablauf gliedert sich in drei Schritte: Zunächst übergibst du deinen Reisepass bei der Agentur. Während der Bearbeitungszeit von rund 10 Tagen erledigt die Agentur die notwendigen Formalitäten. Anschließend erhältst du deinen Pass mit dem gültigen Eintrag zurück.\n\n## Die Rolle der Agenten und persönliche Netzwerke\n\nDie Agenten spielen eine entscheidende Rolle im Visasystem. Sie sind oft gut vernetzt und kennen die lokalen Behörden, was den Prozess wesentlich beschleunigen kann. Hierbei ist es hilfreich, ein persönliches Netzwerk aufzubauen. Wichtig: Die Vorschriften und Anforderungen für Visa in Kambodscha können sich häufig ändern. Daher ist es ratsam, sich regelmäßig über die aktuellen Bestimmungen zu informieren.\n\n## Strategische Überlegungen zum Aufenthaltsstatus\n\nDer Aufenthaltsstatus ist Grundlage für Mietverträge, Geschäftsaufbau und langfristige Planung. Ohne gültiges Visum ist es nahezu unmöglich, legal einen Mietvertrag abzuschließen oder ein Geschäft zu gründen. Ein rechtmäßiger Aufenthalt stärkt zudem die Verhandlungsposition gegenüber Vermietern und Geschäftspartnern.\n\n## Abschluss\n\nJeder, der über eine Auswanderung nach Kambodscha nachdenkt, sollte sich intensiv mit den Visa-Möglichkeiten auseinandersetzen. Visa-Regelungen können sich ändern. Informiere dich selbst über aktuelle Bestimmungen, um unangenehme Überraschungen zu vermeiden.",
|
||||
"seo_title": "Ohne Visum kein Kambodscha – Visa-Grundlagen für Auswanderer",
|
||||
"seo_description": "Warum Immigration vor Hausbau oder Business: Aufenthaltsmodelle, Visa-Optionen und die Rolle von Agenturen im Riverside Phnom Penh.",
|
||||
"focus_keyword": "Kambodscha Visum"
|
||||
}
|
||||
|
|
@ -272,9 +272,8 @@ def api_generate():
|
|||
data = request.json
|
||||
source = data.get('source', '')
|
||||
tone = data.get('tone', 'informativ')
|
||||
prompt_id = data.get('prompt_id')
|
||||
|
||||
prompt = db.get_prompt_by_id(prompt_id) if prompt_id else db.get_default_prompt()
|
||||
prompt = db.get_default_prompt()
|
||||
if not prompt:
|
||||
return jsonify({'error': 'Kein Prompt konfiguriert'}), 400
|
||||
|
||||
|
|
@ -303,127 +302,6 @@ def api_generate():
|
|||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/polish', methods=['POST'])
|
||||
def api_polish():
|
||||
"""KI-gestützte Textverbesserung: Nutzer gibt Anweisung, KI poliert Titel + Inhalt."""
|
||||
data = request.json
|
||||
title = data.get('title', '')
|
||||
content = data.get('content', '')
|
||||
instruction = data.get('instruction', '').strip()
|
||||
|
||||
if not instruction:
|
||||
return jsonify({'error': 'Bitte Anweisung eingeben (z.B. kürzer, lockerer, Einstieg packender)'}), 400
|
||||
if not content and not title:
|
||||
return jsonify({'error': 'Kein Inhalt zum Verbessern'}), 400
|
||||
|
||||
system = """Du bist ein Redakteur. Deine Aufgabe: Artikel bearbeiten nach Anweisung. Gib IMMER den bearbeiteten Text zurück – keine Ablehnung, kein "Ich kann nicht helfen".
|
||||
|
||||
Erledige die Anweisung (Stil ändern, kürzen, Bild einfügen, etc.) und gib NUR den Ergebnis-Text zurück.
|
||||
Format: Erste Zeile = Titel (ohne #). Dann HTML (<p>, <h2>, <h3>, <strong>, <img src="..." />).
|
||||
Am Ende: SEO_TITLE: [max 60]
|
||||
SEO_DESC: [max 155]
|
||||
KEYWORD: [1 Wort]"""
|
||||
|
||||
user_msg = f"Anweisung: {instruction}\n\n---\nTitel: {title}\n\nInhalt:\n{content}"
|
||||
|
||||
try:
|
||||
raw = openrouter.generate(system, user_msg)
|
||||
parsed_content, seo_title, seo_desc, keyword = rss_fetcher._parse_ki_output(raw)
|
||||
lines = parsed_content.strip().split('\n')
|
||||
new_title = lines[0].lstrip('#').strip() if lines else title
|
||||
new_content = '\n'.join(lines[1:]).strip() if len(lines) > 1 else parsed_content
|
||||
flog.info('article_polished', instruction=instruction[:50])
|
||||
return jsonify({
|
||||
'title': new_title,
|
||||
'content': new_content,
|
||||
'seo_title': seo_title or data.get('seo_title'),
|
||||
'seo_description': seo_desc or data.get('seo_description'),
|
||||
'focus_keyword': keyword or data.get('focus_keyword'),
|
||||
})
|
||||
except Exception as e:
|
||||
flog.error('polish_failed', error=str(e))
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/chat', methods=['POST'])
|
||||
def api_chat():
|
||||
"""Freier KI-Chat mit Artikelkontext. History max 6 Paare."""
|
||||
data = request.json
|
||||
message = (data.get('message') or '').strip()
|
||||
history = data.get('history') or []
|
||||
ctx = data.get('context') or {}
|
||||
|
||||
if not message:
|
||||
return jsonify({'error': 'Bitte Nachricht eingeben'}), 400
|
||||
|
||||
# History auf 6 Paare begrenzen
|
||||
history = history[-12:] # max 12 messages (6 user + 6 assistant)
|
||||
|
||||
title = ctx.get('title', '')
|
||||
content = ctx.get('content', '')
|
||||
seo_title = ctx.get('seo_title', '')
|
||||
seo_desc = ctx.get('seo_description', '')
|
||||
keyword = ctx.get('focus_keyword', '')
|
||||
|
||||
system = """Du bist ein Redakteurs-Assistent. Der User arbeitet an einem WordPress-Artikel.
|
||||
Kontext des aktuellen Artikels:
|
||||
- Titel: """ + (title or '(leer)') + """
|
||||
- Inhalt: """ + (content[:3000] + '...' if len(content or '') > 3000 else (content or '(leer)')) + """
|
||||
- SEO-Titel: """ + (seo_title or '(leer)') + """
|
||||
- SEO-Beschreibung: """ + (seo_desc or '(leer)') + """
|
||||
- Fokus-Keyword: """ + (keyword or '(leer)') + """
|
||||
|
||||
Antworte kurz und handlungsorientiert. Wenn der User Änderungen am Artikel wünscht (kürzen, umschreiben, Teaser, etc.):
|
||||
Gib deine Antwort, und am Ende in diesem Format den überarbeiteten Inhalt:
|
||||
===APPLY===
|
||||
TITEL: [neuer Titel falls geändert]
|
||||
INHALT: [HTML-Inhalt]
|
||||
SEO_TITLE: [optional, max 60 Zeichen]
|
||||
SEO_DESC: [optional, max 155 Zeichen]
|
||||
KEYWORD: [optional]
|
||||
===ENDAPPLY===
|
||||
Nur die Felder angeben die sich ändern. Ohne ===APPLY=== wenn du keinen Text zum Übernehmen lieferst."""
|
||||
|
||||
messages = [{"role": "system", "content": system}]
|
||||
for h in history:
|
||||
if h.get('role') in ('user', 'assistant') and h.get('content'):
|
||||
messages.append({"role": h["role"], "content": h["content"]})
|
||||
messages.append({"role": "user", "content": message})
|
||||
|
||||
try:
|
||||
raw = openrouter.generate_chat(messages)
|
||||
suggested = None
|
||||
if '===APPLY===' in raw and '===ENDAPPLY===' in raw:
|
||||
try:
|
||||
block = raw.split('===APPLY===')[1].split('===ENDAPPLY===')[0].strip()
|
||||
parts = {}
|
||||
for line in block.split('\n'):
|
||||
if ':' in line:
|
||||
k, v = line.split(':', 1)
|
||||
parts[k.strip().upper()] = v.strip()
|
||||
suggested = {
|
||||
'title': parts.get('TITEL') or title,
|
||||
'content': parts.get('INHALT') or content,
|
||||
'seo_title': parts.get('SEO_TITLE') or seo_title,
|
||||
'seo_description': parts.get('SEO_DESC') or seo_desc,
|
||||
'focus_keyword': parts.get('KEYWORD') or keyword,
|
||||
}
|
||||
reply = raw.split('===APPLY===')[0].strip()
|
||||
except Exception:
|
||||
reply = raw
|
||||
else:
|
||||
reply = raw
|
||||
|
||||
flog.info('chat_message', msg_len=len(message))
|
||||
out = {'reply': reply}
|
||||
if suggested:
|
||||
out['suggested_content'] = suggested
|
||||
return jsonify(out)
|
||||
except Exception as e:
|
||||
flog.error('chat_failed', error=str(e))
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/article/save', methods=['POST'])
|
||||
def api_save_article():
|
||||
data = request.json
|
||||
|
|
@ -460,24 +338,12 @@ def api_save_article():
|
|||
db.update_article(article_id, {'wp_post_id': wp_post_id})
|
||||
|
||||
wp_base = os.getenv('WP_URL', '').rstrip('/')
|
||||
admin_base = os.getenv('WP_ADMIN_DIRECT_URL', wp_base).rstrip('/')
|
||||
wp_preview_url = f"{wp_base}/?p={wp_post_id}&preview=true"
|
||||
wp_edit_url = f"{admin_base}/wp-admin/post.php?post={wp_post_id}&action=edit"
|
||||
flog.info('article_saved_as_draft', article_id=article_id, wp_post_id=wp_post_id)
|
||||
except Exception as e:
|
||||
flog.warn('draft_push_failed', article_id=article_id, error=str(e))
|
||||
if wp_post_id:
|
||||
wp_base = os.getenv('WP_URL', '').rstrip('/')
|
||||
admin_base = os.getenv('WP_ADMIN_DIRECT_URL', wp_base).rstrip('/')
|
||||
wp_edit_url = f"{admin_base}/wp-admin/post.php?post={wp_post_id}&action=edit"
|
||||
else:
|
||||
wp_edit_url = None
|
||||
|
||||
return jsonify({
|
||||
'success': True, 'id': article_id, 'wp_post_id': wp_post_id,
|
||||
'wp_preview_url': wp_preview_url,
|
||||
'wp_edit_url': wp_edit_url if wp_post_id else None
|
||||
})
|
||||
return jsonify({'success': True, 'id': article_id, 'wp_post_id': wp_post_id, 'wp_preview_url': wp_preview_url})
|
||||
|
||||
|
||||
@app.route('/api/article/schedule', methods=['POST'])
|
||||
|
|
@ -668,18 +534,14 @@ def api_save_prompt():
|
|||
"UPDATE prompts SET name=?, system_prompt=? WHERE id=?",
|
||||
(data['name'], data['system_prompt'], data['id'])
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'id': data['id']})
|
||||
else:
|
||||
cur = conn.execute(
|
||||
conn.execute(
|
||||
"INSERT INTO prompts (name, system_prompt) VALUES (?,?)",
|
||||
(data['name'], data['system_prompt'])
|
||||
)
|
||||
new_id = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'id': new_id})
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@app.route('/api/prompts/<int:pid>/default', methods=['POST'])
|
||||
|
|
|
|||
|
|
@ -403,13 +403,6 @@ def get_default_prompt():
|
|||
return dict(row) if row else None
|
||||
|
||||
|
||||
def get_prompt_by_id(pid: int):
|
||||
conn = get_conn()
|
||||
row = conn.execute("SELECT * FROM prompts WHERE id=?", (pid,)).fetchone()
|
||||
conn.close()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
# ── Settings ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def get_setting(key, default=None):
|
||||
|
|
|
|||
|
|
@ -88,32 +88,7 @@ def generate(system_prompt: str, user_message: str) -> str:
|
|||
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
timeout = _aiohttp.ClientTimeout(total=90)
|
||||
async with _aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.post(f"{OPENROUTER_BASE}/chat/completions", json=payload, headers=headers) as resp:
|
||||
data = await resp.json()
|
||||
if resp.status != 200:
|
||||
raise Exception(f"OpenRouter Fehler {resp.status}: {data}")
|
||||
return data["choices"][0]["message"]["content"].strip()
|
||||
return asyncio.run(_gen())
|
||||
|
||||
|
||||
def generate_chat(messages: list) -> str:
|
||||
"""Chat mit History. messages: [{"role":"system/user/assistant","content":"..."}, ...]"""
|
||||
import asyncio, aiohttp as _aiohttp
|
||||
async def _gen():
|
||||
payload = {
|
||||
"model": DEFAULT_MODEL,
|
||||
"messages": messages,
|
||||
"max_tokens": 2500,
|
||||
"temperature": 0.7
|
||||
}
|
||||
headers = {
|
||||
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
timeout = _aiohttp.ClientTimeout(total=90)
|
||||
async with _aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with _aiohttp.ClientSession() as session:
|
||||
async with session.post(f"{OPENROUTER_BASE}/chat/completions", json=payload, headers=headers) as resp:
|
||||
data = await resp.json()
|
||||
if resp.status != 200:
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Redax-WP — Studio{% endblock %}
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
#wp-preview, #wp-preview p, #wp-preview h1, #wp-preview h2, #wp-preview h3, #wp-preview li, #wp-preview span { color: #0f172a !important; }
|
||||
#wp-preview a { color: #2563eb !important; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-6 py-6" data-wp-admin="{{ wp_admin_direct }}" data-wp-url="{{ wp_url }}">
|
||||
<div class="max-w-7xl mx-auto px-6 py-6">
|
||||
|
||||
<!-- Status-Bar -->
|
||||
<div class="flex items-center gap-4 mb-4 text-xs text-slate-500 flex-wrap">
|
||||
|
|
@ -19,13 +14,10 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- WordPress-Targets (einklappbar) -->
|
||||
<div class="mb-3">
|
||||
<button type="button" onclick="var b=document.getElementById('wp-targets-box');b.classList.toggle('hidden');document.getElementById('wp-targets-chevron').textContent=b.classList.contains('hidden')?'▸':'▾'"
|
||||
class="text-xs text-slate-500 hover:text-slate-300 flex items-center gap-1.5 py-1">
|
||||
<span id="wp-targets-chevron">▸</span> 📡 Publish-Ziele
|
||||
</button>
|
||||
<div id="wp-targets-box" class="hidden mt-2" style="display:flex;flex-wrap:wrap;gap:0.75rem">
|
||||
<!-- WordPress-Targets -->
|
||||
<div class="mb-6">
|
||||
<div class="text-xs text-slate-500 mb-2">📡 Publish-Ziele</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{% for t in wp_targets %}
|
||||
<div style="background:#1e293b;border:1px solid #334155;border-radius:8px;padding:8px 12px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
|
||||
|
|
@ -71,20 +63,20 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
|
||||
<!-- ═══ STUDIO (oben, volle Breite) ═══ -->
|
||||
<div class="space-y-4">
|
||||
<!-- ═══ STUDIO (links, 2/3) ═══ -->
|
||||
<div class="xl:col-span-2 space-y-4">
|
||||
|
||||
<!-- Artikel-Generator -->
|
||||
<div class="card p-5">
|
||||
<h2 class="text-base font-semibold text-white mb-4">✍️ Artikel-Studio</h2>
|
||||
|
||||
<!-- Quelle + Ton + Prompt -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4">
|
||||
<!-- Quelle + Ton -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-4">
|
||||
<div class="md:col-span-2">
|
||||
<label class="text-xs text-slate-400 block mb-1">Quelle (URL oder Text)</label>
|
||||
<input type="text" id="source-input" placeholder="https://... oder Text einfügen" class="w-full">
|
||||
<input type="url" id="source-input" placeholder="https://..." class="w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 block mb-1">Ton</label>
|
||||
|
|
@ -94,14 +86,6 @@
|
|||
<option value="reportage">Reportage</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 block mb-1">Prompt</label>
|
||||
<select id="prompt-select" class="w-full">
|
||||
{% for p in prompts %}
|
||||
<option value="{{ p.id }}" {% if p.is_default %}selected{% endif %}>{{ p.name }}{% if p.is_default %} ✓{% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Titel -->
|
||||
|
|
@ -110,53 +94,25 @@
|
|||
<input type="text" id="article-title" placeholder="Artikel-Titel" class="w-full">
|
||||
</div>
|
||||
|
||||
<!-- Zwei-Spalten Editor + Vorschau (große Designfläche) -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4" style="min-height: 65vh">
|
||||
<div class="flex flex-col min-h-0">
|
||||
<!-- Zwei-Spalten Editor -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 block mb-1">Inhalt (HTML)</label>
|
||||
<textarea id="article-content" placeholder="Artikel-Inhalt..."
|
||||
oninput="updatePreview()" class="flex-1 min-h-[320px]"
|
||||
style="min-height: 50vh; resize: vertical"></textarea>
|
||||
<div class="mt-2 flex gap-2 items-center flex-wrap">
|
||||
<input type="text" id="inline-image-url" placeholder="Bild-URL für Inhalt..." class="flex-1 min-w-[120px] text-xs">
|
||||
<button type="button" onclick="insertImageAtCursor()" class="text-xs px-3 py-1.5 rounded bg-slate-700 hover:bg-slate-600 text-slate-200">🖼️ Hier einfügen</button>
|
||||
</div>
|
||||
<textarea id="article-content" rows="14" placeholder="Artikel-Inhalt..."
|
||||
oninput="updatePreview()"></textarea>
|
||||
</div>
|
||||
<div class="flex flex-col min-h-0">
|
||||
<label class="text-xs text-slate-400 block mb-1">Vorschau</label>
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 block mb-1">WordPress-Vorschau</label>
|
||||
<div id="wp-preview"
|
||||
class="rounded-lg p-5 overflow-y-auto border border-slate-600 flex-1"
|
||||
style="min-height: 50vh; max-height: 75vh; font-family: Georgia, serif; line-height: 1.7; font-size: 1rem; background: #f8fafc; color: #0f172a;">
|
||||
<span class="text-slate-500 italic">Vorschau erscheint beim Tippen...</span>
|
||||
class="bg-white text-slate-900 rounded-lg p-4 text-sm overflow-y-auto"
|
||||
style="min-height: 14rem; max-height: 24rem; font-family: Georgia, serif; line-height: 1.8;">
|
||||
<span class="text-slate-400 italic text-xs">Vorschau erscheint beim Tippen...</span>
|
||||
</div>
|
||||
<div id="wp-draft-link" class="hidden"></div>
|
||||
<div id="mirror-status-box" class="hidden mt-2 text-xs space-y-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KI-Chat (freie Eingabe) -->
|
||||
<div class="card p-4 mb-4">
|
||||
<div class="text-sm font-semibold text-white mb-2 flex items-center gap-2">
|
||||
<span>💬 KI-Chat</span>
|
||||
<button type="button" id="chat-toggle" onclick="var b=document.getElementById('chat-box');b.classList.toggle('hidden');this.textContent=b.classList.contains('hidden')?'▸ aufklappen':'▾ einklappen'"
|
||||
class="text-xs text-slate-500 hover:text-slate-300">▾ einklappen</button>
|
||||
</div>
|
||||
<div id="chat-box">
|
||||
<div id="chat-messages" class="space-y-3 mb-3 max-h-[220px] overflow-y-auto rounded-lg border border-slate-600 p-3 bg-slate-900/50 text-sm"
|
||||
style="min-height: 80px">
|
||||
<span class="text-slate-500 italic text-xs">Schreibe eine Nachricht – die KI kennt deinen Artikel.</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="chat-input" placeholder="z.B. Kürze den ersten Absatz, schreib einen Teaser..."
|
||||
class="flex-1 text-sm" onkeydown="if(event.key==='Enter')sendChat()">
|
||||
<button type="button" id="btn-chat" onclick="sendChat()" class="btn btn-primary">Senden</button>
|
||||
</div>
|
||||
<div id="chat-apply-row" class="hidden mt-2">
|
||||
<button type="button" onclick="applyChatSuggestion()" class="btn btn-success text-xs">✓ In Editor übernehmen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEO-Panel -->
|
||||
<div class="bg-slate-900/50 border border-slate-700 rounded-lg p-3 mb-4">
|
||||
<div class="text-xs text-slate-400 font-semibold mb-2">🔍 SEO</div>
|
||||
|
|
@ -194,18 +150,13 @@
|
|||
</div>
|
||||
|
||||
<!-- Aktions-Buttons -->
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button id="btn-generate" onclick="generateArticle()" class="btn btn-primary">🤖 KI generieren</button>
|
||||
<button onclick="saveDraft()" class="btn btn-ghost">💾 Entwurf</button>
|
||||
<button onclick="publishNow()" class="btn btn-success">🚀 Sofort veröffentlichen</button>
|
||||
<button onclick="toggleSchedulePanel()" class="btn" style="background:#4c1d95;color:#fff">📅 Einplanen</button>
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap items-center p-3 rounded-lg border border-slate-600 bg-slate-800/60">
|
||||
<span class="text-sm font-medium text-slate-300">✨ KI verbessern:</span>
|
||||
<input type="text" id="polish-instruction" placeholder="z.B. kürzer, lockerer — oder: Bild https://... nach Absatz 2 einfügen" class="flex-1 min-w-[200px] text-sm">
|
||||
<button id="btn-polish" type="button" onclick="polishArticle()" class="btn btn-primary">Verbessern</button>
|
||||
</div>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<button onclick="generateArticle()" class="btn btn-primary">
|
||||
<span id="gen-spinner" class="spinner hidden">⟳</span> 🤖 KI generieren
|
||||
</button>
|
||||
<button onclick="saveDraft()" class="btn btn-ghost">💾 Entwurf</button>
|
||||
<button onclick="publishNow()" class="btn btn-success">🚀 Sofort veröffentlichen</button>
|
||||
<button onclick="toggleSchedulePanel()" class="btn" style="background:#4c1d95;color:#fff">📅 Einplanen</button>
|
||||
</div>
|
||||
|
||||
<!-- Einplan-Panel -->
|
||||
|
|
@ -230,7 +181,7 @@
|
|||
|
||||
</div>
|
||||
|
||||
<!-- ═══ REDAKTIONSPLAN (unten, volle Breite, scrollen) ═══ -->
|
||||
<!-- ═══ REDAKTIONSPLAN (rechts, 1/3) ═══ -->
|
||||
<div class="space-y-4">
|
||||
|
||||
{% set draft_arts = [] %}
|
||||
|
|
@ -390,183 +341,32 @@ function updatePreview() {
|
|||
preview.innerHTML = (title ? `<h1 style="font-size:1.4rem;font-weight:700;margin-bottom:1rem">${title}</h1>` : '') + content;
|
||||
}
|
||||
|
||||
function insertImageAtCursor() {
|
||||
const url = document.getElementById('inline-image-url').value.trim();
|
||||
const ta = document.getElementById('article-content');
|
||||
if (!url) { showToast('⚠️ Bild-URL eingeben'); return; }
|
||||
const img = `<p><img src="${url.replace(/"/g, '"')}" alt="Bild" style="max-width:100%;height:auto;"></p>`;
|
||||
const start = ta.selectionStart, end = ta.selectionEnd;
|
||||
const before = ta.value.substring(0, start), after = ta.value.substring(end);
|
||||
ta.value = before + img + after;
|
||||
ta.selectionStart = ta.selectionEnd = start + img.length;
|
||||
ta.focus();
|
||||
updatePreview();
|
||||
showToast('🖼️ Bild eingefügt');
|
||||
}
|
||||
|
||||
function setButtonLoading(btnId, loading) {
|
||||
const btn = document.getElementById(btnId);
|
||||
if (!btn) return;
|
||||
const labels = { 'btn-generate': '🤖 KI generieren', 'btn-polish': 'Verbessern', 'btn-chat': 'Senden' };
|
||||
if (loading) {
|
||||
btn.dataset.origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Bitte warten...';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = btn.dataset.origText || labels[btnId] || 'OK';
|
||||
}
|
||||
}
|
||||
|
||||
let chatHistory = [];
|
||||
let lastSuggested = null;
|
||||
|
||||
function renderChatMessages() {
|
||||
const el = document.getElementById('chat-messages');
|
||||
if (!el) return;
|
||||
if (chatHistory.length === 0) {
|
||||
el.innerHTML = '<span class="text-slate-500 italic text-xs">Schreibe eine Nachricht – die KI kennt deinen Artikel.</span>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = chatHistory.map(m => {
|
||||
const isUser = m.role === 'user';
|
||||
return `<div class="flex ${isUser ? 'justify-end' : 'justify-start'}">
|
||||
<div class="max-w-[85%] rounded-lg px-3 py-2 text-xs ${isUser ? 'bg-blue-900/50 border border-blue-700' : 'bg-slate-800 border border-slate-600'}">
|
||||
${(m.content || '').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>')}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
async function sendChat() {
|
||||
const input = document.getElementById('chat-input');
|
||||
const msg = (input?.value || '').trim();
|
||||
if (!msg) return;
|
||||
|
||||
const ctx = getArticleData();
|
||||
const history = chatHistory.slice(-12).map(m => ({ role: m.role, content: m.content }));
|
||||
chatHistory.push({ role: 'user', content: msg });
|
||||
input.value = '';
|
||||
renderChatMessages();
|
||||
document.getElementById('chat-apply-row').classList.add('hidden');
|
||||
lastSuggested = null;
|
||||
|
||||
setButtonLoading('btn-chat', true);
|
||||
try {
|
||||
const r = await fetch('/api/chat', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({
|
||||
message: msg,
|
||||
history: history,
|
||||
context: { title: ctx.title, content: ctx.content, seo_title: ctx.seo_title, seo_description: ctx.seo_description, focus_keyword: ctx.focus_keyword }
|
||||
})
|
||||
});
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (d.error) { showToast('❌ ' + d.error); chatHistory.pop(); renderChatMessages(); return; }
|
||||
chatHistory.push({ role: 'assistant', content: d.reply });
|
||||
if (chatHistory.length > 12) chatHistory = chatHistory.slice(-12);
|
||||
if (d.suggested_content) { lastSuggested = d.suggested_content; document.getElementById('chat-apply-row').classList.remove('hidden'); }
|
||||
renderChatMessages();
|
||||
} catch (e) {
|
||||
showToast('❌ ' + (e.message || 'Fehler')); chatHistory.pop(); renderChatMessages();
|
||||
} finally {
|
||||
setButtonLoading('btn-chat', false);
|
||||
}
|
||||
}
|
||||
|
||||
function applyChatSuggestion() {
|
||||
if (!lastSuggested) return;
|
||||
if (lastSuggested.title) document.getElementById('article-title').value = lastSuggested.title;
|
||||
if (lastSuggested.content) document.getElementById('article-content').value = lastSuggested.content;
|
||||
if (lastSuggested.seo_title) document.getElementById('seo-title').value = lastSuggested.seo_title;
|
||||
if (lastSuggested.seo_description) document.getElementById('seo-description').value = lastSuggested.seo_description;
|
||||
if (lastSuggested.focus_keyword) document.getElementById('focus-keyword').value = lastSuggested.focus_keyword;
|
||||
const c1 = document.getElementById('seo-title-count');
|
||||
if (c1) c1.textContent = (lastSuggested.seo_title || '').length + '/60';
|
||||
const c2 = document.getElementById('seo-desc-count');
|
||||
if (c2) c2.textContent = (lastSuggested.seo_description || '').length + '/155';
|
||||
updatePreview();
|
||||
document.getElementById('chat-apply-row').classList.add('hidden');
|
||||
lastSuggested = null;
|
||||
showToast('✓ Übernommen');
|
||||
}
|
||||
|
||||
async function polishArticle() {
|
||||
const instruction = document.getElementById('polish-instruction').value.trim();
|
||||
const title = document.getElementById('article-title').value;
|
||||
const content = document.getElementById('article-content').value;
|
||||
if (!instruction) { showToast('⚠️ Anweisung eingeben'); return; }
|
||||
if (!content && !title) { showToast('⚠️ Kein Inhalt zum Verbessern'); return; }
|
||||
|
||||
setButtonLoading('btn-polish', true);
|
||||
const backup = setTimeout(() => setButtonLoading('btn-polish', false), 130000);
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const t = setTimeout(() => ctrl.abort(), 120000);
|
||||
const r = await fetch('/api/polish', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ title, content, instruction, seo_title: document.getElementById('seo-title').value, seo_description: document.getElementById('seo-description').value, focus_keyword: document.getElementById('focus-keyword').value }),
|
||||
signal: ctrl.signal
|
||||
});
|
||||
clearTimeout(t);
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (d.error) { showToast('❌ ' + d.error); return; }
|
||||
document.getElementById('article-title').value = d.title || '';
|
||||
document.getElementById('article-content').value = d.content || '';
|
||||
if (d.seo_title) document.getElementById('seo-title').value = d.seo_title;
|
||||
if (d.seo_description) document.getElementById('seo-description').value = d.seo_description;
|
||||
if (d.focus_keyword) document.getElementById('focus-keyword').value = d.focus_keyword;
|
||||
const c1 = document.getElementById('seo-title-count');
|
||||
if (c1) c1.textContent = (d.seo_title || '').length + '/60';
|
||||
const c2 = document.getElementById('seo-desc-count');
|
||||
if (c2) c2.textContent = (d.seo_description || '').length + '/155';
|
||||
updatePreview();
|
||||
document.getElementById('polish-instruction').value = '';
|
||||
showToast('✨ Text verbessert');
|
||||
} catch (e) {
|
||||
showToast('❌ ' + (e.name === 'AbortError' ? 'Timeout' : (e.message || 'Fehler')));
|
||||
} finally {
|
||||
clearTimeout(backup);
|
||||
setButtonLoading('btn-polish', false);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateArticle() {
|
||||
const source = document.getElementById('source-input').value.trim();
|
||||
const tone = document.getElementById('tone-select').value;
|
||||
const promptId = document.getElementById('prompt-select')?.value || null;
|
||||
if (!source) { showToast('⚠️ Bitte Quelle eingeben'); return; }
|
||||
|
||||
setButtonLoading('btn-generate', true);
|
||||
const backup = setTimeout(() => setButtonLoading('btn-generate', false), 130000);
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const t = setTimeout(() => ctrl.abort(), 120000);
|
||||
const r = await fetch('/api/generate', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({source, tone, prompt_id: promptId ? parseInt(promptId, 10) : null}),
|
||||
signal: ctrl.signal
|
||||
});
|
||||
clearTimeout(t);
|
||||
const d = await r.json().catch(() => ({}));
|
||||
if (d.error) { showToast('❌ ' + d.error); return; }
|
||||
document.getElementById('gen-spinner').classList.remove('hidden');
|
||||
const r = await fetch('/api/generate', {
|
||||
method: 'POST', headers: {'Content-Type':'application/json'},
|
||||
body: JSON.stringify({source, tone})
|
||||
});
|
||||
document.getElementById('gen-spinner').classList.add('hidden');
|
||||
const d = await r.json();
|
||||
if (d.error) { showToast('❌ ' + d.error); return; }
|
||||
|
||||
document.getElementById('article-title').value = d.title || '';
|
||||
document.getElementById('article-content').value = d.content || '';
|
||||
document.getElementById('seo-title').value = d.seo_title || '';
|
||||
document.getElementById('seo-description').value = d.seo_description || '';
|
||||
document.getElementById('focus-keyword').value = d.focus_keyword || '';
|
||||
updatePreview();
|
||||
document.getElementById('article-title').value = d.title || '';
|
||||
document.getElementById('article-content').value = d.content || '';
|
||||
document.getElementById('seo-title').value = d.seo_title || '';
|
||||
document.getElementById('seo-description').value = d.seo_description || '';
|
||||
document.getElementById('focus-keyword').value = d.focus_keyword || '';
|
||||
updatePreview();
|
||||
|
||||
if (source.startsWith('http')) fetchOgImage(source);
|
||||
showToast('✅ Artikel generiert');
|
||||
} catch (e) {
|
||||
showToast('❌ ' + (e.name === 'AbortError' ? 'Timeout (>2 Min)' : (e.message || 'Fehler')));
|
||||
} finally {
|
||||
clearTimeout(backup);
|
||||
setButtonLoading('btn-generate', false);
|
||||
// og:image automatisch holen
|
||||
if (source.startsWith('http')) {
|
||||
fetchOgImage(source);
|
||||
}
|
||||
showToast('✅ Artikel generiert');
|
||||
}
|
||||
|
||||
async function fetchOgImage(url) {
|
||||
|
|
@ -606,19 +406,13 @@ async function saveDraft() {
|
|||
currentArticleId = d.id;
|
||||
if (d.wp_post_id) currentWpPostId = d.wp_post_id;
|
||||
|
||||
// Vorschau- und Editor-Links anzeigen
|
||||
// Vorschau-Link anzeigen
|
||||
const linkBox = document.getElementById('wp-draft-link');
|
||||
if ((d.wp_preview_url || d.wp_edit_url) && linkBox) {
|
||||
const links = [];
|
||||
if (d.wp_edit_url) links.push(`<a href="${d.wp_edit_url}" target="_blank"
|
||||
style="display:inline-flex;align-items:center;gap:6px;background:#e0f2fe;border:1px solid #7dd3fc;
|
||||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none;margin-right:8px">
|
||||
✎ Im WP-Editor bearbeiten →</a>`);
|
||||
if (d.wp_preview_url) links.push(`<a href="${d.wp_preview_url}" target="_blank"
|
||||
if (d.wp_preview_url && linkBox) {
|
||||
linkBox.innerHTML = `<a href="${d.wp_preview_url}" target="_blank"
|
||||
style="display:inline-flex;align-items:center;gap:6px;background:#f0f9ff;border:1px solid #bae6fd;
|
||||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none">
|
||||
👁 Vorschau ansehen</a>`);
|
||||
linkBox.innerHTML = links.join('');
|
||||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none;margin-top:8px">
|
||||
👁 Entwurf in WordPress ansehen →</a>`;
|
||||
linkBox.classList.remove('hidden');
|
||||
}
|
||||
showToast('💾 Entwurf gespeichert & nach WordPress gepusht');
|
||||
|
|
@ -688,7 +482,6 @@ async function confirmSchedule() {
|
|||
}
|
||||
|
||||
async function loadArticle(id) {
|
||||
if (currentArticleId !== id) { chatHistory = []; lastSuggested = null; if (document.getElementById('chat-apply-row')) document.getElementById('chat-apply-row').classList.add('hidden'); renderChatMessages(); }
|
||||
const r = await fetch(`/api/article/${id}`);
|
||||
const d = await r.json();
|
||||
currentArticleId = id;
|
||||
|
|
@ -702,22 +495,14 @@ async function loadArticle(id) {
|
|||
document.getElementById('featured-image').value = d.featured_image_url || '';
|
||||
if (d.category_id) document.getElementById('category-select').value = d.category_id;
|
||||
|
||||
// WP-Links wiederherstellen wenn vorhanden
|
||||
// WP-Vorschau-Link wiederherstellen wenn vorhanden
|
||||
const linkBox = document.getElementById('wp-draft-link');
|
||||
if (d.wp_post_id && linkBox) {
|
||||
const wrap = document.querySelector('[data-wp-admin]');
|
||||
const wpAdmin = wrap ? wrap.dataset.wpAdmin : '';
|
||||
const wpBase = wrap ? wrap.dataset.wpUrl : '';
|
||||
const links = [];
|
||||
if (wpAdmin) links.push(`<a href="${wpAdmin}/wp-admin/post.php?post=${d.wp_post_id}&action=edit" target="_blank"
|
||||
style="display:inline-flex;align-items:center;gap:6px;background:#e0f2fe;border:1px solid #7dd3fc;
|
||||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none;margin-right:8px">
|
||||
✎ Im WP-Editor bearbeiten →</a>`);
|
||||
if (wpBase) links.push(`<a href="${wpBase}/?p=${d.wp_post_id}&preview=true" target="_blank"
|
||||
const wpBase = '{{ wp_url }}';
|
||||
linkBox.innerHTML = `<a href="${wpBase}/?p=${d.wp_post_id}&preview=true" target="_blank"
|
||||
style="display:inline-flex;align-items:center;gap:6px;background:#f0f9ff;border:1px solid #bae6fd;
|
||||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none">
|
||||
👁 Vorschau ansehen</a>`);
|
||||
linkBox.innerHTML = links.join('');
|
||||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none;margin-top:8px">
|
||||
👁 Entwurf in WordPress ansehen →</a>`;
|
||||
linkBox.classList.remove('hidden');
|
||||
} else if (linkBox) {
|
||||
linkBox.classList.add('hidden');
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
<h1 class="text-xl font-bold text-white">🧠 Prompt-Bibliothek</h1>
|
||||
<button onclick="newPrompt()" class="btn btn-primary text-sm">+ Neuer Prompt</button>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mb-4">Klicke auf einen Prompt, um ihn zu bearbeiten oder als Standard zu setzen. Neuer Prompt: Speichern setzt ihn automatisch als Standard.</p>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="space-y-3">
|
||||
{% for p in prompts %}
|
||||
|
|
@ -59,25 +58,15 @@ function newPrompt() {
|
|||
document.getElementById('prompt-text').value = '';
|
||||
}
|
||||
async function savePrompt() {
|
||||
const currentId = document.getElementById('prompt-id').value || null;
|
||||
const r = await fetch('/api/prompts/save', {method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({id: currentId,
|
||||
body: JSON.stringify({id: document.getElementById('prompt-id').value || null,
|
||||
name: document.getElementById('prompt-name').value,
|
||||
system_prompt: document.getElementById('prompt-text').value})});
|
||||
const d = await r.json();
|
||||
if (d.success) {
|
||||
showToast('💾 Gespeichert');
|
||||
// Neuer Prompt: automatisch als Standard setzen
|
||||
if (!currentId && d.id) {
|
||||
await fetch(`/api/prompts/${d.id}/default`, {method:'POST'});
|
||||
showToast('💾 Gespeichert & ⭐ als Standard gesetzt');
|
||||
}
|
||||
location.reload();
|
||||
}
|
||||
if ((await r.json()).success) { showToast('💾 Gespeichert'); location.reload(); }
|
||||
}
|
||||
async function setDefault() {
|
||||
const id = document.getElementById('prompt-id').value;
|
||||
if (!id) { showToast('⚠️ Prompt zuerst auswählen (klicken) und ggf. speichern'); return; }
|
||||
if (!id) { showToast('⚠️ Prompt zuerst speichern'); return; }
|
||||
await fetch(`/api/prompts/${id}/default`, {method:'POST'});
|
||||
showToast('⭐ Als Standard gesetzt'); location.reload();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
# Brief für Blogartikel – Redax-Quelle
|
||||
|
||||
Titel: Ohne Visum kein Kambodscha – warum dein erster Schritt nicht Hausbau oder Business, sondern Immigration sein sollte
|
||||
|
||||
Zielgruppe: Auswanderungsinteressierte, die länger als touristisch in Kambodscha bleiben wollen.
|
||||
|
||||
Ton: nüchtern, erfahren, realistisch. Keine Romantisierung. Kein Werbestil. Keine Rechtsberatung. Keine Preisgarantien. Keine konkreten Agentennamen.
|
||||
|
||||
---
|
||||
|
||||
Inhaltliche Anforderungen:
|
||||
|
||||
Einstieg: Wer langfristig bleiben will, braucht ein stabiles Aufenthaltsmodell. Viele Visa-Agenturen sind im Riverside-Bereich von Phnom Penh tätig.
|
||||
|
||||
Unterschied erklären: Tourist Visa, Ordinary (Business) Visa, Jahres-Extension.
|
||||
|
||||
Praxisbeispiel: Eine Visa-Agentur im Riverside-Bereich organisierte innerhalb von ca. 10 Tagen eine saubere Jahresverlängerung im Reisepass. Ablauf: Passabgabe, Bearbeitungszeit, Rückgabe mit gültigem Eintrag.
|
||||
|
||||
System: Rolle von Agenten in Kambodscha, Bedeutung persönlicher Netzwerke, warum sich Regeln ändern können.
|
||||
|
||||
Strategisch: Warum Aufenthaltsstatus Grundlage ist für Mietverträge, Geschäftsaufbau, langfristige Planung, Verhandlungsposition.
|
||||
|
||||
Abschluss: Visa-Regelungen können sich ändern, jeder sollte selbst aktuelle Informationen prüfen.
|
||||
|
||||
---
|
||||
|
||||
Länge: 900 bis 1.200 Wörter. Zwischenüberschriften verwenden. Keine Bulletpoints im Fließtext. Sachliche Sprache. Keine dramatisierenden Formulierungen.
|
||||
Loading…
Add table
Reference in a new issue