Initial commit
13
.claude/settings.local.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(tr '>' '\\n')",
|
||||
"Bash(docker-compose restart:*)",
|
||||
"Bash(docker compose:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
backend/data/*.db
|
||||
backend/data/*.db-shm
|
||||
backend/data/*.db-wal
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
191
README.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Facebook Post Tracker
|
||||
|
||||
Eine Chrome/Firefox-Extension mit Docker-Backend zum Verwalten und Abhaken von Facebook-Beiträgen über mehrere Browser-Profile hinweg.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Browser-Extension**: Fügt jedem Facebook-Beitrag Tracking-Funktionalität hinzu
|
||||
- 📊 **Multi-Profile-Support**: Verwalte 5 verschiedene Browser-Profile
|
||||
- 🌐 **Web-Interface**: Übersicht über alle zu bearbeitenden Beiträge
|
||||
- 🔄 **Auto-Check**: Beiträge werden automatisch beim Öffnen abgehakt
|
||||
- 🐳 **Docker-Setup**: Einfaches Backend-Deployment
|
||||
|
||||
## Systemarchitektur
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Browser │
|
||||
│ Extension │◄─────┐
|
||||
└────────┬────────┘ │
|
||||
│ │
|
||||
▼ │
|
||||
┌─────────────────┐ │
|
||||
│ Docker Backend │ │
|
||||
│ (Node.js API) │ │
|
||||
│ + SQLite DB │ │
|
||||
└────────┬────────┘ │
|
||||
│ │
|
||||
▼ │
|
||||
┌─────────────────┐ │
|
||||
│ Web Interface │──────┘
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Backend starten (Docker)
|
||||
|
||||
```bash
|
||||
# In das Projektverzeichnis wechseln
|
||||
cd /mnt/c/fb
|
||||
|
||||
# Docker Container bauen und starten
|
||||
docker-compose up -d
|
||||
|
||||
# Logs ansehen (optional)
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
Das Backend läuft nun auf `http://localhost:3000`
|
||||
Das Web-Interface ist erreichbar unter `http://localhost:8080`
|
||||
|
||||
### 2. Browser-Extension installieren
|
||||
|
||||
#### Chrome:
|
||||
1. Öffne `chrome://extensions/`
|
||||
2. Aktiviere "Entwicklermodus" (oben rechts)
|
||||
3. Klicke auf "Entpackte Erweiterung laden"
|
||||
4. Wähle den Ordner `/mnt/c/fb/extension`
|
||||
|
||||
#### Firefox:
|
||||
1. Öffne `about:debugging#/runtime/this-firefox`
|
||||
2. Klicke auf "Temporäres Add-on laden"
|
||||
3. Wähle die Datei `/mnt/c/fb/extension/manifest.json`
|
||||
|
||||
### 3. Extension konfigurieren
|
||||
|
||||
1. Klicke auf das Extension-Icon 📋 in der Browser-Toolbar
|
||||
2. Wähle dein aktuelles Profil (1-5) aus
|
||||
3. Klicke auf "Speichern"
|
||||
|
||||
## Nutzung
|
||||
|
||||
### In der Browser-Extension (auf Facebook):
|
||||
|
||||
1. **Beitrag hinzufügen**: Bei jedem Facebook-Beitrag erscheint automatisch ein Tracking-UI
|
||||
- Gib die Anzahl ein (1-5), wie viele Profile den Beitrag abhaken sollen
|
||||
- Klicke auf "Hinzufügen"
|
||||
- Der Beitrag wird automatisch für das aktuelle Profil abgehakt
|
||||
|
||||
2. **Beitrag abhaken**: Bei bereits getrackten Beiträgen
|
||||
- Siehst du den Status: `X/Y Profile ✓/✗`
|
||||
- Klicke auf "Abhaken", wenn noch nicht für dein Profil erledigt
|
||||
|
||||
### Im Web-Interface:
|
||||
|
||||
1. Öffne `http://localhost:8080`
|
||||
2. Wähle dein aktuelles Profil aus
|
||||
3. **Offene Beiträge**: Zeigt alle Beiträge, die noch nicht von allen Profilen abgehakt wurden
|
||||
4. **Alle Beiträge**: Zeigt alle erfassten Beiträge
|
||||
5. Klicke auf "Beitrag öffnen & abhaken", um:
|
||||
- Den Beitrag in neuem Tab zu öffnen
|
||||
- Automatisch für dein Profil abzuhaken
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
Das Backend stellt folgende REST-API bereit:
|
||||
|
||||
- `GET /api/posts` - Alle Beiträge abrufen
|
||||
- `GET /api/posts/by-url?url=...` - Beitrag über URL abrufen
|
||||
- `POST /api/posts` - Neuen Beitrag erstellen
|
||||
- `POST /api/posts/:id/check` - Beitrag für Profil abhaken
|
||||
- `POST /api/check-by-url` - Beitrag über URL abhaken
|
||||
- `DELETE /api/posts/:id` - Beitrag löschen
|
||||
|
||||
## Datenbankstruktur
|
||||
|
||||
```sql
|
||||
posts:
|
||||
- id (TEXT, PRIMARY KEY)
|
||||
- url (TEXT, UNIQUE)
|
||||
- title (TEXT)
|
||||
- target_count (INTEGER)
|
||||
- created_at (DATETIME)
|
||||
|
||||
checks:
|
||||
- id (INTEGER, PRIMARY KEY)
|
||||
- post_id (TEXT, FOREIGN KEY)
|
||||
- profile_number (INTEGER)
|
||||
- checked_at (DATETIME)
|
||||
- UNIQUE(post_id, profile_number)
|
||||
```
|
||||
|
||||
## Docker-Befehle
|
||||
|
||||
```bash
|
||||
# Container starten
|
||||
docker-compose up -d
|
||||
|
||||
# Container stoppen
|
||||
docker-compose down
|
||||
|
||||
# Logs ansehen
|
||||
docker-compose logs -f
|
||||
|
||||
# Container neu bauen
|
||||
docker-compose build
|
||||
|
||||
# Datenbank zurücksetzen (ACHTUNG: Löscht alle Daten!)
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Entwicklung
|
||||
|
||||
### Backend lokal entwickeln (ohne Docker):
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Extension während Entwicklung neu laden:
|
||||
|
||||
- **Chrome**: Gehe zu `chrome://extensions/` und klicke auf das Reload-Symbol
|
||||
- **Firefox**: Gehe zu `about:debugging` und klicke auf "Neu laden"
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Backend nicht erreichbar
|
||||
|
||||
- Prüfe, ob Docker läuft: `docker ps`
|
||||
- Prüfe die Logs: `docker-compose logs backend`
|
||||
- Stelle sicher, dass Port 3000 nicht bereits belegt ist
|
||||
|
||||
### Extension funktioniert nicht
|
||||
|
||||
- Öffne die Browser-Konsole (F12) und prüfe auf Fehler
|
||||
- Stelle sicher, dass das Backend läuft
|
||||
- Lade die Extension neu
|
||||
- Prüfe, ob du auf Facebook.com bist
|
||||
|
||||
### CORS-Fehler
|
||||
|
||||
- Das Backend hat CORS aktiviert
|
||||
- Stelle sicher, dass du `http://localhost:3000` verwendest (nicht `127.0.0.1`)
|
||||
|
||||
### Icons fehlen in Extension
|
||||
|
||||
- Die Extension verwendet Emoji-Icons als Fallback
|
||||
- Für richtige Icons kannst du PNG-Dateien in den Größen 16x16, 48x48, 128x128 erstellen und in `extension/icons/` ablegen
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
- **Backend**: Node.js, Express, SQLite (better-sqlite3)
|
||||
- **Web-Interface**: Vanilla JavaScript, HTML, CSS
|
||||
- **Extension**: Manifest V3 (Chrome & Firefox kompatibel)
|
||||
- **Deployment**: Docker, docker-compose
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT
|
||||
15
backend/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install --production
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
BIN
backend/noScreenshot.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
19
backend/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "fb-tracker-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend for Facebook post tracker",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
1944
backend/server.js
Normal file
BIN
data/_data/screenshots/0068dcfa-18cf-4aa2-aa43-24eb3a890391.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
data/_data/screenshots/00b8b2a1-b1c2-4a1b-9b5d-5a7c532fb161.jpg
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
data/_data/screenshots/025ebdc2-969f-4de4-a374-cfca28575636.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
data/_data/screenshots/0969792f-b150-4635-adae-09bbdc595011.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
data/_data/screenshots/0a017b13-6794-4133-96d2-147fd28d6ccf.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
data/_data/screenshots/0f6e6fd4-790f-4627-995a-7ca3fd6d0ce1.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
data/_data/screenshots/11069847-41c3-42a6-9d5a-cf0d4ae65ece.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
data/_data/screenshots/286db7e5-0d36-4c1c-a290-73b193470ca5.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
data/_data/screenshots/338b3c24-ddaa-4666-acf9-05e5848e6ac4.jpg
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
data/_data/screenshots/347edaa2-ead7-49fc-8594-8db7b799e751.jpg
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
data/_data/screenshots/37827b3a-2b7a-41d6-9bab-234789c34545.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
data/_data/screenshots/46eb7edd-536c-4bca-bae4-f96b8c6fecde.jpg
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
data/_data/screenshots/48564db4-d1bd-4ec9-b327-551c303388a4.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
data/_data/screenshots/48f9c844-24b4-48a1-bb44-d45e54a8f7b1.jpg
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
data/_data/screenshots/49a2b60d-b14a-43d0-a02c-84f61c0914f2.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
data/_data/screenshots/4b4464a1-a38f-4454-bb98-0e696aed37bd.jpg
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
data/_data/screenshots/4de302e1-04e1-4292-8612-3f0503ec83ee.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
data/_data/screenshots/54b56007-6927-483e-ba06-a19c69e0ce06.jpg
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
data/_data/screenshots/59c49a02-867f-435a-bf3b-13ca40a1e22a.jpg
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
data/_data/screenshots/5bdc951b-8d12-4922-9055-98cbaddd62bc.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
data/_data/screenshots/5f691344-58b0-40a0-bb20-0ff11349d622.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
data/_data/screenshots/6026f6bf-d7e5-47c4-8a3e-e16d5576bdc6.jpg
Normal file
|
After Width: | Height: | Size: 179 KiB |
BIN
data/_data/screenshots/60ec7a10-1e50-4a6f-96e8-d865abb2c3e7.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
data/_data/screenshots/661a7efd-db35-4963-8e86-da0f087c47e4.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
data/_data/screenshots/6656a743-1689-4492-a7f2-44fd5e2db051.jpg
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
data/_data/screenshots/70158d79-a86d-4a21-824e-18a9ecccd7ed.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
data/_data/screenshots/753957f5-9508-474f-8055-99d7b0592279.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
data/_data/screenshots/771ae55e-66e6-491e-9915-379cc543c6e1.jpg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
data/_data/screenshots/77a234b7-6bd4-4e81-abae-76c93739b64b.jpg
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
data/_data/screenshots/7db61ad4-8e1f-46c2-bf52-266e946d3925.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
data/_data/screenshots/82643673-8a80-44a0-9de1-a67435306099.jpg
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
data/_data/screenshots/88ca9a64-4415-4bc5-8ca0-f24ab0443806.jpg
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
data/_data/screenshots/8b9413ce-f2d3-4acb-a76b-1adb99068471.jpg
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
data/_data/screenshots/9b71c2cb-072c-4b2d-8831-98aba98e6ed7.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
data/_data/screenshots/a5266ed6-7e7f-4275-a406-ebd1aaec93a2.jpg
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
data/_data/screenshots/a6177dbe-5946-4ddc-b611-bd8e0dbdc732.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
data/_data/screenshots/a6197b86-3162-4375-886d-53dfd79a403d.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
data/_data/screenshots/aa1ff7c6-1e4c-4a33-bd92-c661d50bff9e.jpg
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
data/_data/screenshots/ab3296ae-94f4-4884-b873-df0ece5ff4c8.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
data/_data/screenshots/b1aaf381-927a-453c-9881-76a1cfd21a5b.jpg
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
data/_data/screenshots/b1b4bc40-baac-4bac-91e5-79979ca4de53.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
data/_data/screenshots/b62db094-4ed5-4dd3-b3c2-81c23aaea92c.jpg
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
data/_data/screenshots/b7b3765f-e6ed-436a-9f9b-3bb529f026ee.jpg
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
data/_data/screenshots/ba29024d-545f-4384-8b4a-ca7f41ea90f4.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
data/_data/screenshots/bcef9016-b3f0-474e-a8b1-f6bf9e11557c.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
data/_data/screenshots/beb8be22-5a4b-4b64-aae1-4d3e008ffd58.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
data/_data/screenshots/c4b71f22-754a-4567-b7e3-f5293d15922b.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
data/_data/screenshots/cf921190-f429-4293-9580-fb4499ef6099.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
data/_data/screenshots/d97b86b0-cc5e-41bc-9471-d9be757a720d.jpg
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
data/_data/screenshots/d9a3e791-e135-4d63-a376-637137f17187.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
data/_data/screenshots/dc70e5e0-57db-4605-bb99-e85bd8b862dd.jpg
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
data/_data/screenshots/ded93d20-b3c6-4d1f-b1e1-6d96044b7257.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
data/_data/screenshots/e5f5a36c-3fd4-48a2-b742-9c231ded1098.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
data/_data/screenshots/e5f60072-18a7-4303-a718-9f6beb6fb5b7.jpg
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
data/_data/screenshots/eac0eb15-cea2-45b0-a00b-ffe68d8644d0.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
data/_data/screenshots/f2c8fdae-bb7f-4073-a06c-d9f6a03414b2.jpg
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
data/_data/screenshots/f9375c46-1653-49e3-b0c5-a5d52afe0673.jpg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
data/_data/tracker.db
Normal file
BIN
data/tracker.db
Normal file
29
docker-compose.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: fb-tracker-backend
|
||||
ports:
|
||||
- "3001:3000"
|
||||
volumes:
|
||||
- ./backend/server.js:/app/server.js:ro
|
||||
- db-data:/app/data
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
labels:
|
||||
- com.centurylinklabs.watchtower.enable=false
|
||||
restart: unless-stopped
|
||||
|
||||
web:
|
||||
build: ./web
|
||||
container_name: fb-tracker-web
|
||||
ports:
|
||||
- "8081:80"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
48
extension/background.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// Background script for service worker
|
||||
// Currently minimal, can be extended for additional functionality
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
console.log('Facebook Post Tracker extension installed');
|
||||
|
||||
// Set default profile if not set
|
||||
chrome.storage.sync.get(['profileNumber'], (result) => {
|
||||
if (!result.profileNumber) {
|
||||
chrome.storage.sync.set({ profileNumber: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
// Create context menu for manual post parsing
|
||||
chrome.contextMenus.create({
|
||||
id: 'fb-tracker-reparse',
|
||||
title: 'FB Tracker: Post neu parsen',
|
||||
contexts: ['all'],
|
||||
documentUrlPatterns: ['*://*.facebook.com/*']
|
||||
});
|
||||
});
|
||||
|
||||
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||
if (info.menuItemId === 'fb-tracker-reparse') {
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
type: 'reparsePost',
|
||||
x: info.pageX || 0,
|
||||
y: info.pageY || 0
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message && message.type === 'captureScreenshot') {
|
||||
const windowId = sender && sender.tab ? sender.tab.windowId : chrome.windows.WINDOW_ID_CURRENT;
|
||||
|
||||
chrome.tabs.captureVisibleTab(windowId, { format: 'jpeg', quality: 80 }, (imageData) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
sendResponse({ error: chrome.runtime.lastError.message });
|
||||
return;
|
||||
}
|
||||
|
||||
sendResponse({ imageData });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
3
extension/config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// API Configuration
|
||||
// Passe die URL an deine Deployment-Domain an
|
||||
const API_BASE_URL = 'https://fb.srv.medeba-media.de';
|
||||
64
extension/content.css
Normal file
@@ -0,0 +1,64 @@
|
||||
.fb-tracker-ui {
|
||||
margin: 8px 0;
|
||||
padding: 6px 12px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 6px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.fb-tracker-status,
|
||||
.fb-tracker-add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.fb-tracker-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.fb-tracker-text {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #050505;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fb-tracker-status.complete .fb-tracker-text {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
.fb-tracker-count {
|
||||
padding: 4px 6px;
|
||||
border: 1px solid #ccd0d5;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fb-tracker-add-btn,
|
||||
.fb-tracker-check-btn {
|
||||
padding: 4px 12px;
|
||||
background: #1877f2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.fb-tracker-add-btn:hover,
|
||||
.fb-tracker-check-btn:hover {
|
||||
background: #166fe5;
|
||||
}
|
||||
|
||||
.fb-tracker-check-btn {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.fb-tracker-check-btn:hover {
|
||||
background: #047857;
|
||||
}
|
||||
3608
extension/content.js
Normal file
BIN
extension/icons/icon-128.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
extension/icons/icon-16.png
Normal file
|
After Width: | Height: | Size: 209 B |
BIN
extension/icons/icon-32.png
Normal file
|
After Width: | Height: | Size: 340 B |
BIN
extension/icons/icon-48.png
Normal file
|
After Width: | Height: | Size: 465 B |
BIN
extension/icons/icon-512.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
4
extension/icons/icon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<rect width="128" height="128" rx="20" fill="#1877f2"/>
|
||||
<text x="64" y="90" font-size="80" text-anchor="middle" fill="white">📋</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 210 B |
47
extension/manifest.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Facebook Post Tracker",
|
||||
"version": "1.1.0",
|
||||
"description": "Track Facebook posts across multiple profiles",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"contextMenus"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>",
|
||||
"https://www.facebook.com/*",
|
||||
"https://facebook.com/*",
|
||||
"http://localhost:3001/*",
|
||||
"https://fb.srv.medeba-media.de/*"
|
||||
],
|
||||
"icons": {
|
||||
"16": "icons/icon-16.png",
|
||||
"32": "icons/icon-32.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"128": "icons/icon-128.png"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icons/icon-16.png",
|
||||
"32": "icons/icon-32.png",
|
||||
"48": "icons/icon-48.png"
|
||||
}
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://www.facebook.com/*",
|
||||
"https://facebook.com/*"
|
||||
],
|
||||
"js": ["config.js", "content.js"],
|
||||
"css": ["content.css"],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
}
|
||||
}
|
||||
119
extension/popup.html
Normal file
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Facebook Post Tracker</title>
|
||||
<style>
|
||||
body {
|
||||
width: 300px;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 16px 0;
|
||||
color: #050505;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #050505;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccd0d5;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 12px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #65676b;
|
||||
}
|
||||
|
||||
.status.saved {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background: #1877f2;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #166fe5;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: #e4e6eb;
|
||||
color: #050505;
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: #d8dadf;
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 12px;
|
||||
color: #65676b;
|
||||
margin-top: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>📋 Facebook Post Tracker</h1>
|
||||
|
||||
<div class="section">
|
||||
<label for="profileSelect">Aktuelles Profil:</label>
|
||||
<select id="profileSelect">
|
||||
<option value="1">Profil 1</option>
|
||||
<option value="2">Profil 2</option>
|
||||
<option value="3">Profil 3</option>
|
||||
<option value="4">Profil 4</option>
|
||||
<option value="5">Profil 5</option>
|
||||
</select>
|
||||
<div class="info">Wähle das Browser-Profil aus, mit dem du aktuell arbeitest.</div>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status"></div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button id="saveBtn">Speichern</button>
|
||||
<button id="webInterfaceBtn" class="secondary">Web-Interface</button>
|
||||
</div>
|
||||
|
||||
<script src="config.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
99
extension/popup.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const profileSelect = document.getElementById('profileSelect');
|
||||
const statusEl = document.getElementById('status');
|
||||
|
||||
function apiFetch(url, options = {}) {
|
||||
const config = {
|
||||
...options,
|
||||
credentials: 'include'
|
||||
};
|
||||
|
||||
if (options && options.headers) {
|
||||
config.headers = { ...options.headers };
|
||||
}
|
||||
|
||||
return fetch(url, config);
|
||||
}
|
||||
|
||||
async function fetchProfileState() {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE_URL}/api/profile-state`);
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data && typeof data.profile_number !== 'undefined') {
|
||||
const parsed = parseInt(data.profile_number, 10);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn('Profile state fetch failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfileState(profileNumber) {
|
||||
try {
|
||||
const response = await apiFetch(`${API_BASE_URL}/api/profile-state`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ profile_number: profileNumber })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update profile state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(message, saved = false) {
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = saved ? 'status saved' : 'status';
|
||||
}
|
||||
|
||||
async function initProfileSelect() {
|
||||
const backendProfile = await fetchProfileState();
|
||||
if (backendProfile) {
|
||||
profileSelect.value = String(backendProfile);
|
||||
chrome.storage.sync.set({ profileNumber: backendProfile });
|
||||
updateStatus(`Profil ${backendProfile} ausgewählt`);
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.storage.sync.get(['profileNumber'], (result) => {
|
||||
const profileNumber = result.profileNumber || 1;
|
||||
profileSelect.value = String(profileNumber);
|
||||
updateStatus(`Profil ${profileNumber} ausgewählt (lokal)`);
|
||||
});
|
||||
}
|
||||
|
||||
initProfileSelect();
|
||||
|
||||
function reloadFacebookTabs() {
|
||||
chrome.tabs.query({ url: ['https://www.facebook.com/*', 'https://facebook.com/*'] }, (tabs) => {
|
||||
tabs.forEach(tab => {
|
||||
chrome.tabs.reload(tab.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('saveBtn').addEventListener('click', async () => {
|
||||
const profileNumber = parseInt(profileSelect.value, 10);
|
||||
|
||||
chrome.storage.sync.set({ profileNumber }, async () => {
|
||||
updateStatus(`Profil ${profileNumber} gespeichert!`, true);
|
||||
await updateProfileState(profileNumber);
|
||||
reloadFacebookTabs();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('webInterfaceBtn').addEventListener('click', () => {
|
||||
chrome.tabs.create({ url: API_BASE_URL });
|
||||
});
|
||||
1
fb_buttonbar.txt
Normal file
446
fb_feed.txt
Normal file
2
fb_nottracked.txt
Normal file
1
fb_post.txt
Normal file
1
fb_postLinkVariante1.txt
Normal file
1
fb_postLinkVariante2.txt
Normal file
BIN
noScreenshot.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
16
web/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
COPY dashboard.html /usr/share/nginx/html/
|
||||
COPY settings.html /usr/share/nginx/html/
|
||||
COPY style.css /usr/share/nginx/html/
|
||||
COPY dashboard.css /usr/share/nginx/html/
|
||||
COPY settings.css /usr/share/nginx/html/
|
||||
COPY app.js /usr/share/nginx/html/
|
||||
COPY dashboard.js /usr/share/nginx/html/
|
||||
COPY settings.js /usr/share/nginx/html/
|
||||
COPY assets /usr/share/nginx/html/assets/
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
2869
web/app.js
Normal file
BIN
web/assets/app-icon-192.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
web/assets/app-icon-512.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
web/assets/app-icon-64.png
Normal file
|
After Width: | Height: | Size: 621 B |
804
web/dashboard.css
Normal file
@@ -0,0 +1,804 @@
|
||||
/* ==========================================
|
||||
DASHBOARD - UNIFIED DESIGN SYSTEM
|
||||
========================================== */
|
||||
|
||||
/* Color Palette */
|
||||
:root {
|
||||
--color-primary: #1877f2;
|
||||
--color-success: #42b983;
|
||||
--color-warning: #f39c12;
|
||||
--color-danger: #e74c3c;
|
||||
--color-info: #3498db;
|
||||
|
||||
--bg-card: #ffffff;
|
||||
--bg-section: #f8f9fa;
|
||||
--bg-hover: #f0f2f5;
|
||||
|
||||
--text-primary: #1c1e21;
|
||||
--text-secondary: #65676b;
|
||||
--text-muted: #8a8d91;
|
||||
|
||||
--border-light: #e4e6eb;
|
||||
--border-medium: #d1d5db;
|
||||
|
||||
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
--shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
|
||||
--spacing-xs: 8px;
|
||||
--spacing-sm: 12px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
}
|
||||
|
||||
/* Dashboard Container */
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xl);
|
||||
padding: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
SECTIONS
|
||||
========================================== */
|
||||
|
||||
.dashboard-section {
|
||||
background: var(--bg-section);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-lg) 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
OVERVIEW SECTION
|
||||
========================================== */
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
transition: all 0.2s ease;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.stat-card--primary { border-left-color: var(--color-primary); }
|
||||
.stat-card--success { border-left-color: var(--color-success); }
|
||||
.stat-card--warning { border-left-color: var(--color-warning); }
|
||||
.stat-card--danger { border-left-color: var(--color-danger); }
|
||||
.stat-card--info { border-left-color: var(--color-info); }
|
||||
|
||||
.stat-card__icon {
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.stat-card__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-card__label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.stat-card__value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Metrics Grid */
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.metric-card__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.metric-card__value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.metric-card__change {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 4px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.metric-card__change--up {
|
||||
color: var(--color-success);
|
||||
background: rgba(66, 185, 131, 0.1);
|
||||
}
|
||||
|
||||
.metric-card__change--down {
|
||||
color: var(--color-danger);
|
||||
background: rgba(231, 76, 60, 0.1);
|
||||
}
|
||||
|
||||
.metric-card__change--neutral {
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.metric-card__subtext {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
ANALYTICS SECTION
|
||||
========================================== */
|
||||
|
||||
.charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.charts-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.chart-card--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.chart-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.chart-card__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.chart-card__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chart-card__subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chart-card__body {
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Bar Chart (for profile chart) */
|
||||
.bar-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.bar-chart-item {
|
||||
display: grid;
|
||||
grid-template-columns: 80px 1fr 60px;
|
||||
gap: var(--spacing-sm);
|
||||
align-items: center;
|
||||
padding: var(--spacing-xs);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.bar-chart-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.bar-chart-item__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bar-chart-item__bar-container {
|
||||
background: var(--border-light);
|
||||
border-radius: var(--radius-sm);
|
||||
height: 32px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.bar-chart-item__bar {
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, #4a9eff 100%);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: var(--spacing-xs);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: width 0.4s ease;
|
||||
min-width: 2px;
|
||||
}
|
||||
|
||||
.bar-chart-item__value {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
PERFORMANCE COMPARISONS SECTION
|
||||
========================================== */
|
||||
|
||||
.comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.comparison-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.comparison-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.comparison-card__title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
padding-bottom: var(--spacing-sm);
|
||||
border-bottom: 2px solid var(--border-light);
|
||||
}
|
||||
|
||||
.comparison-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.comparison-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.comparison-item__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.comparison-item__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.comparison-item__value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.comparison-item__bar {
|
||||
background: var(--border-light);
|
||||
border-radius: 100px;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.comparison-item__bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 100px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.comparison-item__bar-fill--current {
|
||||
background: linear-gradient(90deg, var(--color-primary), #4a9eff);
|
||||
}
|
||||
|
||||
.comparison-item__bar-fill--success {
|
||||
background: linear-gradient(90deg, var(--color-success), #5dd39e);
|
||||
}
|
||||
|
||||
.comparison-item__subtext {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.comparison-item__change {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 3px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.comparison-item__change--up {
|
||||
color: var(--color-success);
|
||||
background: rgba(66, 185, 131, 0.12);
|
||||
}
|
||||
|
||||
.comparison-item__change--down {
|
||||
color: var(--color-danger);
|
||||
background: rgba(231, 76, 60, 0.12);
|
||||
}
|
||||
|
||||
.comparison-item__change--neutral {
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.comparison-divider {
|
||||
height: 1px;
|
||||
background: var(--border-light);
|
||||
margin: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
/* Success Comparison Grid */
|
||||
.success-comparison-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.success-comparison-card {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-light);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.success-comparison-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.success-comparison-card__title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 var(--spacing-md) 0;
|
||||
padding-bottom: var(--spacing-sm);
|
||||
border-bottom: 2px solid var(--color-success);
|
||||
}
|
||||
|
||||
.success-comparison-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
DETAILS SECTION
|
||||
========================================== */
|
||||
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.detail-card--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.detail-card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.detail-card__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding-bottom: var(--spacing-sm);
|
||||
border-bottom: 2px solid var(--border-light);
|
||||
}
|
||||
|
||||
.detail-card__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detail-card__badge {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 4px 10px;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.detail-card__body {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Performers List */
|
||||
.performers-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.performer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--bg-hover);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.performer-item:hover {
|
||||
background: var(--border-light);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.performer-item--gold {
|
||||
background: linear-gradient(135deg, #ffd700 20%, #fff9e6 100%);
|
||||
border: 2px solid #ffd700;
|
||||
}
|
||||
|
||||
.performer-item--silver {
|
||||
background: linear-gradient(135deg, #c0c0c0 20%, #f5f5f5 100%);
|
||||
border: 2px solid #c0c0c0;
|
||||
}
|
||||
|
||||
.performer-item--bronze {
|
||||
background: linear-gradient(135deg, #cd7f32 20%, #fff5e6 100%);
|
||||
border: 2px solid #cd7f32;
|
||||
}
|
||||
|
||||
.performer-item__rank {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary);
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.performer-item__avatar {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.performer-item__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.performer-item__name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.performer-item__stats {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.performer-item__badge {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 4px 12px;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
/* Deadline List */
|
||||
.deadline-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.deadline-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--bg-hover);
|
||||
border-radius: var(--radius-sm);
|
||||
border-left: 4px solid var(--color-info);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.deadline-item:hover {
|
||||
background: var(--border-light);
|
||||
}
|
||||
|
||||
.deadline-item--warning {
|
||||
border-left-color: var(--color-warning);
|
||||
background: rgba(243, 156, 18, 0.05);
|
||||
}
|
||||
|
||||
.deadline-item--danger {
|
||||
border-left-color: var(--color-danger);
|
||||
background: rgba(231, 76, 60, 0.05);
|
||||
}
|
||||
|
||||
.deadline-item__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.deadline-item__title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.deadline-item__progress {
|
||||
font-size: 11px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.deadline-item__time {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary);
|
||||
padding: 4px 10px;
|
||||
background: white;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* Activity List */
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--bg-hover);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.activity-item:hover {
|
||||
background: var(--border-light);
|
||||
}
|
||||
|
||||
.activity-item__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-success);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-item__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.activity-item__text {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.activity-item__profile {
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.activity-item__time {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
EMPTY STATES
|
||||
========================================== */
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-xl);
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: var(--spacing-sm);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
RESPONSIVE DESIGN
|
||||
========================================== */
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.charts-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chart-card--full {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-section {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
|
||||
.stats-grid,
|
||||
.metrics-grid,
|
||||
.comparison-grid,
|
||||
.success-comparison-grid,
|
||||
.details-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-card--full {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.stat-card__value,
|
||||
.metric-card__value {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================
|
||||
ANIMATIONS
|
||||
========================================== */
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-section {
|
||||
animation: fadeInUp 0.4s ease forwards;
|
||||
}
|
||||
|
||||
.dashboard-section:nth-child(1) { animation-delay: 0s; }
|
||||
.dashboard-section:nth-child(2) { animation-delay: 0.1s; }
|
||||
.dashboard-section:nth-child(3) { animation-delay: 0.2s; }
|
||||
.dashboard-section:nth-child(4) { animation-delay: 0.3s; }
|
||||
274
web/dashboard.html
Normal file
@@ -0,0 +1,274 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard - Facebook Post Tracker</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="assets/app-icon-192.png">
|
||||
<link rel="apple-touch-icon" href="assets/app-icon-192.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-main">
|
||||
<h1>📊 Dashboard</h1>
|
||||
<a href="?view=posts" class="btn btn-secondary">Zurück zu Beiträgen</a>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<div class="control-group">
|
||||
<label for="timeFilter">Zeitraum:</label>
|
||||
<select id="timeFilter" class="control-select">
|
||||
<option value="all">Alle</option>
|
||||
<option value="today">Heute</option>
|
||||
<option value="week" selected>Diese Woche</option>
|
||||
<option value="month">Dieser Monat</option>
|
||||
<option value="year">Dieses Jahr</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="profileFilter">Profil-Filter:</label>
|
||||
<select id="profileFilter" class="control-select">
|
||||
<option value="all">Alle Profile</option>
|
||||
<option value="1">Profil 1</option>
|
||||
<option value="2">Profil 2</option>
|
||||
<option value="3">Profil 3</option>
|
||||
<option value="4">Profil 4</option>
|
||||
<option value="5">Profil 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="refreshBtn">
|
||||
🔄 Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="loading" class="loading">Lade Statistiken...</div>
|
||||
<div id="error" class="error" style="display: none;"></div>
|
||||
|
||||
<div id="dashboardContainer" class="dashboard-container" style="display: none;">
|
||||
|
||||
<!-- SECTION 1: OVERVIEW -->
|
||||
<section class="dashboard-section">
|
||||
<h2 class="section-title">Übersicht</h2>
|
||||
|
||||
<!-- Primary Stats -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card stat-card--primary">
|
||||
<div class="stat-card__icon">📋</div>
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__label">Gesamt Beiträge</div>
|
||||
<div class="stat-card__value" id="totalPosts">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card stat-card--success">
|
||||
<div class="stat-card__icon">✓</div>
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__label">Abgeschlossen</div>
|
||||
<div class="stat-card__value" id="completedPosts">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card stat-card--warning">
|
||||
<div class="stat-card__icon">⏳</div>
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__label">In Bearbeitung</div>
|
||||
<div class="stat-card__value" id="activePosts">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card stat-card--danger">
|
||||
<div class="stat-card__icon">⚠️</div>
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__label">Abgelaufen</div>
|
||||
<div class="stat-card__value" id="expiredPosts">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card stat-card--info">
|
||||
<div class="stat-card__icon">🏆</div>
|
||||
<div class="stat-card__content">
|
||||
<div class="stat-card__label">Erfolgreich</div>
|
||||
<div class="stat-card__value" id="successfulPosts">0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">Erfolgsquote</div>
|
||||
<div class="metric-card__value" id="successRateMetric">0%</div>
|
||||
<div class="metric-card__change" id="successRateChange"></div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">Ø Bearbeitungszeit</div>
|
||||
<div class="metric-card__value" id="avgCompletionTime">-</div>
|
||||
<div class="metric-card__subtext">bis Abschluss</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">Teilnahmen heute</div>
|
||||
<div class="metric-card__value" id="checksToday">0</div>
|
||||
<div class="metric-card__change" id="checksTodayChange"></div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card">
|
||||
<div class="metric-card__label">Deadline-Risiko</div>
|
||||
<div class="metric-card__value" id="deadlineRiskValue">0</div>
|
||||
<div class="metric-card__subtext" id="deadlineRiskText">keine Risiken</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SECTION 2: ANALYTICS -->
|
||||
<section class="dashboard-section">
|
||||
<h2 class="section-title">Analyse</h2>
|
||||
|
||||
<div class="charts-row">
|
||||
<!-- Activity Timeline -->
|
||||
<div class="chart-card chart-card--full">
|
||||
<div class="chart-card__header">
|
||||
<h3 class="chart-card__title">Aktivitätsverlauf</h3>
|
||||
<span class="chart-card__subtitle" id="timelineSubtitle"></span>
|
||||
</div>
|
||||
<div class="chart-card__body">
|
||||
<canvas id="timelineChart" width="1200" height="280"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<!-- Profile Performance -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-card__header">
|
||||
<h3 class="chart-card__title">Teilnahmen pro Profil</h3>
|
||||
<span class="chart-card__subtitle" id="profileChartSubtitle"></span>
|
||||
</div>
|
||||
<div class="chart-card__body">
|
||||
<div id="profileChart" class="bar-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Overview -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-card__header">
|
||||
<h3 class="chart-card__title">Status-Verteilung</h3>
|
||||
<span class="chart-card__subtitle" id="progressChartSubtitle"></span>
|
||||
</div>
|
||||
<div class="chart-card__body">
|
||||
<canvas id="progressChart" width="400" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="charts-row">
|
||||
<!-- Period Trend -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-card__header">
|
||||
<h3 class="chart-card__title">Teilnahmen-Trend</h3>
|
||||
<span class="chart-card__subtitle" id="trendChartSubtitle"></span>
|
||||
</div>
|
||||
<div class="chart-card__body">
|
||||
<canvas id="periodTrendChart" width="500" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Comparison -->
|
||||
<div class="chart-card">
|
||||
<div class="chart-card__header">
|
||||
<h3 class="chart-card__title">Profilvergleich</h3>
|
||||
<span class="chart-card__subtitle" id="profileComparisonSubtitle"></span>
|
||||
</div>
|
||||
<div class="chart-card__body">
|
||||
<canvas id="profileComparisonChart" width="500" height="300"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SECTION 3: PERFORMANCE COMPARISONS -->
|
||||
<section class="dashboard-section">
|
||||
<h2 class="section-title">Performance-Vergleiche</h2>
|
||||
|
||||
<div class="comparison-grid">
|
||||
<div class="comparison-card">
|
||||
<h3 class="comparison-card__title">Wochenvergleich</h3>
|
||||
<div class="comparison-card__content" id="weeklyComparison"></div>
|
||||
</div>
|
||||
<div class="comparison-card">
|
||||
<h3 class="comparison-card__title">Monatsvergleich</h3>
|
||||
<div class="comparison-card__content" id="monthlyComparison"></div>
|
||||
</div>
|
||||
<div class="comparison-card">
|
||||
<h3 class="comparison-card__title">Jahresvergleich</h3>
|
||||
<div class="comparison-card__content" id="yearlyComparison"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="success-comparison-grid">
|
||||
<div class="success-comparison-card">
|
||||
<h3 class="success-comparison-card__title">Erfolgsanalyse: Woche</h3>
|
||||
<div class="success-comparison-card__content" id="weeklySuccessComparison"></div>
|
||||
</div>
|
||||
<div class="success-comparison-card">
|
||||
<h3 class="success-comparison-card__title">Erfolgsanalyse: Monat</h3>
|
||||
<div class="success-comparison-card__content" id="monthlySuccessComparison"></div>
|
||||
</div>
|
||||
<div class="success-comparison-card">
|
||||
<h3 class="success-comparison-card__title">Erfolgsanalyse: Jahr</h3>
|
||||
<div class="success-comparison-card__content" id="yearlySuccessComparison"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SECTION 4: DETAILS -->
|
||||
<section class="dashboard-section">
|
||||
<h2 class="section-title">Details</h2>
|
||||
|
||||
<div class="details-grid">
|
||||
<!-- Top Performers -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card__header">
|
||||
<h3 class="detail-card__title">Top Performers</h3>
|
||||
<span class="detail-card__badge" id="performersCount">0</span>
|
||||
</div>
|
||||
<div class="detail-card__body">
|
||||
<div id="topPerformers" class="performers-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upcoming Deadlines -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card__header">
|
||||
<h3 class="detail-card__title">Anstehende Deadlines</h3>
|
||||
<span class="detail-card__badge" id="deadlinesCount">0</span>
|
||||
</div>
|
||||
<div class="detail-card__body">
|
||||
<div id="upcomingDeadlines" class="deadline-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="detail-card detail-card--full">
|
||||
<div class="detail-card__header">
|
||||
<h3 class="detail-card__title">Letzte Aktivitäten</h3>
|
||||
<span class="detail-card__badge" id="activityCount">0</span>
|
||||
</div>
|
||||
<div class="detail-card__body">
|
||||
<div id="recentActivity" class="activity-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1581
web/dashboard.js
Normal file
136
web/index.html
Normal file
@@ -0,0 +1,136 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Post Tracker - Web Interface</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="assets/app-icon-192.png">
|
||||
<link rel="apple-touch-icon" href="assets/app-icon-192.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-main">
|
||||
<h1>📋 Post Tracker</h1>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<a href="?view=dashboard" class="btn btn-secondary">Dashboard</a>
|
||||
<a href="settings.html" class="btn btn-secondary">⚙️ Einstellungen</a>
|
||||
<button type="button" class="btn btn-primary" id="openManualPostModalBtn">Beitrag hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-controls">
|
||||
<div class="control-group">
|
||||
<label for="profileSelect">Dein Profil:</label>
|
||||
<select id="profileSelect" class="control-select">
|
||||
<option value="1">Profil 1</option>
|
||||
<option value="2">Profil 2</option>
|
||||
<option value="3">Profil 3</option>
|
||||
<option value="4">Profil 4</option>
|
||||
<option value="5">Profil 5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label class="switch">
|
||||
<input type="checkbox" id="autoRefreshToggle" checked>
|
||||
<span>Auto-Refresh</span>
|
||||
</label>
|
||||
<select id="autoRefreshInterval" class="control-select">
|
||||
<option value="15000">15 s</option>
|
||||
<option value="30000">30 s</option>
|
||||
<option value="60000">1 min</option>
|
||||
<option value="120000">2 min</option>
|
||||
<option value="300000">5 min</option>
|
||||
</select>
|
||||
<button type="button" id="manualRefreshBtn" class="refresh-btn" aria-label="Aktualisieren" title="Aktualisieren">🔄</button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="sortMode">Sortierung:</label>
|
||||
<div class="sort-controls">
|
||||
<select id="sortMode" class="control-select">
|
||||
<option value="created">Erstelldatum</option>
|
||||
<option value="deadline">Deadline</option>
|
||||
<option value="lastCheck">Letzte Teilnahme</option>
|
||||
<option value="lastChange">Letzte Änderung</option>
|
||||
<option value="smart">Smart (Dringlichkeit)</option>
|
||||
</select>
|
||||
<button type="button" id="sortDirectionToggle" class="sort-direction-toggle" aria-label="Absteigend" aria-pressed="false" title="Absteigend">
|
||||
<span class="sort-direction-toggle__icon" aria-hidden="true">▼</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="tabs-section">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="pending">Offene Beiträge</button>
|
||||
<button class="tab-btn" data-tab="expired">Abgelaufen/Abgeschlossen</button>
|
||||
<button class="tab-btn" data-tab="all">Alle Beiträge</button>
|
||||
</div>
|
||||
<div class="search-container">
|
||||
<input type="text" id="searchInput" class="search-input" placeholder="Beiträge durchsuchen...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loading" class="loading">Lade Beiträge...</div>
|
||||
<div id="error" class="error" style="display: none;"></div>
|
||||
|
||||
<div id="postsContainer" class="posts-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="screenshotModal" class="screenshot-modal" hidden>
|
||||
<div id="screenshotModalBackdrop" class="screenshot-modal__backdrop" aria-hidden="true"></div>
|
||||
<div id="screenshotModalContent" class="screenshot-modal__content" role="dialog" aria-modal="true">
|
||||
<button type="button" id="screenshotModalClose" class="screenshot-modal__close" aria-label="Schließen">×</button>
|
||||
<img id="screenshotModalImage" alt="Screenshot zum Beitrag" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="manualPostModal" class="modal" hidden>
|
||||
<div id="manualPostModalBackdrop" class="modal__backdrop" aria-hidden="true"></div>
|
||||
<div id="manualPostModalContent" class="modal__content" role="dialog" aria-modal="true" aria-labelledby="manualPostModalTitle" tabindex="-1">
|
||||
<button type="button" id="manualPostModalClose" class="modal__close" aria-label="Schließen">×</button>
|
||||
<h2 id="manualPostModalTitle">Beitrag hinzufügen</h2>
|
||||
<form id="manualPostForm" novalidate>
|
||||
<div class="form-grid">
|
||||
<label class="form-field">
|
||||
<span>Direktlink *</span>
|
||||
<input type="url" id="manualPostUrl" placeholder="https://www.facebook.com/..." required>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Titel</span>
|
||||
<input type="text" id="manualPostTitle" placeholder="Kurzbeschreibung" maxlength="200">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Benötigte Profile *</span>
|
||||
<select id="manualPostTarget" required>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Erstellt von (Facebook-Name)</span>
|
||||
<input type="text" id="manualPostCreatorName" placeholder="z.B. Max Mustermann">
|
||||
</label>
|
||||
<label class="form-field">
|
||||
<span>Deadline</span>
|
||||
<input type="datetime-local" id="manualPostDeadline">
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" id="manualPostSubmitBtn">Speichern</button>
|
||||
<button type="button" class="btn btn-secondary" id="manualPostReset">Zurücksetzen</button>
|
||||
</div>
|
||||
<div id="manualPostMessage" class="form-message" role="status" aria-live="polite"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
339
web/settings.css
Normal file
@@ -0,0 +1,339 @@
|
||||
/* Settings Page Styles */
|
||||
|
||||
.settings-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.settings-section {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1c1e21;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: 14px;
|
||||
color: #65676b;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 32px 0;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1c1e21;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-label input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form-input,
|
||||
.form-select,
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.form-input:focus,
|
||||
.form-select:focus,
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #1877f2;
|
||||
box-shadow: 0 0 0 3px rgba(24, 119, 242, 0.1);
|
||||
}
|
||||
|
||||
.form-input::placeholder,
|
||||
.form-textarea::placeholder {
|
||||
color: #8a8d91;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.form-help {
|
||||
font-size: 12px;
|
||||
color: #65676b;
|
||||
margin: 6px 0 0 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-help a {
|
||||
color: #1877f2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.form-help a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #e4e6eb;
|
||||
}
|
||||
|
||||
/* Credentials List */
|
||||
|
||||
.credentials-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.credential-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border: 1px solid #e4e6eb;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.credential-item:hover {
|
||||
border-color: #1877f2;
|
||||
box-shadow: 0 2px 8px rgba(24, 119, 242, 0.1);
|
||||
}
|
||||
|
||||
.credential-item.drag-over {
|
||||
border-color: #42b72a;
|
||||
background: #f0fdf4;
|
||||
box-shadow: 0 2px 8px rgba(66, 183, 42, 0.2);
|
||||
}
|
||||
|
||||
.credential-item__drag-handle {
|
||||
font-size: 20px;
|
||||
color: #65676b;
|
||||
margin-right: 12px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.credential-item__drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.credential-item__info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.credential-item__name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1c1e21;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.credential-item__provider {
|
||||
font-size: 13px;
|
||||
color: #65676b;
|
||||
}
|
||||
|
||||
.credential-item__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #65676b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
padding: 14px 18px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
/* Test Modal */
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal__backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal__content {
|
||||
position: relative;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.modal__close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: #f0f2f5;
|
||||
color: #65676b;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal__close:hover {
|
||||
background: #e4e6eb;
|
||||
color: #1c1e21;
|
||||
}
|
||||
|
||||
.modal__title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1c1e21;
|
||||
margin: 0 0 24px 0;
|
||||
}
|
||||
|
||||
.modal__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.test-loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #65676b;
|
||||
font-size: 14px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.test-result {
|
||||
background: #f0f2f5;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.test-result h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1c1e21;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
|
||||
.test-comment {
|
||||
background: white;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #1c1e21;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.test-error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 14px 18px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.settings-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal__content {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
202
web/settings.html
Normal file
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Einstellungen - Facebook Post Tracker</title>
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="assets/app-icon-192.png">
|
||||
<link rel="apple-touch-icon" href="assets/app-icon-192.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link rel="stylesheet" href="settings.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="header-main">
|
||||
<h1>⚙️ Einstellungen</h1>
|
||||
<a href="index.html" class="btn btn-secondary">Zurück zu Beiträgen</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="loading" class="loading" style="display: none;">Lade Einstellungen...</div>
|
||||
<div id="error" class="error" style="display: none;"></div>
|
||||
<div id="success" class="success" style="display: none;"></div>
|
||||
|
||||
<div class="settings-container">
|
||||
|
||||
<!-- AI Credentials Section -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">AI-Anmeldedaten</h2>
|
||||
<p class="section-description">
|
||||
Verwalte deine API-Schlüssel für verschiedene AI-Provider. Du kannst mehrere Credentials speichern und schnell zwischen ihnen wechseln.
|
||||
</p>
|
||||
|
||||
<div id="credentialsList" class="credentials-list"></div>
|
||||
|
||||
<button type="button" class="btn btn-primary" id="addCredentialBtn">
|
||||
➕ Neue Anmeldedaten hinzufügen
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- AI Settings Section -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">AI-Kommentar-Generator</h2>
|
||||
<p class="section-description">
|
||||
Konfiguriere die automatische Generierung von Kommentaren durch KI.
|
||||
</p>
|
||||
|
||||
<form id="aiSettingsForm">
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<input type="checkbox" id="aiEnabled" class="form-checkbox">
|
||||
<span>AI-Kommentar-Generator aktivieren</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="activeCredential" class="form-label">Aktive Anmeldedaten</label>
|
||||
<select id="activeCredential" class="form-select">
|
||||
<option value="">-- Bitte wählen --</option>
|
||||
</select>
|
||||
<p class="form-help">
|
||||
Wähle welche API-Anmeldedaten verwendet werden sollen
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="aiPromptPrefix" class="form-label">Prompt-Präfix</label>
|
||||
<textarea id="aiPromptPrefix" class="form-textarea" rows="4"
|
||||
placeholder="Anweisungen für die KI vor dem Post-Text..."></textarea>
|
||||
<p class="form-help">
|
||||
Dieser Text wird vor dem eigentlichen Post-Text an die KI gesendet. Verwende <code>{FREUNDE}</code> als Platzhalter für Freundesnamen.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Profile Friends Section -->
|
||||
<section class="settings-section">
|
||||
<h2 class="section-title">👥 Freundesnamen pro Profil</h2>
|
||||
<p class="section-description">
|
||||
Gib für jedes Profil eine Liste von Freundesnamen an, die im Prompt verwendet werden können.
|
||||
</p>
|
||||
|
||||
<div id="profileFriendsList"></div>
|
||||
</section>
|
||||
|
||||
<!-- Save Button at the end -->
|
||||
<section class="settings-section">
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-primary" id="saveAllBtn">
|
||||
💾 Einstellungen speichern
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="testBtn">
|
||||
🧪 Kommentar testen
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Credential Modal -->
|
||||
<div id="credentialModal" class="modal" hidden>
|
||||
<div class="modal__backdrop"></div>
|
||||
<div class="modal__content" role="dialog" aria-modal="true">
|
||||
<button type="button" id="credentialModalClose" class="modal__close">×</button>
|
||||
<h2 class="modal__title" id="credentialModalTitle">Anmeldedaten hinzufügen</h2>
|
||||
|
||||
<form id="credentialForm">
|
||||
<input type="hidden" id="credentialId">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="credentialName" class="form-label">Name *</label>
|
||||
<input type="text" id="credentialName" class="form-input" placeholder="z.B. Mein Gemini Key" required>
|
||||
<p class="form-help">Gib einen eindeutigen Namen für diese Anmeldedaten an</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="credentialProvider" class="form-label">Provider *</label>
|
||||
<select id="credentialProvider" class="form-select" required>
|
||||
<option value="gemini">Google Gemini</option>
|
||||
<option value="claude">Anthropic Claude</option>
|
||||
<option value="openai">OpenAI / ChatGPT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="credentialApiKey" class="form-label">API-Schlüssel</label>
|
||||
<input type="password" id="credentialApiKey" class="form-input" placeholder="sk-...">
|
||||
<p class="form-help" id="credentialApiKeyHelp"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="credentialBaseUrlGroup" style="display: none;">
|
||||
<label for="credentialBaseUrl" class="form-label">Basis-URL</label>
|
||||
<input type="text" id="credentialBaseUrl" class="form-input" placeholder="https://api.openai.com/v1">
|
||||
<p class="form-help" id="credentialBaseUrlHelp"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="credentialModel" class="form-label">Modell</label>
|
||||
<input type="text" id="credentialModel" class="form-input" list="credentialModelOptions"
|
||||
placeholder="z.B. gpt-4o-mini">
|
||||
<datalist id="credentialModelOptions"></datalist>
|
||||
<p class="form-help" id="credentialModelHelp"></p>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
<button type="button" class="btn btn-secondary" id="credentialCancelBtn">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Modal -->
|
||||
<div id="testModal" class="modal" hidden>
|
||||
<div class="modal__backdrop"></div>
|
||||
<div class="modal__content" role="dialog" aria-modal="true">
|
||||
<button type="button" id="testModalClose" class="modal__close">×</button>
|
||||
<h2 class="modal__title">Kommentar-Generator testen</h2>
|
||||
|
||||
<div class="modal__body">
|
||||
<div class="form-group">
|
||||
<label for="testProfileNumber" class="form-label">Test mit Profil</label>
|
||||
<select id="testProfileNumber" class="form-select">
|
||||
<option value="1">Profil 1</option>
|
||||
<option value="2">Profil 2</option>
|
||||
<option value="3">Profil 3</option>
|
||||
<option value="4">Profil 4</option>
|
||||
<option value="5">Profil 5</option>
|
||||
</select>
|
||||
<p class="form-help">Wähle ein Profil, um die Freundesnamen zu testen</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="testPostText" class="form-label">Test Post-Text</label>
|
||||
<textarea id="testPostText" class="form-textarea" rows="4"
|
||||
placeholder="Füge hier einen Beispiel-Post-Text ein..."></textarea>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-primary" id="generateTestComment">
|
||||
✨ Kommentar generieren
|
||||
</button>
|
||||
|
||||
<div id="testLoading" class="test-loading" style="display: none;">
|
||||
Generiere Kommentar...
|
||||
</div>
|
||||
|
||||
<div id="testResult" class="test-result" style="display: none;">
|
||||
<h3>Generierter Kommentar:</h3>
|
||||
<div id="testComment" class="test-comment"></div>
|
||||
</div>
|
||||
|
||||
<div id="testError" class="test-error" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
656
web/settings.js
Normal file
@@ -0,0 +1,656 @@
|
||||
const API_URL = 'https://fb.srv.medeba-media.de/api';
|
||||
|
||||
const PROVIDER_MODELS = {
|
||||
gemini: [
|
||||
{ value: '', label: 'Standard (gemini-2.0-flash-exp)' },
|
||||
{ value: 'gemini-2.0-flash-exp', label: 'Gemini 2.0 Flash' },
|
||||
{ value: 'gemini-1.5-flash', label: 'Gemini 1.5 Flash' },
|
||||
{ value: 'gemini-1.5-pro', label: 'Gemini 1.5 Pro' }
|
||||
],
|
||||
claude: [
|
||||
{ value: '', label: 'Standard (claude-3-5-haiku)' },
|
||||
{ value: 'claude-3-5-haiku-20241022', label: 'Claude 3.5 Haiku (schnell)' },
|
||||
{ value: 'claude-3-5-sonnet-20241022', label: 'Claude 3.5 Sonnet (beste Qualität)' },
|
||||
{ value: 'claude-3-opus-20240229', label: 'Claude 3 Opus' }
|
||||
],
|
||||
openai: [
|
||||
{ value: '', label: 'Standard (gpt-3.5-turbo)' },
|
||||
{ value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo (günstig)' },
|
||||
{ value: 'gpt-4o-mini', label: 'GPT-4o Mini' },
|
||||
{ value: 'gpt-4o', label: 'GPT-4o' },
|
||||
{ value: 'gpt-4-turbo', label: 'GPT-4 Turbo' }
|
||||
]
|
||||
};
|
||||
|
||||
const PROVIDER_INFO = {
|
||||
gemini: {
|
||||
name: 'Google Gemini',
|
||||
apiKeyLink: 'https://aistudio.google.com/app/apikey',
|
||||
apiKeyHelp: 'API-Schlüssel erforderlich. Erstelle ihn im Google AI Studio.'
|
||||
},
|
||||
claude: {
|
||||
name: 'Anthropic Claude',
|
||||
apiKeyLink: 'https://console.anthropic.com/settings/keys',
|
||||
apiKeyHelp: 'API-Schlüssel erforderlich. Erstelle ihn in der Anthropic Console.'
|
||||
},
|
||||
openai: {
|
||||
name: 'OpenAI',
|
||||
apiKeyLink: 'https://platform.openai.com/api-keys',
|
||||
apiKeyHelp: 'Für lokale OpenAI-kompatible Server (z.B. Ollama) kannst du den Schlüssel leer lassen.'
|
||||
}
|
||||
};
|
||||
|
||||
let credentials = [];
|
||||
let currentSettings = null;
|
||||
|
||||
function apiFetch(url, options = {}) {
|
||||
return fetch(url, {...options, credentials: 'include'});
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
background: ${type === 'error' ? '#e74c3c' : type === 'success' ? '#42b72a' : '#1877f2'};
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
z-index: 999999;
|
||||
max-width: 350px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
toast.textContent = message;
|
||||
|
||||
if (!document.getElementById('settings-toast-styles')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'settings-toast-styles';
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slideOut {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(400px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.3s ease-out';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function showError(msg) { showToast(msg, 'error'); }
|
||||
function showSuccess(msg) { showToast(msg, 'success'); }
|
||||
|
||||
async function loadCredentials() {
|
||||
const res = await apiFetch(`${API_URL}/ai-credentials`);
|
||||
if (!res.ok) throw new Error('Failed to load credentials');
|
||||
credentials = await res.json();
|
||||
renderCredentials();
|
||||
updateActiveCredentialSelect();
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const res = await apiFetch(`${API_URL}/ai-settings`);
|
||||
if (!res.ok) throw new Error('Failed to load settings');
|
||||
currentSettings = await res.json();
|
||||
document.getElementById('aiEnabled').checked = currentSettings.enabled === 1;
|
||||
document.getElementById('activeCredential').value = currentSettings.active_credential_id || '';
|
||||
document.getElementById('aiPromptPrefix').value = currentSettings.prompt_prefix ||
|
||||
'Schreibe einen freundlichen, authentischen Kommentar auf Deutsch zu folgendem Facebook-Post. Der Kommentar soll natürlich wirken und maximal 2-3 Sätze lang sein:\n\n';
|
||||
}
|
||||
|
||||
function renderCredentials() {
|
||||
const list = document.getElementById('credentialsList');
|
||||
if (!credentials.length) {
|
||||
list.innerHTML = '<p class="empty-state">Noch keine Anmeldedaten gespeichert</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = credentials.map((c, index) => {
|
||||
const providerName = escapeHtml(PROVIDER_INFO[c.provider]?.name || c.provider);
|
||||
const modelLabel = c.model ? ` · ${escapeHtml(c.model)}` : '';
|
||||
const endpointLabel = c.base_url ? ` · ${escapeHtml(c.base_url)}` : '';
|
||||
|
||||
return `
|
||||
<div class="credential-item" draggable="true" data-credential-id="${c.id}" data-index="${index}">
|
||||
<div class="credential-item__drag-handle" title="Ziehen zum Sortieren">⋮⋮</div>
|
||||
<div class="credential-item__info">
|
||||
<label class="form-label" style="display: flex; align-items: center; gap: 8px; margin: 0;">
|
||||
<input type="checkbox" class="form-checkbox"
|
||||
${c.is_active ? 'checked' : ''}
|
||||
onchange="toggleCredentialActive(${c.id}, this.checked)">
|
||||
<div>
|
||||
<div class="credential-item__name">${escapeHtml(c.name)}</div>
|
||||
<div class="credential-item__provider">${providerName}${modelLabel}${endpointLabel}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="credential-item__actions">
|
||||
<button onclick="editCredential(${c.id})" class="btn-icon" title="Bearbeiten">✏️</button>
|
||||
<button onclick="deleteCredential(${c.id})" class="btn-icon" title="Löschen">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Add drag and drop event listeners
|
||||
setupDragAndDrop();
|
||||
}
|
||||
|
||||
function updateActiveCredentialSelect() {
|
||||
const select = document.getElementById('activeCredential');
|
||||
select.innerHTML = '<option value="">-- Bitte wählen --</option>' +
|
||||
credentials.map(c => `<option value="${c.id}">${escapeHtml(c.name)} (${PROVIDER_INFO[c.provider]?.name})</option>`).join('');
|
||||
if (currentSettings?.active_credential_id) {
|
||||
select.value = currentSettings.active_credential_id;
|
||||
}
|
||||
}
|
||||
|
||||
function updateModelOptions(provider) {
|
||||
const modelInput = document.getElementById('credentialModel');
|
||||
const modelList = document.getElementById('credentialModelOptions');
|
||||
const apiKeyInput = document.getElementById('credentialApiKey');
|
||||
const baseUrlGroup = document.getElementById('credentialBaseUrlGroup');
|
||||
const baseUrlHelp = document.getElementById('credentialBaseUrlHelp');
|
||||
const baseUrlInput = document.getElementById('credentialBaseUrl');
|
||||
const info = PROVIDER_INFO[provider];
|
||||
|
||||
const models = PROVIDER_MODELS[provider] || [];
|
||||
if (modelList) {
|
||||
modelList.innerHTML = models.map(m => `<option value="${m.value}">${m.label}</option>`).join('');
|
||||
}
|
||||
|
||||
if (modelInput) {
|
||||
const firstSuggestion = models.find(m => m.value)?.value;
|
||||
modelInput.placeholder = firstSuggestion
|
||||
? `z.B. ${firstSuggestion}`
|
||||
: 'Modell-ID (z.B. llama3.1)';
|
||||
}
|
||||
|
||||
const help = document.getElementById('credentialApiKeyHelp');
|
||||
if (help) {
|
||||
if (info) {
|
||||
const parts = [];
|
||||
if (info.apiKeyHelp) {
|
||||
parts.push(info.apiKeyHelp);
|
||||
}
|
||||
if (info.apiKeyLink) {
|
||||
parts.push(`<a href="${info.apiKeyLink}" target="_blank">API-Schlüssel erstellen</a>`);
|
||||
}
|
||||
help.innerHTML = parts.join(' ');
|
||||
} else {
|
||||
help.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (apiKeyInput) {
|
||||
if (provider === 'openai') {
|
||||
apiKeyInput.placeholder = 'sk-... oder leer für lokale Server';
|
||||
} else {
|
||||
apiKeyInput.placeholder = 'API-Schlüssel';
|
||||
}
|
||||
}
|
||||
|
||||
if (baseUrlGroup && baseUrlHelp) {
|
||||
if (provider === 'openai') {
|
||||
baseUrlGroup.style.display = 'block';
|
||||
baseUrlHelp.textContent = 'Leer lassen für die offizielle OpenAI-API. Für lokale OpenAI/Ollama-Server gib die Basis-URL an, z.B. http://localhost:11434/v1';
|
||||
if (baseUrlInput) {
|
||||
baseUrlInput.placeholder = 'https://api.openai.com/v1 oder http://localhost:11434/v1';
|
||||
}
|
||||
} else {
|
||||
baseUrlGroup.style.display = 'none';
|
||||
baseUrlHelp.textContent = '';
|
||||
if (baseUrlInput) {
|
||||
baseUrlInput.placeholder = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const modelHelp = document.getElementById('credentialModelHelp');
|
||||
if (modelHelp) {
|
||||
modelHelp.textContent = 'Trage die Modell-ID ein. Du kannst einen Vorschlag auswählen oder einen eigenen Wert eingeben.';
|
||||
}
|
||||
}
|
||||
|
||||
function openCredentialModal(credential = null) {
|
||||
const modal = document.getElementById('credentialModal');
|
||||
const form = document.getElementById('credentialForm');
|
||||
const apiKeyInput = document.getElementById('credentialApiKey');
|
||||
const baseUrlInput = document.getElementById('credentialBaseUrl');
|
||||
|
||||
if (credential) {
|
||||
document.getElementById('credentialModalTitle').textContent = 'Anmeldedaten bearbeiten';
|
||||
document.getElementById('credentialId').value = credential.id;
|
||||
document.getElementById('credentialName').value = credential.name;
|
||||
document.getElementById('credentialProvider').value = credential.provider;
|
||||
updateModelOptions(credential.provider);
|
||||
document.getElementById('credentialModel').value = credential.model || '';
|
||||
if (baseUrlInput) {
|
||||
baseUrlInput.value = credential.base_url || '';
|
||||
}
|
||||
if (apiKeyInput) {
|
||||
apiKeyInput.value = '';
|
||||
apiKeyInput.placeholder = credential.provider === 'openai'
|
||||
? 'Leer lassen, um den bestehenden Schlüssel zu behalten'
|
||||
: 'Leer lassen, um den bestehenden Schlüssel zu behalten';
|
||||
}
|
||||
} else {
|
||||
document.getElementById('credentialModalTitle').textContent = 'Anmeldedaten hinzufügen';
|
||||
form.reset();
|
||||
updateModelOptions('gemini');
|
||||
document.getElementById('credentialId').value = '';
|
||||
if (apiKeyInput) {
|
||||
apiKeyInput.value = '';
|
||||
apiKeyInput.placeholder = 'API-Schlüssel';
|
||||
}
|
||||
if (baseUrlInput) {
|
||||
baseUrlInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
modal.removeAttribute('hidden');
|
||||
}
|
||||
|
||||
function closeCredentialModal() {
|
||||
document.getElementById('credentialModal').setAttribute('hidden', '');
|
||||
}
|
||||
|
||||
async function saveCredential(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const id = document.getElementById('credentialId').value;
|
||||
const name = document.getElementById('credentialName').value.trim();
|
||||
const provider = document.getElementById('credentialProvider').value;
|
||||
const apiKey = document.getElementById('credentialApiKey').value.trim();
|
||||
const model = document.getElementById('credentialModel').value.trim();
|
||||
const baseUrlRaw = document.getElementById('credentialBaseUrl')?.value.trim() || '';
|
||||
|
||||
if (!name) {
|
||||
throw new Error('Bitte einen Namen angeben');
|
||||
}
|
||||
|
||||
const data = {
|
||||
name,
|
||||
provider,
|
||||
model: model || null,
|
||||
base_url: provider === 'openai' ? baseUrlRaw : ''
|
||||
};
|
||||
|
||||
if (!id) {
|
||||
if (!apiKey && !(provider === 'openai' && baseUrlRaw)) {
|
||||
throw new Error('API-Schlüssel ist erforderlich (oder Basis-URL für lokale OpenAI-kompatible Server angeben)');
|
||||
}
|
||||
data.api_key = apiKey;
|
||||
} else if (apiKey) {
|
||||
data.api_key = apiKey;
|
||||
}
|
||||
|
||||
const url = id ? `${API_URL}/ai-credentials/${id}` : `${API_URL}/ai-credentials`;
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
|
||||
const res = await apiFetch(url, {
|
||||
method,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Fehler beim Speichern der Anmeldedaten');
|
||||
}
|
||||
|
||||
await loadCredentials();
|
||||
closeCredentialModal();
|
||||
showSuccess('✅ Anmeldedaten erfolgreich gespeichert');
|
||||
} catch (err) {
|
||||
showError('❌ ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function editCredential(id) {
|
||||
const cred = credentials.find(c => c.id === id);
|
||||
if (!cred) {
|
||||
showError('Anmeldedaten nicht gefunden');
|
||||
return;
|
||||
}
|
||||
|
||||
openCredentialModal(cred);
|
||||
}
|
||||
|
||||
async function toggleCredentialActive(id, isActive) {
|
||||
try {
|
||||
const res = await apiFetch(`${API_URL}/ai-credentials/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ is_active: isActive ? 1 : 0 })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Fehler beim Aktualisieren');
|
||||
}
|
||||
|
||||
await loadCredentials();
|
||||
showSuccess(`✅ Login ${isActive ? 'aktiviert' : 'deaktiviert'}`);
|
||||
} catch (err) {
|
||||
showError('❌ ' + err.message);
|
||||
await loadCredentials(); // Reload to reset checkbox
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCredential(id) {
|
||||
if (!confirm('Wirklich löschen?')) return;
|
||||
|
||||
const res = await apiFetch(`${API_URL}/ai-credentials/${id}`, {method: 'DELETE'});
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Failed to delete');
|
||||
}
|
||||
|
||||
await loadCredentials();
|
||||
showSuccess('Anmeldedaten gelöscht');
|
||||
}
|
||||
|
||||
// Make function globally accessible
|
||||
window.toggleCredentialActive = toggleCredentialActive;
|
||||
|
||||
// ============================================================================
|
||||
// DRAG AND DROP
|
||||
// ============================================================================
|
||||
|
||||
let draggedElement = null;
|
||||
|
||||
function setupDragAndDrop() {
|
||||
const items = document.querySelectorAll('.credential-item');
|
||||
|
||||
items.forEach(item => {
|
||||
item.addEventListener('dragstart', handleDragStart);
|
||||
item.addEventListener('dragover', handleDragOver);
|
||||
item.addEventListener('drop', handleDrop);
|
||||
item.addEventListener('dragend', handleDragEnd);
|
||||
item.addEventListener('dragenter', handleDragEnter);
|
||||
item.addEventListener('dragleave', handleDragLeave);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDragStart(e) {
|
||||
draggedElement = this;
|
||||
this.style.opacity = '0.4';
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/html', this.innerHTML);
|
||||
}
|
||||
|
||||
function handleDragOver(e) {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleDragEnter(e) {
|
||||
if (this !== draggedElement) {
|
||||
this.classList.add('drag-over');
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragLeave(e) {
|
||||
this.classList.remove('drag-over');
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
if (e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
if (draggedElement !== this) {
|
||||
// Get the container
|
||||
const container = this.parentNode;
|
||||
const allItems = [...container.querySelectorAll('.credential-item')];
|
||||
|
||||
// Get indices
|
||||
const draggedIndex = allItems.indexOf(draggedElement);
|
||||
const targetIndex = allItems.indexOf(this);
|
||||
|
||||
// Reorder in DOM
|
||||
if (draggedIndex < targetIndex) {
|
||||
this.parentNode.insertBefore(draggedElement, this.nextSibling);
|
||||
} else {
|
||||
this.parentNode.insertBefore(draggedElement, this);
|
||||
}
|
||||
|
||||
// Update backend
|
||||
saveCredentialOrder();
|
||||
}
|
||||
|
||||
this.classList.remove('drag-over');
|
||||
return false;
|
||||
}
|
||||
|
||||
function handleDragEnd(e) {
|
||||
this.style.opacity = '1';
|
||||
|
||||
// Remove all drag-over classes
|
||||
document.querySelectorAll('.credential-item').forEach(item => {
|
||||
item.classList.remove('drag-over');
|
||||
});
|
||||
}
|
||||
|
||||
async function saveCredentialOrder() {
|
||||
try {
|
||||
const items = document.querySelectorAll('.credential-item');
|
||||
const order = Array.from(items).map(item => parseInt(item.dataset.credentialId));
|
||||
|
||||
const res = await apiFetch(`${API_URL}/ai-credentials/reorder`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ order })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Fehler beim Speichern der Reihenfolge');
|
||||
}
|
||||
|
||||
credentials = await res.json();
|
||||
showSuccess('✅ Reihenfolge gespeichert');
|
||||
} catch (err) {
|
||||
showError('❌ ' + err.message);
|
||||
await loadCredentials(); // Reload to restore original order
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const data = {
|
||||
enabled: document.getElementById('aiEnabled').checked,
|
||||
active_credential_id: parseInt(document.getElementById('activeCredential').value) || null,
|
||||
prompt_prefix: document.getElementById('aiPromptPrefix').value
|
||||
};
|
||||
|
||||
const res = await apiFetch(`${API_URL}/ai-settings`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Fehler beim Speichern der Einstellungen');
|
||||
}
|
||||
|
||||
currentSettings = await res.json();
|
||||
showSuccess('✅ Einstellungen erfolgreich gespeichert');
|
||||
} catch (err) {
|
||||
showError('❌ ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testComment() {
|
||||
const modal = document.getElementById('testModal');
|
||||
modal.removeAttribute('hidden');
|
||||
document.getElementById('testResult').style.display = 'none';
|
||||
document.getElementById('testError').style.display = 'none';
|
||||
|
||||
// Load last test data from localStorage
|
||||
const lastTest = localStorage.getItem('lastTestComment');
|
||||
if (lastTest) {
|
||||
try {
|
||||
const data = JSON.parse(lastTest);
|
||||
document.getElementById('testPostText').value = data.postText || '';
|
||||
document.getElementById('testProfileNumber').value = data.profileNumber || '1';
|
||||
} catch (e) {
|
||||
console.error('Failed to load last test comment:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generateTest() {
|
||||
const text = document.getElementById('testPostText').value;
|
||||
const profileNumber = parseInt(document.getElementById('testProfileNumber').value);
|
||||
|
||||
if (!text) return;
|
||||
|
||||
document.getElementById('testLoading').style.display = 'block';
|
||||
document.getElementById('testResult').style.display = 'none';
|
||||
document.getElementById('testError').style.display = 'none';
|
||||
|
||||
try {
|
||||
const res = await apiFetch(`${API_URL}/ai/generate-comment`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({postText: text, profileNumber})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Failed');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
document.getElementById('testComment').textContent = data.comment;
|
||||
document.getElementById('testResult').style.display = 'block';
|
||||
|
||||
// Save test data to localStorage
|
||||
localStorage.setItem('lastTestComment', JSON.stringify({
|
||||
postText: text,
|
||||
profileNumber: profileNumber,
|
||||
comment: data.comment,
|
||||
timestamp: new Date().toISOString()
|
||||
}));
|
||||
} catch (err) {
|
||||
document.getElementById('testError').textContent = err.message;
|
||||
document.getElementById('testError').style.display = 'block';
|
||||
} finally {
|
||||
document.getElementById('testLoading').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROFILE FRIENDS
|
||||
// ============================================================================
|
||||
|
||||
let profileFriends = {};
|
||||
|
||||
async function loadProfileFriends() {
|
||||
const list = document.getElementById('profileFriendsList');
|
||||
list.innerHTML = '';
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const res = await apiFetch(`${API_URL}/profile-friends/${i}`);
|
||||
const data = await res.json();
|
||||
profileFriends[i] = data.friend_names || '';
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'form-group';
|
||||
div.innerHTML = `
|
||||
<label for="friends${i}" class="form-label">Profil ${i}</label>
|
||||
<input type="text" id="friends${i}" class="form-input"
|
||||
placeholder="z.B. Anna, Max, Lisa"
|
||||
value="${escapeHtml(profileFriends[i])}">
|
||||
<p class="form-help">Kommagetrennte Liste von Freundesnamen für Profil ${i}</p>
|
||||
`;
|
||||
list.appendChild(div);
|
||||
|
||||
document.getElementById(`friends${i}`).addEventListener('blur', async (e) => {
|
||||
const newValue = e.target.value.trim();
|
||||
if (newValue !== profileFriends[i]) {
|
||||
await saveFriends(i, newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFriends(profileNumber, friendNames) {
|
||||
try {
|
||||
const res = await apiFetch(`${API_URL}/profile-friends/${profileNumber}`, {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ friend_names: friendNames })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.error || 'Fehler beim Speichern');
|
||||
}
|
||||
|
||||
profileFriends[profileNumber] = friendNames;
|
||||
showSuccess(`✅ Freunde für Profil ${profileNumber} gespeichert`);
|
||||
} catch (err) {
|
||||
showError('❌ ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('addCredentialBtn').addEventListener('click', () => openCredentialModal());
|
||||
document.getElementById('credentialModalClose').addEventListener('click', closeCredentialModal);
|
||||
document.getElementById('credentialCancelBtn').addEventListener('click', closeCredentialModal);
|
||||
document.getElementById('credentialForm').addEventListener('submit', saveCredential);
|
||||
document.getElementById('credentialProvider').addEventListener('change', e => updateModelOptions(e.target.value));
|
||||
document.getElementById('aiSettingsForm').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
saveSettings(e);
|
||||
});
|
||||
document.getElementById('saveAllBtn').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
saveSettings(e);
|
||||
});
|
||||
document.getElementById('testBtn').addEventListener('click', testComment);
|
||||
document.getElementById('testModalClose').addEventListener('click', () => document.getElementById('testModal').setAttribute('hidden', ''));
|
||||
document.getElementById('generateTestComment').addEventListener('click', generateTest);
|
||||
|
||||
// Initialize
|
||||
Promise.all([loadCredentials(), loadSettings(), loadProfileFriends()]).catch(err => showError(err.message));
|
||||