- Alle AIDA-Referenzen durch generische Begriffe ersetzt
(aida -> attendance im Code, "Zeiterfassung" im UI)
- DB-Spalten: last_aida_status -> last_attendance_status
- Settings: aida_csv_path -> attendance_csv_path
- REST API fuer Zeiterfassung auf Port 8081:
POST /api/attendance/{personal_nr} mit {"status": "anwesend"}
GET /api/attendance - alle Status abfragen
- Automatische DB-Migration fuer bestehende Installationen
- CSV bleibt als Option, API als Echtzeit-Alternative
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
225 lines
12 KiB
HTML
225 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Einstellungen - Busylight{% endblock %}
|
|
|
|
{% block content %}
|
|
<h4 class="mb-4">Einstellungen</h4>
|
|
|
|
{% if request.query_params.get('saved') %}
|
|
<div class="alert alert-success alert-dismissible fade show">
|
|
<i class="bi bi-check-circle"></i> Einstellungen gespeichert.
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<form method="post" action="/api/settings">
|
|
<div class="row g-4">
|
|
<!-- Azure AD -->
|
|
<div class="col-lg-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="mb-0"><i class="bi bi-microsoft"></i> Microsoft Entra ID (Azure AD)</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Anwendungs-ID (Client)</label>
|
|
<input type="text" name="azure_client_id" class="form-control font-monospace"
|
|
value="{{ settings.azure_client_id }}"
|
|
placeholder="z.B. b19325df-9905-4e93-8419-30639376277d">
|
|
<small class="text-muted">Azure Portal > App-Registrierungen > Uebersicht > Anwendungs-ID (Client)</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Verzeichnis-ID (Mandant)</label>
|
|
<input type="text" name="azure_tenant_id" class="form-control font-monospace"
|
|
value="{{ settings.azure_tenant_id }}"
|
|
placeholder="z.B. 6a0349fc-7bb3-4868-a711-27a0d1c58c83">
|
|
<input type="hidden" name="azure_auth_tenant" value="{{ settings.azure_tenant_id }}">
|
|
<input type="hidden" name="azure_client_secret" value="{{ settings.azure_client_secret }}">
|
|
<small class="text-muted">Azure Portal > App-Registrierungen > Uebersicht > Verzeichnis-ID (Mandant)</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Graph API Berechtigungen</label>
|
|
<input type="text" name="azure_scopes" class="form-control"
|
|
value="{{ settings.azure_scopes }}"
|
|
placeholder="User.Read User.Read.All Presence.Read Presence.Read.All">
|
|
<small class="text-muted">Standard: User.Read User.Read.All Presence.Read Presence.Read.All</small>
|
|
</div>
|
|
<div id="auth-status" class="border rounded p-3 bg-light">
|
|
{% if authenticated %}
|
|
<i class="bi bi-check-circle text-success"></i>
|
|
<strong class="text-success">Verbunden</strong>
|
|
<div class="mt-2">
|
|
<button type="button" class="btn btn-sm btn-outline-warning" onclick="startAuth()">
|
|
<i class="bi bi-arrow-repeat"></i> Neu verbinden
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<i class="bi bi-x-circle text-danger"></i>
|
|
<strong class="text-danger">Nicht verbunden</strong>
|
|
<div class="mt-2">
|
|
<button type="button" class="btn btn-sm btn-primary" onclick="startAuth()">
|
|
<i class="bi bi-microsoft"></i> Mit Azure verbinden
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div id="auth-flow" class="border rounded p-3 bg-light mt-2" style="display:none;">
|
|
<div id="auth-waiting" style="display:none;">
|
|
<p class="mb-2"><strong>Schritt 1:</strong> Oeffne den folgenden Link:</p>
|
|
<p><a id="auth-url" href="#" target="_blank" class="btn btn-sm btn-outline-primary">
|
|
<i class="bi bi-box-arrow-up-right"></i> Bei Microsoft anmelden
|
|
</a></p>
|
|
<p class="mb-2"><strong>Schritt 2:</strong> Gib diesen Code ein:</p>
|
|
<p class="text-center">
|
|
<span id="auth-code" class="badge bg-dark fs-4 font-monospace p-3"></span>
|
|
</p>
|
|
<p class="small text-muted mb-0">
|
|
<i class="bi bi-hourglass-split"></i> Warte auf Anmeldung...
|
|
</p>
|
|
</div>
|
|
<div id="auth-starting" style="display:none;">
|
|
<i class="bi bi-hourglass-split"></i> Verbindung wird hergestellt...
|
|
</div>
|
|
<div id="auth-success" style="display:none;">
|
|
<i class="bi bi-check-circle text-success"></i>
|
|
<strong class="text-success">Erfolgreich verbunden!</strong>
|
|
<p class="small text-muted mt-1 mb-0">Seite wird neu geladen...</p>
|
|
</div>
|
|
<div id="auth-error" style="display:none;">
|
|
<i class="bi bi-exclamation-triangle text-danger"></i>
|
|
<strong class="text-danger">Fehler:</strong>
|
|
<span id="auth-error-msg"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Polling & Zeiterfassung -->
|
|
<div class="col-lg-6">
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h6 class="mb-0"><i class="bi bi-clock"></i> Polling</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Abfrage-Intervall (Sekunden)</label>
|
|
<input type="number" name="poll_interval" class="form-control"
|
|
value="{{ settings.poll_interval }}" min="5" max="300">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h6 class="mb-0"><i class="bi bi-shield-lock"></i> Zugangsschutz</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Admin-Passwort</label>
|
|
<input type="password" name="admin_password" class="form-control"
|
|
placeholder="Neues Passwort eingeben (leer = nicht aendern)">
|
|
{% if settings.admin_password %}
|
|
<small class="text-success"><i class="bi bi-check-circle"></i> Passwort ist gesetzt</small>
|
|
{% else %}
|
|
<small class="text-warning"><i class="bi bi-exclamation-triangle"></i> Kein Passwort gesetzt - alle Seiten sind frei zugaenglich</small>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="mb-0"><i class="bi bi-file-earmark-spreadsheet"></i> Zeiterfassung (CSV)</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">CSV-Pfad</label>
|
|
<input type="text" name="attendance_csv_path" class="form-control font-monospace"
|
|
value="{{ settings.attendance_csv_path }}"
|
|
placeholder="/opt/busylight/status.csv">
|
|
<small class="text-muted">Pfad zur Anwesenheits-CSV (optional, wenn REST API genutzt wird)</small>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">CSV-Code: Anwesend</label>
|
|
<input type="text" name="attendance_status_anwesend" class="form-control"
|
|
value="{{ settings.attendance_status_anwesend }}"
|
|
placeholder="~~1~~">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">CSV-Code: Abwesend</label>
|
|
<input type="text" name="attendance_status_abwesend" class="form-control"
|
|
value="{{ settings.attendance_status_abwesend }}"
|
|
placeholder="~~0~~">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">CSV-Code: Homeoffice</label>
|
|
<input type="text" name="attendance_status_homeoffice" class="form-control"
|
|
value="{{ settings.attendance_status_homeoffice }}"
|
|
placeholder="~~2~~">
|
|
</div>
|
|
<div class="border rounded p-3 bg-light">
|
|
<small class="text-muted">
|
|
<strong>Alternativ: REST API</strong><br>
|
|
Status kann auch per API gesetzt werden (Port 8081):<br>
|
|
<code>POST /api/attendance/{personal_nr}</code><br>
|
|
Body: <code>{"status": "anwesend"}</code>
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="bi bi-check-lg"></i> Einstellungen speichern
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let authPollInterval = null;
|
|
|
|
function startAuth() {
|
|
document.getElementById('auth-flow').style.display = 'block';
|
|
document.getElementById('auth-starting').style.display = 'block';
|
|
document.getElementById('auth-waiting').style.display = 'none';
|
|
document.getElementById('auth-success').style.display = 'none';
|
|
document.getElementById('auth-error').style.display = 'none';
|
|
|
|
fetch('/api/azure/auth', {method: 'POST'})
|
|
.then(() => {
|
|
authPollInterval = setInterval(pollAuthStatus, 1000);
|
|
});
|
|
}
|
|
|
|
function pollAuthStatus() {
|
|
fetch('/api/azure/auth/status')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.status === 'waiting') {
|
|
document.getElementById('auth-starting').style.display = 'none';
|
|
document.getElementById('auth-waiting').style.display = 'block';
|
|
document.getElementById('auth-code').textContent = data.user_code;
|
|
const url = document.getElementById('auth-url');
|
|
url.href = data.verification_uri;
|
|
} else if (data.status === 'success') {
|
|
clearInterval(authPollInterval);
|
|
document.getElementById('auth-waiting').style.display = 'none';
|
|
document.getElementById('auth-starting').style.display = 'none';
|
|
document.getElementById('auth-success').style.display = 'block';
|
|
setTimeout(() => location.reload(), 2000);
|
|
} else if (data.status === 'error') {
|
|
clearInterval(authPollInterval);
|
|
document.getElementById('auth-waiting').style.display = 'none';
|
|
document.getElementById('auth-starting').style.display = 'none';
|
|
document.getElementById('auth-error').style.display = 'block';
|
|
document.getElementById('auth-error-msg').textContent = data.error;
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|