feat: rewrite busylight as FastAPI web application with SQLite
Replace the standalone Python script + config.cfg with a full web application featuring: - FastAPI backend with REST API - SQLite database (config, state, activity log separated) - Bootstrap 5 web interface for configuration - Pages: Dashboard, Users, Devices, Assignments, Color Rules, Settings, Log - APScheduler background polling for Teams presence + AIDA status - systemd service file for Linux deployment - Migration script to import existing config.cfg data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Zuordnungen - Busylight{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-4">Zuordnungen (Benutzer → WLED-Segment)</h4>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benutzer</th>
|
||||
<th>WLED-Geraet</th>
|
||||
<th>Segment</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for a in assignments %}
|
||||
<tr>
|
||||
<td>{{ a.display_name }}</td>
|
||||
<td>
|
||||
<code>{{ a.hostname }}</code>
|
||||
{% if a.device_name and a.device_name != a.hostname %}
|
||||
<small class="text-muted">({{ a.device_name }})</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary">Segment {{ a.segment }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="/api/assignments/{{ a.id }}/delete" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% if not assignments %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-4">
|
||||
Noch keine Zuordnungen vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-plus-circle"></i> Neue Zuordnung</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/api/assignments">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Benutzer</label>
|
||||
<select name="user_azure_id" class="form-select" required>
|
||||
<option value="">-- Benutzer waehlen --</option>
|
||||
{% for u in users %}
|
||||
<option value="{{ u.azure_id }}">{{ u.display_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">WLED-Geraet</label>
|
||||
<select name="wled_device_id" class="form-select" id="device-select" required>
|
||||
<option value="">-- Geraet waehlen --</option>
|
||||
{% for d in devices %}
|
||||
<option value="{{ d.id }}" data-segments="{{ d.segment_count }}">
|
||||
{{ d.name or d.hostname }} ({{ d.hostname }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Segment</label>
|
||||
<select name="segment" class="form-select" id="segment-select" required>
|
||||
<option value="0">Segment 0 (oben)</option>
|
||||
<option value="1">Segment 1 (unten)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-link-45deg"></i> Zuordnen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.getElementById('device-select').addEventListener('change', function() {
|
||||
const opt = this.selectedOptions[0];
|
||||
const count = parseInt(opt.dataset.segments || '2');
|
||||
const segSelect = document.getElementById('segment-select');
|
||||
segSelect.innerHTML = '';
|
||||
for (let i = 0; i < count; i++) {
|
||||
const label = i === 0 ? 'oben' : i === 1 ? 'unten' : i.toString();
|
||||
segSelect.innerHTML += `<option value="${i}">Segment ${i} (${label})</option>`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Busylight Manager{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link href="/static/style.css" rel="stylesheet">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex">
|
||||
<nav class="sidebar bg-dark text-white d-flex flex-column">
|
||||
<div class="p-3 border-bottom border-secondary">
|
||||
<h5 class="mb-0"><i class="bi bi-lightbulb-fill text-warning"></i> Busylight</h5>
|
||||
<small class="text-secondary">WLED Controller</small>
|
||||
</div>
|
||||
<ul class="nav flex-column mt-2 flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white {% if page == 'dashboard' %}active{% endif %}" href="/">
|
||||
<i class="bi bi-speedometer2"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white {% if page == 'users' %}active{% endif %}" href="/users">
|
||||
<i class="bi bi-people"></i> Benutzer
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white {% if page == 'devices' %}active{% endif %}" href="/devices">
|
||||
<i class="bi bi-router"></i> WLED-Geraete
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white {% if page == 'assignments' %}active{% endif %}" href="/assignments">
|
||||
<i class="bi bi-link-45deg"></i> Zuordnungen
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white {% if page == 'colors' %}active{% endif %}" href="/colors">
|
||||
<i class="bi bi-palette"></i> Farb-Regeln
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white {% if page == 'settings' %}active{% endif %}" href="/settings">
|
||||
<i class="bi bi-gear"></i> Einstellungen
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white {% if page == 'log' %}active{% endif %}" href="/log">
|
||||
<i class="bi bi-journal-text"></i> Protokoll
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="p-3 border-top border-secondary">
|
||||
{% if authenticated %}
|
||||
<small class="text-success"><i class="bi bi-check-circle"></i> Azure verbunden</small>
|
||||
{% else %}
|
||||
<small class="text-danger"><i class="bi bi-x-circle"></i> Azure getrennt</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="main-content flex-grow-1">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,79 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Farb-Regeln - Busylight{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-4">Farb-Regeln</h4>
|
||||
|
||||
{% if request.query_params.get('saved') %}
|
||||
<div class="alert alert-success alert-dismissible fade show">
|
||||
<i class="bi bi-check-circle"></i> Farben gespeichert.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/api/colors">
|
||||
<div class="row g-3">
|
||||
{% for rule in rules %}
|
||||
<div class="col-md-6 col-lg-4 col-xl-3">
|
||||
<div class="card busylight-card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<strong>{{ rule.status }}</strong>
|
||||
<input type="hidden" name="status" value="{{ rule.status }}">
|
||||
</div>
|
||||
<div class="color-preview"
|
||||
style="background: rgb({{ rule.r }}, {{ rule.g }}, {{ rule.b }});"
|
||||
title="Aktuelle Farbe"></div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small text-muted mb-1">Farbe</label>
|
||||
<input type="color" name="color_{{ rule.status }}" class="form-control form-control-color w-100"
|
||||
value="#{{ '%02x%02x%02x' % (rule.r, rule.g, rule.b) | default('000000') }}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label small text-muted mb-1">Effekt (optional)</label>
|
||||
<input type="text" name="effect_{{ rule.status }}" class="form-control form-control-sm"
|
||||
value="{{ rule.effect or '' }}" placeholder="z.B. /win&A=255">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> Alle Farben speichern
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="card" style="max-width: 400px;">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-plus-circle"></i> Neue Farb-Regel</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/api/colors/add">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Status-Name</label>
|
||||
<input type="text" name="status" class="form-control"
|
||||
placeholder="z.B. teams_focusing" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Farbe</label>
|
||||
<input type="color" name="color" class="form-control form-control-color w-100"
|
||||
value="#00ff00">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Effekt (optional)</label>
|
||||
<input type="text" name="effect" class="form-control"
|
||||
placeholder="z.B. /win&A=255">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-outline-primary w-100">Hinzufuegen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,74 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard - Busylight{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0">Dashboard</h4>
|
||||
<div>
|
||||
<form method="post" action="/api/poll" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-arrow-clockwise"></i> Jetzt abfragen
|
||||
</button>
|
||||
</form>
|
||||
<span class="text-muted ms-2" id="last-update"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not authenticated %}
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
Azure ist nicht verbunden. Bitte unter <a href="/settings">Einstellungen</a> konfigurieren und
|
||||
<code>python app.py --auth</code> ausfuehren.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if devices %}
|
||||
<div class="row g-3">
|
||||
{% for device in devices %}
|
||||
<div class="col-md-6 col-lg-4 col-xl-3">
|
||||
<div class="card busylight-card h-100">
|
||||
<div class="card-header bg-white border-0 pb-0">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>{{ device.name }}</strong>
|
||||
<small class="text-muted">{{ device.hostname }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-2">
|
||||
{% for seg in device.segments %}
|
||||
<div class="mb-2">
|
||||
<small class="text-muted d-block mb-1">Segment {{ seg.index }}</small>
|
||||
{% if seg.user %}
|
||||
<div class="segment-bar" style="background: rgba({{ seg.r }}, {{ seg.g }}, {{ seg.b }}, 0.3); border-color: rgb({{ seg.r }}, {{ seg.g }}, {{ seg.b }});">
|
||||
<span class="status-dot me-2" style="background: rgb({{ seg.r }}, {{ seg.g }}, {{ seg.b }});"></span>
|
||||
{{ seg.user }}
|
||||
</div>
|
||||
<small class="text-muted">{{ seg.aida }} / {{ seg.teams }}</small>
|
||||
{% else %}
|
||||
<div class="segment-bar empty">
|
||||
<i class="bi bi-dash-circle me-2"></i> Nicht zugeordnet
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-lightbulb display-1 text-muted"></i>
|
||||
<p class="text-muted mt-3">Noch keine WLED-Geraete konfiguriert.</p>
|
||||
<a href="/devices" class="btn btn-primary">Geraete hinzufuegen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Auto-refresh every 10 seconds
|
||||
setTimeout(() => location.reload(), 10000);
|
||||
document.getElementById('last-update').textContent =
|
||||
'Aktualisiert: ' + new Date().toLocaleTimeString('de-DE');
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,124 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}WLED-Geraete - Busylight{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h4 class="mb-4">WLED-Geraete</h4>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hostname / IP</th>
|
||||
<th>Name</th>
|
||||
<th>Segmente</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for device in devices %}
|
||||
<tr>
|
||||
<td>
|
||||
<code>{{ device.hostname }}</code>
|
||||
</td>
|
||||
<td>{{ device.name }}</td>
|
||||
<td>{{ device.segment_count }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal"
|
||||
data-bs-target="#edit-{{ device.id }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<form method="post" action="/api/devices/{{ device.id }}/delete"
|
||||
class="d-inline"
|
||||
onsubmit="return confirm('Geraet wirklich loeschen?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="edit-{{ device.id }}" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="/api/devices/{{ device.id }}/update">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Geraet bearbeiten</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hostname / IP</label>
|
||||
<input type="text" name="hostname" class="form-control"
|
||||
value="{{ device.hostname }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" name="name" class="form-control"
|
||||
value="{{ device.name }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Anzahl Segmente</label>
|
||||
<input type="number" name="segment_count" class="form-control"
|
||||
value="{{ device.segment_count }}" min="1" max="10">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary">Speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if not devices %}
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-muted py-4">
|
||||
Noch keine Geraete vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-plus-circle"></i> Neues Geraet</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/api/devices">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Hostname / IP</label>
|
||||
<input type="text" name="hostname" class="form-control"
|
||||
placeholder="doorlight0027.intra.example.de" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name (optional)</label>
|
||||
<input type="text" name="name" class="form-control"
|
||||
placeholder="Buero 027">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Anzahl Segmente</label>
|
||||
<input type="number" name="segment_count" class="form-control"
|
||||
value="2" min="1" max="10">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-plus"></i> Hinzufuegen
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Protokoll - Busylight{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0">Protokoll</h4>
|
||||
<span class="text-muted" id="last-update"></span>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover log-table mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeitpunkt</th>
|
||||
<th>Benutzer</th>
|
||||
<th>AIDA-Status</th>
|
||||
<th>Teams-Status</th>
|
||||
<th>WLED-URL</th>
|
||||
<th>Ergebnis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td class="text-nowrap">{{ log.timestamp }}</td>
|
||||
<td>{{ log.user_name }}</td>
|
||||
<td>
|
||||
<span class="badge {% if log.aida_status == 'anwesend' %}bg-success{% elif log.aida_status == 'abwesend' %}bg-secondary{% elif log.aida_status == 'homeoffice' %}bg-warning text-dark{% else %}bg-light text-dark{% endif %}">
|
||||
{{ log.aida_status or '-' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ log.teams_status or '-' }}</span>
|
||||
</td>
|
||||
<td><small class="font-monospace">{{ log.wled_url }}</small></td>
|
||||
<td>
|
||||
{% if log.result and '200' in log.result %}
|
||||
<span class="text-success"><i class="bi bi-check-circle"></i> {{ log.result }}</span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="bi bi-x-circle"></i> {{ log.result }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% if not logs %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted py-4">
|
||||
Noch keine Protokoll-Eintraege vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
setTimeout(() => location.reload(), 15000);
|
||||
document.getElementById('last-update').textContent =
|
||||
'Aktualisiert: ' + new Date().toLocaleTimeString('de-DE');
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,118 @@
|
||||
{% 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> Azure AD / Microsoft Graph</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Client ID</label>
|
||||
<input type="text" name="azure_client_id" class="form-control font-monospace"
|
||||
value="{{ settings.azure_client_id }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Client Secret</label>
|
||||
<input type="password" name="azure_client_secret" class="form-control font-monospace"
|
||||
value="{{ settings.azure_client_secret }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tenant ID</label>
|
||||
<input type="text" name="azure_tenant_id" class="form-control font-monospace"
|
||||
value="{{ settings.azure_tenant_id }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Auth Tenant</label>
|
||||
<input type="text" name="azure_auth_tenant" class="form-control"
|
||||
value="{{ settings.azure_auth_tenant }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Graph Scopes</label>
|
||||
<input type="text" name="azure_scopes" class="form-control"
|
||||
value="{{ settings.azure_scopes }}">
|
||||
</div>
|
||||
<div class="border rounded p-3 bg-light">
|
||||
{% if authenticated %}
|
||||
<i class="bi bi-check-circle text-success"></i>
|
||||
<strong class="text-success">Authentifiziert</strong>
|
||||
{% else %}
|
||||
<i class="bi bi-x-circle text-danger"></i>
|
||||
<strong class="text-danger">Nicht authentifiziert</strong>
|
||||
<p class="small text-muted mt-2 mb-0">
|
||||
Fuehre auf dem Server aus:<br>
|
||||
<code>python app.py --auth</code>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Polling & AIDA -->
|
||||
<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">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0"><i class="bi bi-file-earmark-spreadsheet"></i> AIDA Zeiterfassung</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">CSV-Pfad</label>
|
||||
<input type="text" name="aida_csv_path" class="form-control font-monospace"
|
||||
value="{{ settings.aida_csv_path }}"
|
||||
placeholder="/mnt/zeiterfassung/Status.csv">
|
||||
<small class="text-muted">Auf Linux z.B. via SMB-Mount</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Status: Anwesend</label>
|
||||
<input type="text" name="aida_status_anwesend" class="form-control"
|
||||
value="{{ settings.aida_status_anwesend }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Status: Abwesend</label>
|
||||
<input type="text" name="aida_status_abwesend" class="form-control"
|
||||
value="{{ settings.aida_status_abwesend }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Status: Homeoffice</label>
|
||||
<input type="text" name="aida_status_homeoffice" class="form-control"
|
||||
value="{{ settings.aida_status_homeoffice }}">
|
||||
</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 %}
|
||||
@@ -0,0 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Benutzer - Busylight{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h4 class="mb-0">Benutzer</h4>
|
||||
<form method="post" action="/api/users/sync">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-cloud-download"></i> Aus Azure synchronisieren
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% if request.query_params.get('synced') %}
|
||||
<div class="alert alert-success alert-dismissible fade show">
|
||||
<i class="bi bi-check-circle"></i> {{ request.query_params.get('synced') }} neue Benutzer synchronisiert.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.query_params.get('error') == 'not_authenticated' %}
|
||||
<div class="alert alert-danger alert-dismissible fade show">
|
||||
<i class="bi bi-x-circle"></i> Azure nicht verbunden. Bitte zuerst authentifizieren.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Name</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Personal-Nr (AIDA)</th>
|
||||
<th>Zuordnung</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr class="{% if not user.active %}table-light text-muted{% endif %}">
|
||||
<td>
|
||||
{% if user.active %}
|
||||
<span class="badge bg-success">Aktiv</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Inaktiv</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.display_name }}</td>
|
||||
<td><small>{{ user.email or '-' }}</small></td>
|
||||
<td>
|
||||
<form method="post" action="/api/users/{{ user.azure_id }}/update" class="d-flex gap-1">
|
||||
<input type="text" name="personal_nr" value="{{ user.personal_nr or '' }}"
|
||||
class="form-control form-control-sm" style="width: 120px;"
|
||||
placeholder="~~00000~~">
|
||||
<input type="hidden" name="active" value="{{ '1' if user.active else '0' }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-check"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
{% if user.assignment_id %}
|
||||
<span class="badge bg-info"><i class="bi bi-link-45deg"></i> Zugeordnet</span>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="/api/users/{{ user.azure_id }}/toggle" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm {% if user.active %}btn-outline-warning{% else %}btn-outline-success{% endif %}">
|
||||
{% if user.active %}
|
||||
<i class="bi bi-pause-circle"></i>
|
||||
{% else %}
|
||||
<i class="bi bi-play-circle"></i>
|
||||
{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 text-muted">
|
||||
<small>{{ users|length }} Benutzer insgesamt</small>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user