docs: Update STATE.md mit neuesten Features
Made-with: Cursor
This commit is contained in:
parent
3ebadf08f3
commit
82e0850df2
15 changed files with 721 additions and 99 deletions
|
|
@ -14,8 +14,15 @@
|
|||
| 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,20 +8,32 @@
|
|||
|---|---|---|---|
|
||||
| **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 Muldenstein** (ioBroker, Grafana) | — (in diesem Repo) | smart-home/STATE.md | smart-home/scripts/ |
|
||||
| **Smart Home** (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 | — |
|
||||
|
||||
## Server
|
||||
## Prioritäten
|
||||
|
||||
| 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) |
|
||||
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)
|
||||
|
||||
## Wichtigste Zugangsdaten
|
||||
|
||||
|
|
@ -29,21 +41,28 @@
|
|||
|---|---|
|
||||
| pve-hetzner SSH | root / Astral-Proxmox!2026 |
|
||||
| pve1 SSH | root / astral66 |
|
||||
| helmut-pve SSH | root / astral66 |
|
||||
| Alle lokalen CTs | root / astral66 |
|
||||
| WordPress Admin | admin / astral66 |
|
||||
| WordPress Admin | admin / eJIyhW0p5PFacjvvKGufKeXS |
|
||||
| Seafile | admin@orbitalo.net / astral66 |
|
||||
| n8n | wuttig@gmx.de / Astral66 |
|
||||
| Dify | admin@orbitalo.net / astral66 |
|
||||
| Forgejo | orbitalo / astral66 |
|
||||
| Grafana | admin / astral66 |
|
||||
|
||||
## Telegram Bots
|
||||
|
||||
| Bot | Token | Chat-ID | Projekt |
|
||||
|---|---|---|---|
|
||||
| @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) |
|
||||
| @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
|
||||
|
||||
## Auto-Sync
|
||||
Die STATE.md Dateien werden täglich um 03:00 Uhr automatisch aktualisiert.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
# Infrastruktur — Live State
|
||||
> Auto-generiert: 2026-03-03 10:15
|
||||
> 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)
|
||||
|
||||
---
|
||||
|
||||
## pve-hetzner Disk
|
||||
| Mount | Belegt |
|
||||
|
|
@ -8,17 +23,109 @@
|
|||
| /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) |
|
||||
| 101 | wordpress-v2 | 100.91.212.19 | WordPress + MySQL (Docker) + **CF Tunnel** |
|
||||
| 103 | seafile | 100.75.247.60 | Seafile (seafile.orbitalo.net) |
|
||||
| 109 | rss-manager | 100.113.244.101 | RSS Manager + Matomo |
|
||||
| 109 | rss-manager | 100.113.244.101 | RSS Manager + Matomo + **CF Tunnel** |
|
||||
| 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 |
|
||||
|
|
@ -26,27 +133,62 @@
|
|||
| 104 | n8n | Nicht aktiv genutzt |
|
||||
| 105 | debian-12 | Nicht genutzt |
|
||||
| 106 | wordpress-news | Abgelöst durch CT 101 |
|
||||
| 113 | matomo | Integriert in CT 109 |
|
||||
|
||||
## Container auf pve1 (Kambodscha)
|
||||
| CT | Name | Dienste |
|
||||
---
|
||||
|
||||
## Cloudflare Tunnels & Routing
|
||||
|
||||
### pve-hetzner
|
||||
| CT | Tunnel-ID | Public Hostnames |
|
||||
|---|---|---|
|
||||
| 136 | gold-silber-v3 | Edelmetall-Bot (Tailscale: 100.72.230.87) |
|
||||
| 143 | smart-home | ioBroker + Grafana + InfluxDB |
|
||||
| 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 |
|
||||
|
||||
## 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
|
||||
### 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)
|
||||
|
||||
## Zugangsdaten
|
||||
- pve-hetzner: root / Astral-Proxmox!2026
|
||||
- pve1: root / astral66
|
||||
- Alle CTs: root / astral66
|
||||
- Seafile: admin@orbitalo.net / astral66
|
||||
- Forgejo: orbitalo / astral66
|
||||
|
||||
| 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 |
|
||||
|
||||
## Telegram Bots
|
||||
| Bot | Token (Auszug) | Chat-ID |
|
||||
|
||||
| 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 |
|
||||
|---|---|---|
|
||||
| Mutter (@MutterbotAI_bot) | 8551565940:... | 674951792 |
|
||||
| OpenRouter (GPT-4o-mini) | ~$0,35/Tag | RSS-Manager + Flugscanner Vision |
|
||||
| Cursor Ultra | $200/Monat | Entwicklung |
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# STATE: Redax-WP
|
||||
**Stand: 28.02.2026**
|
||||
**Stand: 03.03.2026**
|
||||
|
||||
---
|
||||
|
||||
## Status
|
||||
|
||||
✅ **Vollständig in Betrieb — 28.02.2026**
|
||||
✅ **Vollständig in Betrieb — 03.03.2026**
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -87,18 +87,40 @@ Docker Container:
|
|||
| Feature | Beschreibung |
|
||||
|---------|-------------|
|
||||
| Artikel-Studio | KI-Generierung via OpenRouter (Ton wählbar) |
|
||||
| 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 (✅/❌) |
|
||||
| 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 (✅/❌) |
|
||||
| RSS-Queue | Feed-Artikel verwalten, KI-Rewrite, Auto-Publish |
|
||||
| Duplikat-Schutz | Mirror überspringt Artikel die bereits vorhanden sind |
|
||||
| Duplikat-Schutz | Mirror überspringt existierende Artikel |
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
|
|
|||
126
redax-wp/PROMPT_EXPERIMENTE.md
Normal file
126
redax-wp/PROMPT_EXPERIMENTE.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# 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)
|
||||
61
redax-wp/PROMPT_KAMBODSCHA_TEST.md
Normal file
61
redax-wp/PROMPT_KAMBODSCHA_TEST.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# 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
|
||||
18
redax-wp/check-wp-push.sh
Executable file
18
redax-wp/check-wp-push.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
|||
#!/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,6 +46,9 @@ 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:
|
||||
|
|
|
|||
7
redax-wp/redax-output-kambodscha.json
Normal file
7
redax-wp/redax-output-kambodscha.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"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,8 +272,9 @@ def api_generate():
|
|||
data = request.json
|
||||
source = data.get('source', '')
|
||||
tone = data.get('tone', 'informativ')
|
||||
prompt_id = data.get('prompt_id')
|
||||
|
||||
prompt = db.get_default_prompt()
|
||||
prompt = db.get_prompt_by_id(prompt_id) if prompt_id else db.get_default_prompt()
|
||||
if not prompt:
|
||||
return jsonify({'error': 'Kein Prompt konfiguriert'}), 400
|
||||
|
||||
|
|
@ -302,6 +303,48 @@ 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/article/save', methods=['POST'])
|
||||
def api_save_article():
|
||||
data = request.json
|
||||
|
|
@ -338,12 +381,24 @@ 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})
|
||||
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
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/article/schedule', methods=['POST'])
|
||||
|
|
@ -534,14 +589,18 @@ 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:
|
||||
conn.execute(
|
||||
cur = conn.execute(
|
||||
"INSERT INTO prompts (name, system_prompt) VALUES (?,?)",
|
||||
(data['name'], data['system_prompt'])
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True})
|
||||
new_id = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'id': new_id})
|
||||
|
||||
|
||||
@app.route('/api/prompts/<int:pid>/default', methods=['POST'])
|
||||
|
|
|
|||
|
|
@ -403,6 +403,13 @@ 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,7 +88,8 @@ def generate(system_prompt: str, user_message: str) -> str:
|
|||
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
async with _aiohttp.ClientSession() as session:
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
{% 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">
|
||||
<div class="max-w-7xl mx-auto px-6 py-6" data-wp-admin="{{ wp_admin_direct }}" data-wp-url="{{ wp_url }}">
|
||||
|
||||
<!-- Status-Bar -->
|
||||
<div class="flex items-center gap-4 mb-4 text-xs text-slate-500 flex-wrap">
|
||||
|
|
@ -72,11 +77,11 @@
|
|||
<div class="card p-5">
|
||||
<h2 class="text-base font-semibold text-white mb-4">✍️ Artikel-Studio</h2>
|
||||
|
||||
<!-- Quelle + Ton -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-4">
|
||||
<!-- Quelle + Ton + Prompt -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 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="url" id="source-input" placeholder="https://..." class="w-full">
|
||||
<input type="text" id="source-input" placeholder="https://... oder Text einfügen" class="w-full">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 block mb-1">Ton</label>
|
||||
|
|
@ -86,6 +91,14 @@
|
|||
<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 -->
|
||||
|
|
@ -94,19 +107,23 @@
|
|||
<input type="text" id="article-title" placeholder="Artikel-Titel" class="w-full">
|
||||
</div>
|
||||
|
||||
<!-- Zwei-Spalten Editor -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
|
||||
<!-- Zwei-Spalten Editor + KI verbessern -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 block mb-1">Inhalt (HTML)</label>
|
||||
<textarea id="article-content" rows="14" placeholder="Artikel-Inhalt..."
|
||||
<textarea id="article-content" rows="16" placeholder="Artikel-Inhalt..."
|
||||
oninput="updatePreview()"></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>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-slate-400 block mb-1">WordPress-Vorschau</label>
|
||||
<div class="min-h-[420px]">
|
||||
<label class="text-xs text-slate-400 block mb-1">Vorschau</label>
|
||||
<div id="wp-preview"
|
||||
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>
|
||||
class="rounded-lg p-5 overflow-y-auto border border-slate-600"
|
||||
style="min-height: 380px; max-height: 60vh; 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>
|
||||
</div>
|
||||
<div id="wp-draft-link" class="hidden"></div>
|
||||
<div id="mirror-status-box" class="hidden mt-2 text-xs space-y-1"></div>
|
||||
|
|
@ -150,13 +167,18 @@
|
|||
</div>
|
||||
|
||||
<!-- Aktions-Buttons -->
|
||||
<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 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>
|
||||
|
||||
<!-- Einplan-Panel -->
|
||||
|
|
@ -341,32 +363,108 @@ 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;
|
||||
if (loading) {
|
||||
btn.dataset.origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Bitte warten...';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = btn.dataset.origText || (btnId === 'btn-generate' ? '🤖 KI generieren' : 'Verbessern');
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
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; }
|
||||
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('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();
|
||||
|
||||
// og:image automatisch holen
|
||||
if (source.startsWith('http')) {
|
||||
fetchOgImage(source);
|
||||
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);
|
||||
}
|
||||
showToast('✅ Artikel generiert');
|
||||
}
|
||||
|
||||
async function fetchOgImage(url) {
|
||||
|
|
@ -406,13 +504,19 @@ async function saveDraft() {
|
|||
currentArticleId = d.id;
|
||||
if (d.wp_post_id) currentWpPostId = d.wp_post_id;
|
||||
|
||||
// Vorschau-Link anzeigen
|
||||
// Vorschau- und Editor-Links anzeigen
|
||||
const linkBox = document.getElementById('wp-draft-link');
|
||||
if (d.wp_preview_url && linkBox) {
|
||||
linkBox.innerHTML = `<a href="${d.wp_preview_url}" target="_blank"
|
||||
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"
|
||||
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;margin-top:8px">
|
||||
👁 Entwurf in WordPress ansehen →</a>`;
|
||||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none">
|
||||
👁 Vorschau ansehen</a>`);
|
||||
linkBox.innerHTML = links.join('');
|
||||
linkBox.classList.remove('hidden');
|
||||
}
|
||||
showToast('💾 Entwurf gespeichert & nach WordPress gepusht');
|
||||
|
|
@ -495,14 +599,22 @@ 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-Vorschau-Link wiederherstellen wenn vorhanden
|
||||
// WP-Links wiederherstellen wenn vorhanden
|
||||
const linkBox = document.getElementById('wp-draft-link');
|
||||
if (d.wp_post_id && linkBox) {
|
||||
const wpBase = '{{ wp_url }}';
|
||||
linkBox.innerHTML = `<a href="${wpBase}/?p=${d.wp_post_id}&preview=true" target="_blank"
|
||||
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"
|
||||
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;margin-top:8px">
|
||||
👁 Entwurf in WordPress ansehen →</a>`;
|
||||
color:#0369a1;padding:6px 12px;border-radius:6px;font-size:0.8em;text-decoration:none">
|
||||
👁 Vorschau ansehen</a>`);
|
||||
linkBox.innerHTML = links.join('');
|
||||
linkBox.classList.remove('hidden');
|
||||
} else if (linkBox) {
|
||||
linkBox.classList.add('hidden');
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
<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 %}
|
||||
|
|
@ -58,15 +59,25 @@ 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: document.getElementById('prompt-id').value || null,
|
||||
body: JSON.stringify({id: currentId,
|
||||
name: document.getElementById('prompt-name').value,
|
||||
system_prompt: document.getElementById('prompt-text').value})});
|
||||
if ((await r.json()).success) { showToast('💾 Gespeichert'); location.reload(); }
|
||||
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();
|
||||
}
|
||||
}
|
||||
async function setDefault() {
|
||||
const id = document.getElementById('prompt-id').value;
|
||||
if (!id) { showToast('⚠️ Prompt zuerst speichern'); return; }
|
||||
if (!id) { showToast('⚠️ Prompt zuerst auswählen (klicken) und ggf. speichern'); return; }
|
||||
await fetch(`/api/prompts/${id}/default`, {method:'POST'});
|
||||
showToast('⭐ Als Standard gesetzt'); location.reload();
|
||||
}
|
||||
|
|
|
|||
27
redax-wp/test-source-kambodscha-visum.md
Normal file
27
redax-wp/test-source-kambodscha-visum.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# 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