from flask import Flask, jsonify, request, render_template_string
import subprocess, json
app = Flask(__name__)
SERVICES = [
{
'id': 'home-scout',
'name': 'Home Scout',
'desc': 'Startet die blaue Hermes-Scout-Desktop-Version.',
'icon': '🛰️',
'url': 'hermes-scout://start',
'container': None,
'compose': None,
'external': True,
},
{'id': 'vpshost', 'name': 'VPS Host', 'desc': 'Host-Kontrolle: Commander, Docker, systemd, Logs, IONOS, n8n, LiteLLM, Portainer, Explorer, Security, Backups.', 'icon': '🖥', 'url': 'https://vpshost.agentsolutions-mallorca.com', 'container': None, 'compose': None, 'external': True},
{
'id': 'hermes-vps',
'name': 'Hermes VPS',
'desc': 'Zentrale Hermes-VPS-Rolle. Oeffnet das zentrale Profil unter /opt/hermes-commander-next-data.',
'icon': '⚡',
'url': 'https://hermes-root.agentsolutions-mallorca.com',
'container': None,
'compose': None,
'external': True,
},
{
'id': 'hermes-apollo',
'name': 'Hermes Apollo',
'desc': 'Realtime Voice, Daily/Weekly Briefing und Wall Mode auf dem bestehenden Hermes.',
'icon': '◉',
'url': 'https://projects.agentsolutions-mallorca.com/apollo',
'container': None,
'compose': None,
'external': True,
}, {
'id': 'hermes-carlo',
'name': 'Hermes — Carlo',
'desc': 'Hermes-VPS-Profil Carlo. Oeffnet HERMES_HOME /var/lib/hermes/carlo.',
'icon': '⚡',
'url': 'https://hermes-carlo.agentsolutions-mallorca.com',
'container': 'hermes-carlo',
'persona_url': 'https://hermes-carlo.agentsolutions-mallorca.com/personas/?instance=carlo',
'compose': '/opt/struktur/hermes-carlo',
},
{
'id': 'ionos',
'name': 'IONOS Cloud Panel',
'desc': 'VPS Verwaltung, DNS, Server-Einstellungen.',
'icon': '🖥️',
'url': 'https://my.ionos.de',
'container': None,
'compose': None,
'external': True,
},
{
'id': 'mirofish',
'name': 'MiroFish',
'desc': 'Multi-Agenten-Simulation zur Zukunftsvorhersage und Marktforschung.',
'icon': '🐟',
'url': 'https://mirofish.agentsolutions-mallorca.com',
'container': 'mirofish',
'compose': '/opt/struktur/mirofish',
},
{
'id': 'explorer',
'name': 'Explorer',
'desc': 'Dateizugriff auf alle VPS-Verzeichnisse und Dateien. (INAKTIV)',
'icon': '📁',
'url': 'https://explorer.agentsolutions-mallorca.com',
'container': 'explorer',
'compose': None,
},
{
'id': 'n8n',
'name': 'n8n',
'desc': 'Workflow-Automatisierung. Verbindet Apps, APIs und KI-Agenten.',
'icon': '🔗',
'url': 'https://n8n.agentsolutions-mallorca.com',
'container': 'n8n',
'compose': None,
},
{
'id': 'graphiti',
'name': 'Graphiti',
'desc': 'Zentrales Wissensgehirn. Knowledge Graph fuer alle Agenten und Datenquellen.',
'icon': '🕸️',
'url': 'https://graphiti.agentsolutions-mallorca.com/docs',
'container': 'graphiti-service',
'compose': '/opt/struktur/graphiti',
},
{
'id': 'litellm',
'name': 'LiteLLM Gateway',
'desc': 'KI-Router: Free-Modelle zuerst, Paid als Fallback. Kostenkontrolle fuer alle Agenten. (INAKTIV)',
'icon': '🔀',
'url': 'https://litellm.agentsolutions-mallorca.com/ui',
'container': 'litellm',
'compose': '/opt/struktur/litellm',
},
{
'id': 'airtable',
'name': 'Airtable — Datenbank',
'desc': 'Ingest Service Datenbank. Kunden, Projekte und analysierte Daten.',
'icon': '🗄️',
'url': 'https://airtable.com/appAlQPqFgQwjFnTu',
'container': None,
'compose': None,
'external': True,
}, {
'id': 'paperclip',
'name': 'Paperclip',
'desc': 'Agent-Orchestrator. CEO-Agent mit DeepSeek via OpenRouter.',
'icon': '📎',
'url': 'https://paperclip.agentsolutions-mallorca.com',
'container': 'docker-paperclip-1',
'compose': '/opt/struktur/paperclip/docker',
},
{
'id': 'portainer',
'name': 'Portainer',
'desc': 'Docker Container Management. Start, Stop, Logs.',
'icon': '🐳',
'url': 'http://127.0.0.1:9000',
'container': 'portainer',
'compose': None,
'external': False,
},
]
def get_status(container):
try:
result = subprocess.run(
['docker', 'inspect', '--format', '{{.State.Status}}', container],
capture_output=True, text=True, timeout=5
)
status = result.stdout.strip()
if status == 'running':
return 'online'
elif status == 'exited':
return 'offline'
else:
return status or 'unbekannt'
except:
return 'unbekannt'
def docker_action(service, action):
if service['compose']:
compose_dir = service['compose']
if action == 'start':
cmd = f"cd {compose_dir} && docker compose up -d"
elif action == 'stop':
cmd = f"cd {compose_dir} && docker compose stop"
elif action == 'restart':
cmd = f"cd {compose_dir} && docker compose restart"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
else:
result = subprocess.run(
['docker', action, service['container']],
capture_output=True, text=True, timeout=30
)
return result.returncode == 0
HTML = '''<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Agent Solutions Workspace</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0f1117; color: #e2e8f0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; min-height: 100vh; }
header { padding: 2rem 3rem; border-bottom: 1px solid #1e2330; }
header h1 { font-size: 1.5rem; font-weight: 600; color: #fff; }
header span { font-size: 0.85rem; color: #fff; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; padding: 3rem; }
.card { background: #161b27; border: 1px solid #1e2330; border-radius: 12px; padding: 1.5rem; }
.card-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 1rem; }
.card-icon { font-size: 2rem; }
.badge { font-size: 0.7rem; padding: 0.25rem 0.7rem; border-radius: 99px; font-weight: 600; }
.badge.online { background: #1a3a2a; color: #4ade80; border: 1px solid #166534; }
.badge.offline { background: #3a1a1a; color: #f87171; border: 1px solid #7f1d1d; }
.badge.loading { background: #2a2a1a; color: #fbbf24; border: 1px solid #78350f; }
.badge.planned { background: #1a2a3a; color: #93c5fd; border: 1px solid #1e3a5f; }
.card-title { font-size: 1.1rem; font-weight: 600; color: #fff; margin-bottom: 0.4rem; }
.card-desc { font-size: 0.85rem; color: #fff; line-height: 1.5; margin-bottom: 1.2rem; }
.card-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.btn { font-size: 0.75rem; padding: 0.4rem 0.9rem; border-radius: 6px; border: none; cursor: pointer; font-weight: 500; transition: all 0.15s; }
.btn-open { background: #1e3a5f; color: #60a5fa; }
.btn-open:hover { background: #1e4a7f; }
.btn-restart { background: #1e3a2a; color: #4ade80; }
.btn-restart:hover { background: #1e5a3a; }
.btn-stop { background: #3a1e1e; color: #f87171; }
.btn-stop:hover { background: #5a1e1e; }
.btn-start { background: #1a3a2a; color: #4ade80; }
.btn-start:hover { background: #1a5a3a; }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:1000; align-items:center; justify-content:center; }
.modal-overlay.visible { display:flex; }
.modal { background:#1e2330; border:1px solid #2d3748; border-radius:14px; padding:2rem; min-width:420px; max-width:95vw; }
.modal h2 { font-size:1.1rem; font-weight:700; color:#fff; margin-bottom:1.2rem; }
.modal select { width:100%; padding:0.5rem 0.75rem; background:#0f1117; border:1px solid #2d3748; border-radius:7px; color:#e2e8f0; font-size:0.9rem; margin-bottom:1rem; }
.modal-actions { display:flex; gap:0.75rem; justify-content:flex-end; margin-top:0.5rem; }
.btn-cancel { background:#1e2330; border:1px solid #2d3748; color:#fff; padding:0.45rem 1.1rem; border-radius:7px; cursor:pointer; font-size:0.85rem; }
.btn-save { background:#1e3a5f; color:#60a5fa; border:none; padding:0.45rem 1.1rem; border-radius:7px; cursor:pointer; font-size:0.85rem; font-weight:600; }
.current-model { font-size:0.8rem; color:#fff; margin-bottom:1rem; }
footer { text-align: center; padding: 2rem; color: #334155; font-size: 0.8rem; }
</style>
</head>
<body>
<header style="display:flex; align-items:center; justify-content:space-between;">
<div>
<h1>Agent Solutions Workspace</h1>
<span>Werkzeuge & Dienste — VPS 31.70.68.228</span>
</div>
<a href="https://agentsolutions-mallorca.com" style="font-size:0.85rem; color:#3b82f6; text-decoration:none; border:1px solid #1e3a5f; padding:0.4rem 1rem; border-radius:8px;">← Zurück</a>
</header>
<div class="grid" id="grid">
{% for s in services %}
<div class="card" id="card-{{s.id}}">
<div class="card-header">
<div class="card-icon">{{s.icon}}</div>
<span class="badge loading" id="badge-{{s.id}}">{% if s.get('external') %}extern{% elif s.get('planned') %}geplant{% else %}...{% endif %}</span>
</div>
<div class="card-title">{{s.name}}</div>
<div class="card-desc">{{s.desc}}</div>
<div class="card-actions">
<a href="{{s.url}}" target="_blank"><button class="btn btn-open">Öffnen</button></a>
{% if s.get('persona_url') %}<a href="{{s.persona_url}}" target="_blank"><button class="btn" style="background:#7c3aed;color:#fff">Personas</button></a>{% endif %}
{% if not s.get('external') %}
<button class="btn btn-restart" onclick="action('{{s.id}}','restart')">Restart</button>
<button class="btn btn-stop" onclick="action('{{s.id}}','stop')">Stop</button>
<button class="btn btn-start" onclick="action('{{s.id}}','start')">Start</button>
{% if s.id == 'hermes-carlo' %}
<button class="btn" style="background:#3a1a1a;color:#f87171;font-weight:700;" onclick="hermesJobsReset()">🗑 Jobs Reset</button>
{% endif %}
{% if s.id == 'mirofish' %}
<button class="btn" style="background:#1e2a3a;color:#a78bfa;" onclick="openMirofishModal()">🧠 Modell</button>
{% endif %}
{% endif %}
</div>
</div>
{% endfor %}
</div>
<footer>Agent Solutions · VPS IONOS · 31.70.68.228</footer>
<script>
function updateStatus() {
fetch('/api/status').then(r=>r.json()).then(data => {
data.forEach(s => {
const b = document.getElementById('badge-'+s.id);
b.textContent = s.status;
b.className = 'badge ' + (s.status === 'online' ? 'online' : s.status === 'offline' ? 'offline' : s.status === 'geplant' ? 'planned' : 'loading');
});
});
}
function action(id, act) {
const b = document.getElementById('badge-'+id);
b.textContent = '...';
b.className = 'badge loading';
fetch('/api/action', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({id, action: act})
}).then(()=> setTimeout(updateStatus, 3000));
}
updateStatus();
setInterval(updateStatus, 15000);
</script>
<div class="modal-overlay" id="mirofish-modal">
<div class="modal">
<h2>🐟 MiroFish — Modell wechseln</h2>
<div class="current-model">Aktuell: <span id="modal-current-model">...</span></div>
<select id="modal-model-select"></select>
<div class="modal-actions">
<button class="btn-cancel" onclick="closeMirofishModal()">Abbrechen</button>
<button class="btn-save" id="btn-modal-save" onclick="saveMirofishModel()">Speichern & Neustart</button>
</div>
</div>
</div>
<script>
async function openMirofishModal() {
document.getElementById('mirofish-modal').classList.add('visible');
const res = await fetch('/api/mirofish/model');
const data = await res.json();
document.getElementById('modal-current-model').textContent = data.current;
const sel = document.getElementById('modal-model-select');
sel.innerHTML = data.models.map(m =>
`<option value="${m.model}" ${m.model===data.current?'selected':''}>[${m.provider}] ${m.label}</option>`
).join('');
}
function closeMirofishModal() {
document.getElementById('mirofish-modal').classList.remove('visible');
}
async function saveMirofishModel() {
const btn = document.getElementById('btn-modal-save');
btn.disabled = true; btn.textContent = '⏳ Neustart...';
const model = document.getElementById('modal-model-select').value;
const res = await fetch('/api/mirofish/model', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({model})});
const data = await res.json();
if (data.ok) { btn.textContent = '✅ Gespeichert'; setTimeout(closeMirofishModal, 1500); }
else { btn.textContent = '❌ Fehler'; btn.disabled = false; }
}
</script>
</body>
</html>'''
@app.route('/')
def index():
return render_template_string(HTML, services=SERVICES)
@app.route('/api/status')
def status():
return jsonify([{'id': s['id'], 'status': 'geplant' if s.get('planned') else (get_status(s['container']) if s.get('container') else 'extern')} for s in SERVICES])
@app.route('/api/action', methods=['POST'])
def action():
data = request.json
service = next((s for s in SERVICES if s['id'] == data['id']), None)
if not service:
return jsonify({'ok': False}), 404
ok = docker_action(service, data['action'])
return jsonify({'ok': ok})
MIROFISH_ENV = '/opt/struktur/mirofish/.env'
MIROFISH_MODELS = [
{"provider": "OpenAI", "label": "GPT-4.1-mini (empfohlen, ~3€/15Rdn)", "model": "gpt-4.1-mini-2025-04-14", "base_url": "https://api.openai.com/v1", "api_key": "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"},
{"provider": "OpenAI", "label": "GPT-4.1 (~15€/15Rdn)", "model": "gpt-4.1-2025-04-14", "base_url": "https://api.openai.com/v1", "api_key": "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"},
{"provider": "OpenAI", "label": "GPT-4o (~30€/15Rdn)", "model": "gpt-4o", "base_url": "https://api.openai.com/v1", "api_key": "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"},
{"provider": "OpenRouter", "label": "DeepSeek-V3 (~2€/15Rdn)", "model": "deepseek/deepseek-chat", "base_url": "https://openrouter.ai/api/v1", "api_key": "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"},
{"provider": "OpenRouter", "label": "DeepSeek-R1 (~5€/15Rdn)", "model": "deepseek/deepseek-reasoner","base_url": "https://openrouter.ai/api/v1", "api_key": "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"},
{"provider": "OpenRouter", "label": "Qwen2.5-72B (~2€/15Rdn)", "model": "qwen/qwen2.5-72b-instruct", "base_url": "https://openrouter.ai/api/v1", "api_key": "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"},
{"provider": "OpenRouter", "label": "Kimi K2 (~2€/15Rdn)", "model": "moonshotai/kimi-k2", "base_url": "https://openrouter.ai/api/v1", "api_key": "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"},
]
def _read_mirofish_model():
try:
with open(MIROFISH_ENV) as f:
for line in f:
if line.startswith('LLM_MODEL_NAME='):
return line.strip().split('=', 1)[1]
except:
pass
return 'unbekannt'
def _write_mirofish_env(model_cfg):
with open(MIROFISH_ENV) as f:
lines = f.readlines()
keys = {
'LLM_API_KEY': model_cfg['api_key'],
'LLM_BASE_URL': model_cfg['base_url'],
'LLM_MODEL_NAME': model_cfg['model'],
'OPENAI_API_KEY': model_cfg['api_key'],
'OPENAI_API_BASE_URL': model_cfg['base_url'],
}
new_lines = []
for line in lines:
replaced = False
for k, v in keys.items():
if line.startswith(k + '='):
new_lines.append(k + '=' + v + chr(10))
replaced = True
break
if not replaced:
new_lines.append(line)
with open(MIROFISH_ENV, 'w') as f:
f.writelines(new_lines)
@app.route('/api/mirofish/model', methods=['GET'])
def mirofish_model_get():
current = _read_mirofish_model()
return jsonify({'current': current, 'models': MIROFISH_MODELS})
@app.route('/api/mirofish/model', methods=['POST'])
def mirofish_model_set():
data = request.json
model_id = data.get('model')
cfg = next((m for m in MIROFISH_MODELS if m['model'] == model_id), None)
if not cfg:
return jsonify({'ok': False, 'error': 'Unbekanntes Modell'}), 400
_write_mirofish_env(cfg)
subprocess.run('cd /opt/struktur/mirofish && docker compose restart mirofish', shell=True, timeout=60)
return jsonify({'ok': True, 'model': model_id})
@app.route('/api/hermes-reset', methods=['POST'])
def hermes_reset():
import urllib.request, urllib.error
try:
req = urllib.request.Request('http://127.0.0.1:9099/reset', method='POST', data=b'')
with urllib.request.urlopen(req, timeout=10) as r:
return jsonify({'ok': True})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5050, debug=False)