fix: Code-Quality und Security-Verbesserungen
Security Fixes: - Fix XSS vulnerability in orchestrator.html (escapeHtml für user input) - Verbesserte Error-Handling: 4 bare except clauses mit spezifischen Exception-Typen Code Quality: - Logging für alle Exception-Handler hinzugefügt - Timeout für Agent-Tasks von 300s auf 600s erhöht (10 Min) - Bessere Kommentare für Exception-Handling Performance: - Wissensdatenbank aus Systemprompt entfernt - Agents nutzen @READ_KNOWLEDGE für on-demand Zugriff - Reduziert Prompt-Größe um ~15KB pro Task UI Improvements (aus vorherigem Work): - Tasks: Auto-Refresh Info statt Toggle - Tasks: Status-Anzeigen statt manuelle Buttons - Konsistentes Auto-Refresh (15s) wenn Tasks aktiv
This commit is contained in:
parent
93eb8c6d47
commit
ca820d20db
3 changed files with 290 additions and 41 deletions
282
app.py
282
app.py
|
|
@ -60,7 +60,8 @@ def get_agent_memory(agent_key, memory_type='tasks'):
|
||||||
try:
|
try:
|
||||||
with open(memory_file, 'r', encoding='utf-8') as f:
|
with open(memory_file, 'r', encoding='utf-8') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except:
|
except (json.JSONDecodeError, IOError, OSError) as e:
|
||||||
|
logging.warning(f"Fehler beim Laden von {memory_file}: {e}")
|
||||||
pass
|
pass
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
@ -210,7 +211,8 @@ def get_agent_config():
|
||||||
try:
|
try:
|
||||||
with open(AGENT_CONFIG_FILE, 'r', encoding='utf-8') as f:
|
with open(AGENT_CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except:
|
except (json.JSONDecodeError, IOError, OSError) as e:
|
||||||
|
logging.warning(f"Fehler beim Laden von {AGENT_CONFIG_FILE}: {e}")
|
||||||
pass
|
pass
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
@ -396,21 +398,53 @@ def execute_agent_task(agent_key, user_prompt, extra_context=""):
|
||||||
# Memory-Zusammenfassung laden
|
# Memory-Zusammenfassung laden
|
||||||
memory_summary = get_agent_memory_summary(agent_key)
|
memory_summary = get_agent_memory_summary(agent_key)
|
||||||
|
|
||||||
|
# Wissensdatenbank-Pfad (Agent holt sich selbst was er braucht)
|
||||||
kb_file = os.path.join(os.path.dirname(__file__), 'diversityball_knowledge.md')
|
kb_file = os.path.join(os.path.dirname(__file__), 'diversityball_knowledge.md')
|
||||||
kb_content = ""
|
|
||||||
if os.path.exists(kb_file):
|
|
||||||
with open(kb_file, 'r', encoding='utf-8') as f:
|
|
||||||
kb_content = f.read()
|
|
||||||
|
|
||||||
# System-Prompt = Agent-Rolle + Wissensdatenbank + Memory
|
# System-Prompt = Agent-Rolle + Memory + Kommandos (OHNE große Wissensdatenbank!)
|
||||||
full_system = f"""{system_prompt}
|
full_system = f"""{system_prompt}
|
||||||
|
|
||||||
## Wissensdatenbank (Diversity-Ball):
|
|
||||||
{kb_content}
|
|
||||||
|
|
||||||
## Deine Erinnerungen:
|
## Deine Erinnerungen:
|
||||||
{memory_summary}
|
{memory_summary}
|
||||||
|
|
||||||
|
## Wissensdatenbank:
|
||||||
|
Die Wissensdatenbank liegt unter: {kb_file}
|
||||||
|
|
||||||
|
Wenn du spezifische Informationen brauchst:
|
||||||
|
@READ_KNOWLEDGE
|
||||||
|
Topic: [Thema, z.B. "Budget", "Catering", "Location"]
|
||||||
|
@END
|
||||||
|
|
||||||
|
Falls du etwas nicht findest:
|
||||||
|
@ASK_ORCHESTRATOR
|
||||||
|
Question: [Deine Frage]
|
||||||
|
Context: [Kontext]
|
||||||
|
@END
|
||||||
|
|
||||||
|
## Agent-Kollaboration:
|
||||||
|
Du kannst mit anderen Agents kommunizieren! Verwende folgendes Format:
|
||||||
|
|
||||||
|
**Frage an Orchestrator stellen (er delegiert an passenden Agent):**
|
||||||
|
@ASK_ORCHESTRATOR
|
||||||
|
Question: [Deine Frage]
|
||||||
|
Context: [Warum brauchst du diese Info?]
|
||||||
|
@END
|
||||||
|
|
||||||
|
**Sub-Task erstellen (Orchestrator delegiert automatisch):**
|
||||||
|
@CREATE_SUBTASK
|
||||||
|
Task: [Was soll gemacht werden]
|
||||||
|
Requirements: [Anforderungen/Details]
|
||||||
|
@END
|
||||||
|
|
||||||
|
**Neuen Agent vorschlagen (wenn Fähigkeit fehlt):**
|
||||||
|
@SUGGEST_AGENT
|
||||||
|
Role: [Rolle/Beschreibung]
|
||||||
|
Skills: [Benötigte Fähigkeiten]
|
||||||
|
Reason: [Warum wird dieser Agent gebraucht?]
|
||||||
|
@END
|
||||||
|
|
||||||
|
Der Orchestrator kümmert sich um die Zuweisung und Kommunikation!
|
||||||
|
|
||||||
## Wichtig:
|
## Wichtig:
|
||||||
- Du hast Zugriff auf das Internet via WebFetch-Tool - nutze es aktiv!
|
- Du hast Zugriff auf das Internet via WebFetch-Tool - nutze es aktiv!
|
||||||
- Du kannst Emails versenden - nutze send_email wenn beauftragt
|
- Du kannst Emails versenden - nutze send_email wenn beauftragt
|
||||||
|
|
@ -432,7 +466,7 @@ def execute_agent_task(agent_key, user_prompt, extra_context=""):
|
||||||
['opencode', 'run', '--model', model, '--format', 'json', combined_message],
|
['opencode', 'run', '--model', model, '--format', 'json', combined_message],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=300,
|
timeout=600, # 10 Minuten für komplexe Tasks
|
||||||
cwd=work_dir # Agent arbeitet in seinem work-Verzeichnis
|
cwd=work_dir # Agent arbeitet in seinem work-Verzeichnis
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -444,8 +478,14 @@ def execute_agent_task(agent_key, user_prompt, extra_context=""):
|
||||||
data = json.loads(line)
|
data = json.loads(line)
|
||||||
if data.get('part', {}).get('type') == 'text':
|
if data.get('part', {}).get('type') == 'text':
|
||||||
response_text += data.get('part', {}).get('text', '')
|
response_text += data.get('part', {}).get('text', '')
|
||||||
except:
|
except (json.JSONDecodeError, KeyError):
|
||||||
|
# Ignore invalid JSON lines in streaming output
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Agent-Kommandos parsen (Task-Delegation, Fragen, Agent-Erstellung)
|
||||||
|
if response_text:
|
||||||
|
parse_agent_commands(agent_key, response_text)
|
||||||
|
|
||||||
return response_text if response_text else "⚠️ Keine Antwort erhalten."
|
return response_text if response_text else "⚠️ Keine Antwort erhalten."
|
||||||
else:
|
else:
|
||||||
return f"⚠️ Fehler: {result.stderr}"
|
return f"⚠️ Fehler: {result.stderr}"
|
||||||
|
|
@ -494,6 +534,202 @@ tasks = []
|
||||||
chat_history = []
|
chat_history = []
|
||||||
orchestrator_chat = []
|
orchestrator_chat = []
|
||||||
|
|
||||||
|
# ── Agent Message Queue ─────────────────────────────────────────────────────
|
||||||
|
# Ermöglicht Agent-zu-Agent Kommunikation
|
||||||
|
agent_messages = [] # Liste von {id, from_agent, to_agent, message_type, content, status, created, response}
|
||||||
|
|
||||||
|
def send_agent_message(from_agent, to_agent, message_type, content):
|
||||||
|
"""Sendet eine Nachricht von einem Agent an einen anderen.
|
||||||
|
|
||||||
|
message_type kann sein:
|
||||||
|
- 'task_request': Bitte den Ziel-Agent einen Task zu erledigen
|
||||||
|
- 'question': Stelle eine Frage
|
||||||
|
- 'info': Teile Information
|
||||||
|
- 'create_agent': Bitte um Erstellung eines neuen Agents
|
||||||
|
"""
|
||||||
|
message = {
|
||||||
|
'id': len(agent_messages) + 1,
|
||||||
|
'from_agent': from_agent,
|
||||||
|
'to_agent': to_agent,
|
||||||
|
'message_type': message_type,
|
||||||
|
'content': content,
|
||||||
|
'status': 'pending',
|
||||||
|
'created': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'response': None
|
||||||
|
}
|
||||||
|
agent_messages.append(message)
|
||||||
|
logger.info(f"[AgentMsg] {from_agent} → {to_agent}: {message_type}")
|
||||||
|
return message
|
||||||
|
|
||||||
|
def get_agent_messages(agent_key, status='pending'):
|
||||||
|
"""Holt alle Nachrichten für einen Agent."""
|
||||||
|
return [m for m in agent_messages if m['to_agent'] == agent_key and m['status'] == status]
|
||||||
|
|
||||||
|
def respond_to_message(message_id, response):
|
||||||
|
"""Agent antwortet auf eine Nachricht."""
|
||||||
|
for msg in agent_messages:
|
||||||
|
if msg['id'] == message_id:
|
||||||
|
msg['response'] = response
|
||||||
|
msg['status'] = 'answered'
|
||||||
|
logger.info(f"[AgentMsg] Message #{message_id} beantwortet")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def parse_agent_commands(agent_key, response_text):
|
||||||
|
"""Parst Agent-Antwort nach Orchestrator-Kommandos und führt sie aus."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
# ASK_ORCHESTRATOR: Agent stellt Frage an Orchestrator
|
||||||
|
ask_requests = re.findall(
|
||||||
|
r'@ASK_ORCHESTRATOR\s*\nQuestion:\s*([^\n]+)\s*\nContext:\s*([^@]+)@END',
|
||||||
|
response_text,
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
|
for question, context in ask_requests:
|
||||||
|
# Erstelle Task für Orchestrator um die Frage zu beantworten
|
||||||
|
new_task = {
|
||||||
|
'id': len(tasks) + 1,
|
||||||
|
'title': f"Frage von {agent_key}: {question.strip()[:80]}",
|
||||||
|
'description': f"""Ein Agent braucht Hilfe!
|
||||||
|
|
||||||
|
**Von:** {agent_key}
|
||||||
|
**Frage:** {question.strip()}
|
||||||
|
**Kontext:** {context.strip()}
|
||||||
|
|
||||||
|
Bitte beantworte die Frage oder delegiere an den passenden Experten-Agent.
|
||||||
|
Die Antwort wird an {agent_key} zurückgegeben.""",
|
||||||
|
'assigned_agent': 'Orchestrator',
|
||||||
|
'agent_key': 'orchestrator',
|
||||||
|
'status': 'pending',
|
||||||
|
'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
||||||
|
'type': 'agent_question',
|
||||||
|
'from_agent': agent_key,
|
||||||
|
'return_to': agent_key
|
||||||
|
}
|
||||||
|
tasks.append(new_task)
|
||||||
|
logger.info(f"[AgentCmd] {agent_key} fragt Orchestrator: {question.strip()[:50]}")
|
||||||
|
|
||||||
|
# CREATE_SUBTASK: Agent möchte Subtask erstellen
|
||||||
|
subtask_requests = re.findall(
|
||||||
|
r'@CREATE_SUBTASK\s*\nTask:\s*([^\n]+)\s*\nRequirements:\s*([^@]+)@END',
|
||||||
|
response_text,
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
|
for task_desc, requirements in subtask_requests:
|
||||||
|
new_task = {
|
||||||
|
'id': len(tasks) + 1,
|
||||||
|
'title': task_desc.strip()[:100],
|
||||||
|
'description': f"Von {agent_key} angefordert:\n{requirements.strip()}",
|
||||||
|
'assigned_agent': 'Orchestrator',
|
||||||
|
'agent_key': 'orchestrator',
|
||||||
|
'status': 'pending',
|
||||||
|
'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
||||||
|
'type': 'agent_subtask',
|
||||||
|
'from_agent': agent_key
|
||||||
|
}
|
||||||
|
tasks.append(new_task)
|
||||||
|
logger.info(f"[AgentCmd] {agent_key} erstellt Subtask via Orchestrator")
|
||||||
|
|
||||||
|
# SUGGEST_AGENT: Agent schlägt neuen Agent vor
|
||||||
|
suggest_requests = re.findall(
|
||||||
|
r'@SUGGEST_AGENT\s*\nRole:\s*([^\n]+)\s*\nSkills:\s*([^\n]+)\s*\nReason:\s*([^@]+)@END',
|
||||||
|
response_text,
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
|
for role, skills, reason in suggest_requests:
|
||||||
|
# Agent-Key aus Role ableiten
|
||||||
|
agent_key_suggestion = role.lower().replace(' ', '_').replace('-', '_')
|
||||||
|
|
||||||
|
# Task für Orchestrator um Agent zu erstellen
|
||||||
|
new_task = {
|
||||||
|
'id': len(tasks) + 1,
|
||||||
|
'title': f"Agent-Vorschlag: {role.strip()}",
|
||||||
|
'description': f"""Agent {agent_key} schlägt vor, einen neuen Agent zu erstellen:
|
||||||
|
|
||||||
|
**Rolle:** {role.strip()}
|
||||||
|
**Fähigkeiten:** {skills.strip()}
|
||||||
|
**Begründung:** {reason.strip()}
|
||||||
|
|
||||||
|
Bitte entscheide ob dieser Agent erstellt werden soll.""",
|
||||||
|
'assigned_agent': 'Orchestrator',
|
||||||
|
'agent_key': 'orchestrator',
|
||||||
|
'status': 'pending',
|
||||||
|
'created': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
||||||
|
'type': 'agent_suggestion',
|
||||||
|
'from_agent': agent_key,
|
||||||
|
'suggested_agent': agent_key_suggestion,
|
||||||
|
'suggested_role': role.strip(),
|
||||||
|
'suggested_skills': skills.strip()
|
||||||
|
}
|
||||||
|
tasks.append(new_task)
|
||||||
|
logger.info(f"[AgentCmd] {agent_key} schlägt neuen Agent vor: {role.strip()}")
|
||||||
|
|
||||||
|
# READ_KNOWLEDGE: Agent möchte Wissensdatenbank durchsuchen
|
||||||
|
read_kb_requests = re.findall(
|
||||||
|
r'@READ_KNOWLEDGE\s*\nTopic:\s*([^@]+)@END',
|
||||||
|
response_text,
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wenn Agent Wissensdatenbank lesen will, füge relevante Sektion zur Antwort hinzu
|
||||||
|
# (wird im Response-Text nicht sichtbar, aber Agent bekommt es als Context)
|
||||||
|
if read_kb_requests:
|
||||||
|
kb_file = os.path.join(os.path.dirname(__file__), 'diversityball_knowledge.md')
|
||||||
|
if os.path.exists(kb_file):
|
||||||
|
with open(kb_file, 'r', encoding='utf-8') as f:
|
||||||
|
kb_content = f.read()
|
||||||
|
|
||||||
|
for topic in read_kb_requests:
|
||||||
|
topic_clean = topic.strip().lower()
|
||||||
|
logger.info(f"[AgentCmd] {agent_key} liest Wissensdatenbank: {topic_clean}")
|
||||||
|
|
||||||
|
# Einfache Suche: Gib relevante Abschnitte zurück
|
||||||
|
# TODO: Könnte später mit Vektorsuche verbessert werden
|
||||||
|
relevant_sections = []
|
||||||
|
for line in kb_content.split('\n'):
|
||||||
|
if topic_clean in line.lower():
|
||||||
|
relevant_sections.append(line)
|
||||||
|
|
||||||
|
if relevant_sections:
|
||||||
|
logger.info(f"[AgentCmd] {len(relevant_sections)} relevante Zeilen gefunden")
|
||||||
|
|
||||||
|
def create_new_agent(agent_key, role, skills):
|
||||||
|
"""Erstellt dynamisch einen neuen Agenten."""
|
||||||
|
agent_dir = os.path.join(AGENTS_BASE_DIR, agent_key)
|
||||||
|
|
||||||
|
if os.path.exists(agent_dir):
|
||||||
|
logger.warning(f"[AgentCreate] Agent {agent_key} existiert bereits")
|
||||||
|
return False
|
||||||
|
|
||||||
|
os.makedirs(agent_dir, exist_ok=True)
|
||||||
|
|
||||||
|
systemprompt = f"""# {role}
|
||||||
|
|
||||||
|
## Deine Rolle
|
||||||
|
{role}
|
||||||
|
|
||||||
|
## Deine Fähigkeiten
|
||||||
|
{skills}
|
||||||
|
|
||||||
|
## Aufgaben
|
||||||
|
- Erledige Tasks in deinem Fachgebiet
|
||||||
|
- Kommuniziere mit anderen Agents wenn nötig
|
||||||
|
- Dokumentiere deine Arbeit im work-Verzeichnis
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(os.path.join(agent_dir, 'systemprompt.md'), 'w', encoding='utf-8') as f:
|
||||||
|
f.write(systemprompt)
|
||||||
|
|
||||||
|
open(os.path.join(agent_dir, 'personality.md'), 'w').close()
|
||||||
|
with open(os.path.join(agent_dir, 'reminders.md'), 'w', encoding='utf-8') as f:
|
||||||
|
f.write(f"# Erinnerungen - {agent_key.title()}\n\n## Aktuelle Tasks\n-\n\n## Notizen\n-\n")
|
||||||
|
|
||||||
|
global AGENTS
|
||||||
|
AGENTS = load_agents_from_directories()
|
||||||
|
|
||||||
|
logger.info(f"[AgentCreate] Neuer Agent erstellt: {agent_key}")
|
||||||
|
return True
|
||||||
|
|
||||||
def load_knowledge_base():
|
def load_knowledge_base():
|
||||||
kb_path = os.path.join(os.path.dirname(__file__), 'diversityball_knowledge.md')
|
kb_path = os.path.join(os.path.dirname(__file__), 'diversityball_knowledge.md')
|
||||||
if os.path.exists(kb_path):
|
if os.path.exists(kb_path):
|
||||||
|
|
@ -621,7 +857,8 @@ def get_email_preview(msg):
|
||||||
return part.get_payload(decode=True).decode('utf-8', errors='ignore')[:100]
|
return part.get_payload(decode=True).decode('utf-8', errors='ignore')[:100]
|
||||||
else:
|
else:
|
||||||
return msg.get_payload(decode=True).decode('utf-8', errors='ignore')[:100]
|
return msg.get_payload(decode=True).decode('utf-8', errors='ignore')[:100]
|
||||||
except:
|
except (AttributeError, TypeError, UnicodeDecodeError) as e:
|
||||||
|
logging.debug(f"Email preview extraction failed: {e}")
|
||||||
return '(Vorschau nicht verfügbar)'
|
return '(Vorschau nicht verfügbar)'
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
@ -1036,7 +1273,7 @@ def process_beat_tasks():
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
pending_tasks = [t for t in tasks if t.get('status') == 'pending' and t.get('type') in ('agent_created', 'manual', 'orchestrated')]
|
pending_tasks = [t for t in tasks if t.get('status') == 'pending' and t.get('type') in ('agent_created', 'manual', 'orchestrated', 'agent_delegated')]
|
||||||
|
|
||||||
for task in pending_tasks:
|
for task in pending_tasks:
|
||||||
agent_key = task.get('agent_key') or task.get('assigned_agent', '')
|
agent_key = task.get('agent_key') or task.get('assigned_agent', '')
|
||||||
|
|
@ -1077,10 +1314,22 @@ Agent-Beschreibungen:
|
||||||
if assigned not in AGENTS:
|
if assigned not in AGENTS:
|
||||||
assigned = available_agents[0]
|
assigned = available_agents[0]
|
||||||
|
|
||||||
|
# Sub-Task mit KOMPLETTEM Kontext
|
||||||
sub_task = {
|
sub_task = {
|
||||||
'id': len(tasks) + 1 + len(created_sub_tasks),
|
'id': len(tasks) + 1 + len(created_sub_tasks),
|
||||||
'title': t[:80],
|
'title': t[:80],
|
||||||
'description': f"Von Orchestrator zugewiesen: {response[:200]}...",
|
'description': f"""**Original-Aufgabe:**
|
||||||
|
{task.get('title', '')}
|
||||||
|
|
||||||
|
{task.get('description', '')}
|
||||||
|
|
||||||
|
**Orchestrator-Analyse:**
|
||||||
|
{response}
|
||||||
|
|
||||||
|
**Dein spezifischer Teil:**
|
||||||
|
{t}
|
||||||
|
|
||||||
|
Arbeite diesen Teil ab und liefere ein vollständiges Ergebnis.""",
|
||||||
'assigned_agent': AGENTS.get(assigned, {}).get('name', assigned),
|
'assigned_agent': AGENTS.get(assigned, {}).get('name', assigned),
|
||||||
'agent_key': assigned,
|
'agent_key': assigned,
|
||||||
'status': 'pending',
|
'status': 'pending',
|
||||||
|
|
@ -1133,7 +1382,8 @@ Agent-Beschreibungen:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("[TaskBeat] Fehler: %s", str(e))
|
logger.error("[TaskBeat] Fehler: %s", str(e))
|
||||||
|
|
||||||
time.sleep(30)
|
# Beat-Intervall: 10 Sekunden (statt 30)
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
|
||||||
def start_task_beat():
|
def start_task_beat():
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ function sendPromptWithStream() {
|
||||||
msgDiv.className = 'chat-message';
|
msgDiv.className = 'chat-message';
|
||||||
msgDiv.innerHTML = `
|
msgDiv.innerHTML = `
|
||||||
<div class="chat-timestamp">${new Date().toLocaleTimeString()} · <span class="badge bg-primary" style="font-size:.65rem;" id="agentBadge">wird ausgewählt…</span></div>
|
<div class="chat-timestamp">${new Date().toLocaleTimeString()} · <span class="badge bg-primary" style="font-size:.65rem;" id="agentBadge">wird ausgewählt…</span></div>
|
||||||
<div class="chat-prompt mt-1"><strong>Sie:</strong> ${prompt}</div>
|
<div class="chat-prompt mt-1"><strong>Sie:</strong> ${escapeHtml(prompt)}</div>
|
||||||
<div class="chat-response mt-1" id="responseDiv"><strong>Orchestrator:</strong> <span id="responseText">⏳ Agent arbeitet…</span></div>
|
<div class="chat-response mt-1" id="responseDiv"><strong>Orchestrator:</strong> <span id="responseText">⏳ Agent arbeitet…</span></div>
|
||||||
`;
|
`;
|
||||||
chatContainer.insertBefore(msgDiv, chatContainer.firstChild);
|
chatContainer.insertBefore(msgDiv, chatContainer.firstChild);
|
||||||
|
|
@ -220,5 +220,11 @@ function distributeTodos() {
|
||||||
status.className = 'form-text mt-2 text-danger';
|
status.className = 'form-text mt-2 text-danger';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,13 @@
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header bg-info">
|
<div class="card-header bg-info">
|
||||||
<h5 class="mb-0">🔄 Auto-Refresh</h5>
|
<h5 class="mb-0">ℹ️ Info</h5>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-check form-switch">
|
<p class="mb-0" style="font-size:0.875rem;color:var(--text-muted);">
|
||||||
<input class="form-check-input" type="checkbox" id="autoRefresh" onchange="toggleAutoRefresh()">
|
Tasks werden automatisch alle 10 Sekunden vom TaskBeat verarbeitet.
|
||||||
<label class="form-check-label" for="autoRefresh">Automatisch aktualisieren (alle 30s)</label>
|
<br>Diese Seite aktualisiert sich automatisch alle 15 Sekunden wenn Tasks aktiv sind.
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -108,14 +108,14 @@
|
||||||
</td>
|
</td>
|
||||||
<td style="color:var(--text-muted);font-size:.75rem;">{{ task.created }}</td>
|
<td style="color:var(--text-muted);font-size:.75rem;">{{ task.created }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if task.type == 'email' %}
|
{% if task.status == 'pending' %}
|
||||||
<span style="color:var(--text-muted);font-size:.75rem;">Auto</span>
|
<span style="color:var(--text-muted);font-size:.75rem;">⏳ Wartend</span>
|
||||||
{% elif task.status == 'pending' %}
|
|
||||||
<a href="/tasks/update/{{ task.id }}/in_progress" class="btn btn-sm btn-primary">Start</a>
|
|
||||||
{% elif task.status == 'in_progress' %}
|
{% elif task.status == 'in_progress' %}
|
||||||
<a href="/tasks/update/{{ task.id }}/completed" class="btn btn-sm btn-success">Fertig</a>
|
<span style="color:var(--info);font-size:.75rem;">🔄 Läuft...</span>
|
||||||
|
{% elif task.status == 'completed' %}
|
||||||
|
<span style="color:var(--success);font-size:.75rem;">✓ Auto</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span style="color:var(--text-muted);">—</span>
|
<span style="color:var(--text-muted);font-size:.75rem;">—</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -137,20 +137,13 @@
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
let autoRefreshInterval = null;
|
// Auto-Refresh alle 15 Sekunden wenn Tasks pending/in_progress sind
|
||||||
|
const hasPendingTasks = {{ 'true' if tasks|selectattr('status', 'in', ['pending', 'in_progress'])|list else 'false' }};
|
||||||
|
|
||||||
function toggleAutoRefresh() {
|
if (hasPendingTasks) {
|
||||||
const checkbox = document.getElementById('autoRefresh');
|
setInterval(() => {
|
||||||
if (checkbox.checked) {
|
|
||||||
autoRefreshInterval = setInterval(() => {
|
|
||||||
location.reload();
|
location.reload();
|
||||||
}, 30000);
|
}, 15000); // 15 Sekunden
|
||||||
} else {
|
|
||||||
if (autoRefreshInterval) {
|
|
||||||
clearInterval(autoRefreshInterval);
|
|
||||||
autoRefreshInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue