04/30 Web Checker web app ver. 1.0
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard — Website Checker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Dashboard</h1>
|
||||
<span class="text-muted" id="today-date"></span>
|
||||
</div>
|
||||
|
||||
<!-- KPI Cards -->
|
||||
<div class="kpi-grid">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value">{{ stats.total_users }}</div>
|
||||
<div class="kpi-label">Total Users</div>
|
||||
</div>
|
||||
<div class="kpi-card kpi-accent">
|
||||
<div class="kpi-value">{{ stats.active_today }}</div>
|
||||
<div class="kpi-label">Active Today</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-value">{{ stats.total_sites }}</div>
|
||||
<div class="kpi-label">Total Sites</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Per-user completion table -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Today's Completion</h2>
|
||||
<button class="btn btn-ghost btn-sm" onclick="location.reload()">↻ Refresh</button>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Checked</th>
|
||||
<th>Total</th>
|
||||
<th style="width:200px">Progress</th>
|
||||
<th>%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in stats.user_stats %}
|
||||
<tr>
|
||||
<td>{{ u.full_name }}</td>
|
||||
<td>{{ u.checked_count }}</td>
|
||||
<td>{{ u.total_sites }}</td>
|
||||
<td>
|
||||
<div class="progress">
|
||||
<div class="progress-bar {% if (u.pct_complete or 0) == 100 %}progress-success{% elif (u.pct_complete or 0) >= 50 %}progress-warn{% else %}progress-danger{% endif %}"
|
||||
style="width: {{ u.pct_complete or 0 }}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td><strong>{{ u.pct_complete or 0 }}%</strong></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="text-center text-muted">No user data for today.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('today-date').textContent =
|
||||
new Date().toLocaleDateString('en-US', {weekday:'long', year:'numeric', month:'long', day:'numeric'});
|
||||
// Auto-refresh every 60 seconds
|
||||
setTimeout(() => location.reload(), 60000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Logs — Website Checker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Logs</h1>
|
||||
</div>
|
||||
|
||||
<!-- Tab bar -->
|
||||
<div class="tab-bar">
|
||||
<a class="tab-item {{ 'active' if tab == 'activity' }}"
|
||||
href="{{ url_for('admin_logs.logs', tab='activity') }}">📋 Activity Log</a>
|
||||
<a class="tab-item {{ 'active' if tab == 'app' }}"
|
||||
href="{{ url_for('admin_logs.logs', tab='app') }}">🖥 App Log</a>
|
||||
</div>
|
||||
|
||||
<!-- Search / filter bar -->
|
||||
<form class="filter-bar" method="get">
|
||||
<input type="hidden" name="tab" value="{{ tab }}">
|
||||
<input class="form-control" name="search" placeholder="Search…" value="{{ search }}">
|
||||
{% if tab == 'app' %}
|
||||
<select class="form-control" name="level">
|
||||
<option value="">All Levels</option>
|
||||
{% for lvl in ['DEBUG','INFO','WARNING','ERROR','CRITICAL'] %}
|
||||
<option {{ 'selected' if level_filter == lvl }}>{{ lvl }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<select class="form-control" name="limit">
|
||||
{% for n in [100, 200, 500] %}
|
||||
<option value="{{ n }}" {{ 'selected' if limit == n }}>{{ n }} rows</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-ghost" type="submit">Filter</button>
|
||||
</form>
|
||||
|
||||
{% if tab == 'activity' %}
|
||||
<div class="card mt-2">
|
||||
<table class="table table-sm">
|
||||
<thead><tr><th>Time</th><th>User</th><th>Action</th><th>Entity</th><th>Detail</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in activity_rows %}
|
||||
<tr>
|
||||
<td class="text-muted nowrap">{{ row.created_at.strftime('%Y-%m-%d %H:%M:%S') if row.created_at else '—' }}</td>
|
||||
<td>{{ row.username or '—' }}</td>
|
||||
<td><code class="action-badge">{{ row.action }}</code></td>
|
||||
<td class="text-muted">{{ row.entity or '' }} {% if row.entity_id %}#{{ row.entity_id }}{% endif %}</td>
|
||||
<td class="text-muted">{{ row.detail or '' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="text-center text-muted">No records found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<span></span>
|
||||
<form method="post" action="{{ url_for('admin_logs.purge') }}"
|
||||
onsubmit="return confirm('Purge old app log records?')" style="display:flex;gap:.5rem;align-items:center">
|
||||
<input class="form-control" name="days" type="number" value="30" style="width:80px">
|
||||
<label class="form-label mb-0">days</label>
|
||||
<button class="btn btn-danger btn-sm" type="submit">Purge</button>
|
||||
</form>
|
||||
</div>
|
||||
<table class="table table-sm">
|
||||
<thead><tr><th>Time</th><th>Level</th><th>Logger</th><th>Message</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in app_rows %}
|
||||
<tr class="log-{{ row.level | lower }}">
|
||||
<td class="text-muted nowrap">{{ row.logged_at.strftime('%Y-%m-%d %H:%M:%S') if row.logged_at else '—' }}</td>
|
||||
<td><span class="badge badge-log-{{ row.level | lower }}">{{ row.level }}</span></td>
|
||||
<td class="text-muted">{{ row.logger_name }}</td>
|
||||
<td class="log-message">{{ row.message }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="4" class="text-center text-muted">No records found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,95 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Reports — Website Checker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Reports</h1>
|
||||
<a class="btn btn-ghost" href="{{ url_for('admin_reports.export_csv', **request.args) }}">⬇ Export CSV</a>
|
||||
</div>
|
||||
|
||||
<div class="tab-bar">
|
||||
<a class="tab-item {{ 'active' if tab == 'detail' }}" href="{{ url_for('admin_reports.reports', tab='detail', date_from=date_from, date_to=date_to) }}">Shift Detail</a>
|
||||
<a class="tab-item {{ 'active' if tab == 'unchecked' }}" href="{{ url_for('admin_reports.reports', tab='unchecked', target_date=target_date) }}">Unchecked</a>
|
||||
<a class="tab-item {{ 'active' if tab == 'summary' }}" href="{{ url_for('admin_reports.reports', tab='summary', date_from=date_from, date_to=date_to) }}">Summary</a>
|
||||
</div>
|
||||
|
||||
<form class="filter-bar mt-2" method="get">
|
||||
<input type="hidden" name="tab" value="{{ tab }}">
|
||||
{% if tab == 'unchecked' %}
|
||||
<label class="form-label mb-0">Date:</label>
|
||||
<input class="form-control" type="date" name="target_date" value="{{ target_date }}">
|
||||
{% else %}
|
||||
<label class="form-label mb-0">From:</label>
|
||||
<input class="form-control" type="date" name="date_from" value="{{ date_from }}">
|
||||
<label class="form-label mb-0">To:</label>
|
||||
<input class="form-control" type="date" name="date_to" value="{{ date_to }}">
|
||||
{% endif %}
|
||||
{% if tab == 'detail' or tab == 'unchecked' %}
|
||||
<select class="form-control" name="user_id">
|
||||
<option value="">All Users</option>
|
||||
{% for u in users %}<option value="{{ u.id }}" {{ 'selected' if user_id == u.id|string }}>{{ u.username }}</option>{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
{% if tab == 'detail' %}
|
||||
<select class="form-control" name="website_id">
|
||||
<option value="">All Websites</option>
|
||||
{% for w in websites %}<option value="{{ w.id }}" {{ 'selected' if website_id == w.id|string }}>{{ w.name }}</option>{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<button class="btn btn-ghost" type="submit">Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="card mt-2">
|
||||
{% if tab == 'detail' %}
|
||||
<table class="table table-sm">
|
||||
<thead><tr><th>Date</th><th>Time</th><th>User</th><th>Website</th><th>URL</th><th>Note</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td>{{ r.check_date }}</td>
|
||||
<td class="text-muted nowrap">{{ r.checked_at.strftime('%H:%M:%S') if r.checked_at else '' }}</td>
|
||||
<td>{{ r.full_name }}</td>
|
||||
<td>{{ r.website_name }}</td>
|
||||
<td><a href="{{ r.url }}" target="_blank" rel="noopener">{{ r.url[:40] }}…</a></td>
|
||||
<td class="text-muted">{{ r.user_note }}</td>
|
||||
</tr>
|
||||
{% else %}<tr><td colspan="6" class="text-center text-muted">No data.</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% elif tab == 'unchecked' %}
|
||||
<table class="table table-sm">
|
||||
<thead><tr><th>Date</th><th>User</th><th>Website</th><th>URL</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td>{{ r.check_date }}</td>
|
||||
<td>{{ r.full_name }}</td>
|
||||
<td>{{ r.website_name }}</td>
|
||||
<td><a href="{{ r.url }}" target="_blank" rel="noopener">{{ r.url[:40] }}…</a></td>
|
||||
<td><span class="badge badge-danger">Not Checked</span></td>
|
||||
</tr>
|
||||
{% else %}<tr><td colspan="5" class="text-center text-muted">No data.</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% elif tab == 'summary' %}
|
||||
<table class="table table-sm">
|
||||
<thead><tr><th>Date</th><th>User</th><th>Checked</th><th>Total</th><th>Progress</th><th>%</th></tr></thead>
|
||||
<tbody>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td>{{ r.check_date }}</td>
|
||||
<td>{{ r.full_name }}</td>
|
||||
<td>{{ r.checked_count }}</td>
|
||||
<td>{{ r.total_sites }}</td>
|
||||
<td>
|
||||
<div class="progress"><div class="progress-bar" style="width:{{ r.pct_complete or 0 }}%"></div></div>
|
||||
</td>
|
||||
<td><strong>{{ r.pct_complete or 0 }}%</strong></td>
|
||||
</tr>
|
||||
{% else %}<tr><td colspan="6" class="text-center text-muted">No data.</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,85 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Settings — Website Checker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header"><h1 class="page-title">Application Settings</h1></div>
|
||||
|
||||
<div class="settings-grid">
|
||||
<!-- Email Settings -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">📧 Email Report Settings</h2></div>
|
||||
<form method="post" action="{{ url_for('admin_settings.save_email') }}">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Enable Email Reports</label>
|
||||
<select class="form-control" name="email_enabled">
|
||||
<option value="1" {{ 'selected' if email.get('email.enabled')=='1' }}>Enabled</option>
|
||||
<option value="0" {{ 'selected' if email.get('email.enabled')!='1' }}>Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">SMTP Host</label>
|
||||
<input class="form-control" name="email_smtp_host" value="{{ email.get('email.smtp_host','') }}">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-1">
|
||||
<label class="form-label">Port</label>
|
||||
<input class="form-control" name="email_smtp_port" value="{{ email.get('email.smtp_port','587') }}">
|
||||
</div>
|
||||
<div class="form-group flex-1">
|
||||
<label class="form-label">Security</label>
|
||||
<select class="form-control" name="email_security">
|
||||
{% for mode in ['starttls','ssl','none'] %}
|
||||
<option {{ 'selected' if email.get('email.security')==mode }}>{{ mode }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">SMTP Username</label>
|
||||
<input class="form-control" name="email_smtp_user" value="{{ email.get('email.smtp_user','') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">SMTP Password</label>
|
||||
<input class="form-control" type="password" name="email_smtp_password" placeholder="(unchanged)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Recipients (comma-separated)</label>
|
||||
<input class="form-control" name="email_recipients" value="{{ email.get('email.recipients','') }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Send Time (HH:MM)</label>
|
||||
<input class="form-control" type="time" name="email_send_time" value="{{ email.get('email.send_time','08:00') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" type="submit">Save Email Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Groq / AI Settings -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title">🤖 Groq AI Settings</h2></div>
|
||||
<form method="post" action="{{ url_for('admin_settings.save_groq') }}">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Groq API Key</label>
|
||||
<input class="form-control" type="password" name="groq_api_key"
|
||||
placeholder="{{ '(key set)' if groq.get('groq.api_key') else 'Enter API key' }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Default Model</label>
|
||||
<select class="form-control" name="groq_model">
|
||||
{% for m in ['llama-3.3-70b-versatile','llama-3.1-8b-instant','gemma2-9b-it'] %}
|
||||
<option {{ 'selected' if groq.get('groq.model')==m }}>{{ m }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" type="submit">Save AI Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,173 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Shifts — Website Checker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Shift Management</h1>
|
||||
<button class="btn btn-primary" onclick="openModal('modal-create-shift')">+ New Shift</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Days</th><th>Time</th><th>Users</th><th>Sites</th><th>Active</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in shifts %}
|
||||
<tr>
|
||||
<td><strong>{{ s.name }}</strong><br><span class="text-muted text-sm">{{ s.note or '' }}</span></td>
|
||||
<td>{{ s.days_of_week }}</td>
|
||||
<td class="text-muted">{{ s.start_time }} – {{ s.end_time }}</td>
|
||||
<td>{{ s.user_count }}</td>
|
||||
<td>{{ s.website_count }}</td>
|
||||
<td><span class="badge badge-{{ 'success' if s.is_active else 'neutral' }}">{{ 'Active' if s.is_active else 'Inactive' }}</span></td>
|
||||
<td class="actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="loadEditShift({{ s.id }})">✎ Edit</button>
|
||||
<form method="post" action="{{ url_for('admin_shifts.delete', shift_id=s.id) }}"
|
||||
style="display:inline" onsubmit="return confirm('Deactivate shift {{ s.name }}?')">
|
||||
<button class="btn btn-danger btn-sm" type="submit">✕</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="text-center text-muted">No shifts configured.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Create Shift Modal -->
|
||||
<div class="modal-overlay" id="modal-create-shift">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3>New Shift</h3>
|
||||
<button class="modal-close" onclick="closeModal('modal-create-shift')">✕</button>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('admin_shifts.create') }}">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Shift Name *</label>
|
||||
<input class="form-control" name="name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Days of Week</label>
|
||||
<div class="day-checkboxes">
|
||||
{% for label, val in day_map %}
|
||||
<label class="day-check">
|
||||
<input type="checkbox" name="days_of_week[]" value="{{ val }}"> {{ label }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-1">
|
||||
<label class="form-label">Start Time</label>
|
||||
<input class="form-control" type="time" name="start_time" value="08:00">
|
||||
</div>
|
||||
<div class="form-group flex-1">
|
||||
<label class="form-label">End Time</label>
|
||||
<input class="form-control" type="time" name="end_time" value="17:00">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Assign Users</label>
|
||||
<div class="checkbox-list">
|
||||
{% for u in all_users %}
|
||||
<label><input type="checkbox" name="user_ids[]" value="{{ u.id }}"> {{ u.username }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Assign Websites</label>
|
||||
<div class="checkbox-list">
|
||||
{% for w in all_sites %}
|
||||
<label><input type="checkbox" name="website_ids[]" value="{{ w.id }}"> {{ w.name }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Note</label>
|
||||
<textarea class="form-control" name="note" rows="2"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="closeModal('modal-create-shift')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create Shift</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Shift Modal -->
|
||||
<div class="modal-overlay" id="modal-edit-shift">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3>Edit Shift</h3>
|
||||
<button class="modal-close" onclick="closeModal('modal-edit-shift')">✕</button>
|
||||
</div>
|
||||
<form method="post" id="edit-shift-form">
|
||||
<div class="modal-body" id="edit-shift-body"><div class="text-center text-muted">Loading…</div></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="closeModal('modal-edit-shift')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
const ALL_USERS = {{ all_users | tojson }};
|
||||
const ALL_SITES = {{ all_sites | tojson }};
|
||||
const DAY_MAP = {{ day_map | tojson }};
|
||||
|
||||
function loadEditShift(id) {
|
||||
fetch('/admin/shifts/' + id + '/detail')
|
||||
.then(r => r.json())
|
||||
.then(s => {
|
||||
document.getElementById('edit-shift-form').action = '/admin/shifts/' + id + '/edit';
|
||||
document.getElementById('edit-shift-body').innerHTML = buildShiftForm(s);
|
||||
openModal('modal-edit-shift');
|
||||
});
|
||||
}
|
||||
|
||||
function buildShiftForm(s) {
|
||||
const days = DAY_MAP.map(([lbl, val]) => `
|
||||
<label class="day-check">
|
||||
<input type="checkbox" name="days_of_week[]" value="${val}"
|
||||
${s.days_of_week.includes(val)?'checked':''}>${lbl}
|
||||
</label>`).join('');
|
||||
|
||||
const users = ALL_USERS.map(u => `
|
||||
<label><input type="checkbox" name="user_ids[]" value="${u.id}"
|
||||
${s.user_ids.includes(u.id)?'checked':''}> ${u.username}</label>`).join('');
|
||||
|
||||
const sites = ALL_SITES.map(w => `
|
||||
<label><input type="checkbox" name="website_ids[]" value="${w.id}"
|
||||
${s.website_ids.includes(w.id)?'checked':''}> ${w.name}</label>`).join('');
|
||||
|
||||
return `
|
||||
<div class="form-group"><label class="form-label">Shift Name *</label>
|
||||
<input class="form-control" name="name" value="${esc(s.name)}" required></div>
|
||||
<div class="form-group"><label class="form-label">Days of Week</label>
|
||||
<div class="day-checkboxes">${days}</div></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-1"><label class="form-label">Start Time</label>
|
||||
<input class="form-control" type="time" name="start_time" value="${esc(s.start_time)}"></div>
|
||||
<div class="form-group flex-1"><label class="form-label">End Time</label>
|
||||
<input class="form-control" type="time" name="end_time" value="${esc(s.end_time)}"></div>
|
||||
</div>
|
||||
<div class="form-group"><label class="form-label">Active</label>
|
||||
<select class="form-control" name="is_active">
|
||||
<option value="1" ${s.is_active?'selected':''}>Yes</option>
|
||||
<option value="0" ${!s.is_active?'selected':''}>No</option>
|
||||
</select></div>
|
||||
<div class="form-group"><label class="form-label">Assign Users</label>
|
||||
<div class="checkbox-list">${users}</div></div>
|
||||
<div class="form-group"><label class="form-label">Assign Websites</label>
|
||||
<div class="checkbox-list">${sites}</div></div>
|
||||
<div class="form-group"><label class="form-label">Note</label>
|
||||
<textarea class="form-control" name="note" rows="2">${esc(s.note)}</textarea></div>`;
|
||||
}
|
||||
function esc(s) { return (s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,145 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Users — Website Checker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">User Management</h1>
|
||||
<button class="btn btn-primary" onclick="openModal('modal-create-user')">+ Add User</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th><th>Username</th><th>Full Name</th><th>Email</th>
|
||||
<th>Role</th><th>Active</th><th>Created</th><th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u.id }}</td>
|
||||
<td><strong>{{ u.username }}</strong></td>
|
||||
<td>{{ u.full_name or '—' }}</td>
|
||||
<td>{{ u.email or '—' }}</td>
|
||||
<td><span class="badge badge-{{ 'accent' if u.role == 'admin' else 'neutral' }}">{{ u.role }}</span></td>
|
||||
<td><span class="badge badge-{{ 'success' if u.is_active else 'danger' }}">{{ 'Yes' if u.is_active else 'No' }}</span></td>
|
||||
<td class="text-muted">{{ u.created_at.strftime('%Y-%m-%d') if u.created_at else '—' }}</td>
|
||||
<td class="actions">
|
||||
<button class="btn btn-ghost btn-sm"
|
||||
onclick='openEditUser({{ u|tojson }})'>✎ Edit</button>
|
||||
<form method="post" action="{{ url_for('admin_users.delete', user_id=u.id) }}"
|
||||
style="display:inline"
|
||||
onsubmit="return confirm('Delete user {{ u.username }}?')">
|
||||
<button class="btn btn-danger btn-sm" type="submit">✕</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="8" class="text-center text-muted">No users found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Create User Modal -->
|
||||
<div class="modal-overlay" id="modal-create-user">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h3>Add User</h3>
|
||||
<button class="modal-close" onclick="closeModal('modal-create-user')">✕</button>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('admin_users.create') }}">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username *</label>
|
||||
<input class="form-control" name="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Full Name</label>
|
||||
<input class="form-control" name="full_name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email</label>
|
||||
<input class="form-control" type="email" name="email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Role</label>
|
||||
<select class="form-control" name="role">
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password *</label>
|
||||
<input class="form-control" type="password" name="password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="closeModal('modal-create-user')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create User</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit User Modal -->
|
||||
<div class="modal-overlay" id="modal-edit-user">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-header">
|
||||
<h3>Edit User</h3>
|
||||
<button class="modal-close" onclick="closeModal('modal-edit-user')">✕</button>
|
||||
</div>
|
||||
<form method="post" id="edit-user-form">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username *</label>
|
||||
<input class="form-control" name="username" id="edit-username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Full Name</label>
|
||||
<input class="form-control" name="full_name" id="edit-full-name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email</label>
|
||||
<input class="form-control" type="email" name="email" id="edit-email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Role</label>
|
||||
<select class="form-control" name="role" id="edit-role">
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Active</label>
|
||||
<select class="form-control" name="is_active" id="edit-is-active">
|
||||
<option value="1">Yes</option>
|
||||
<option value="0">No</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">New Password (leave blank to keep current)</label>
|
||||
<input class="form-control" type="password" name="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="closeModal('modal-edit-user')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openEditUser(u) {
|
||||
document.getElementById('edit-username').value = u.username;
|
||||
document.getElementById('edit-full-name').value = u.full_name || '';
|
||||
document.getElementById('edit-email').value = u.email || '';
|
||||
document.getElementById('edit-role').value = u.role;
|
||||
document.getElementById('edit-is-active').value = u.is_active ? '1' : '0';
|
||||
document.getElementById('edit-user-form').action =
|
||||
'/admin/users/' + u.id + '/edit';
|
||||
openModal('modal-edit-user');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,176 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Websites — Website Checker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Website Link Management</h1>
|
||||
<button class="btn btn-primary" onclick="openModal('modal-create-site')">+ Add Website</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Name</th><th>Type</th><th>URL</th><th>Note</th><th>Visibility</th><th>Created By</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for w in websites %}
|
||||
<tr class="{{ 'row-weekly' if w.check_type == 'weekly' else '' }}">
|
||||
<td>{{ w.id }}</td>
|
||||
<td><strong>{{ w.name }}</strong></td>
|
||||
<td><span class="badge badge-{{ 'warn' if w.check_type == 'weekly' else 'neutral' }}">{{ w.check_type }}</span></td>
|
||||
<td class="url-cell"><a href="{{ w.url }}" target="_blank" rel="noopener">{{ w.url[:60] }}{% if w.url|length > 60 %}…{% endif %}</a></td>
|
||||
<td class="text-muted">{{ (w.note or '')[:40] }}{% if (w.note or '')|length > 40 %}…{% endif %}</td>
|
||||
<td><span class="badge badge-{{ 'accent' if w.visibility == 'assigned' else 'neutral' }}">{{ w.visibility }}</span></td>
|
||||
<td class="text-muted">{{ w.creator or '—' }}</td>
|
||||
<td class="actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="loadEditSite({{ w.id }})">✎ Edit</button>
|
||||
<form method="post" action="{{ url_for('admin_websites.delete', website_id=w.id) }}"
|
||||
style="display:inline" onsubmit="return confirm('Soft-delete {{ w.name }}?')">
|
||||
<button class="btn btn-danger btn-sm" type="submit">✕</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="8" class="text-center text-muted">No websites configured.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Create Website Modal -->
|
||||
<div class="modal-overlay" id="modal-create-site">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3>Add Website</h3>
|
||||
<button class="modal-close" onclick="closeModal('modal-create-site')">✕</button>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('admin_websites.create') }}">
|
||||
<div class="modal-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-1">
|
||||
<label class="form-label">Name *</label>
|
||||
<input class="form-control" name="name" required>
|
||||
</div>
|
||||
<div class="form-group flex-1">
|
||||
<label class="form-label">Check Type</label>
|
||||
<select class="form-control" name="check_type">
|
||||
<option value="daily">📅 Daily</option>
|
||||
<option value="weekly">🗓 Weekly</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">URL *</label>
|
||||
<input class="form-control" name="url" type="url" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Visibility</label>
|
||||
<select class="form-control" name="visibility">
|
||||
<option value="all">All users</option>
|
||||
<option value="assigned">Assigned users only</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Note</label>
|
||||
<textarea class="form-control" name="note" rows="2"></textarea>
|
||||
</div>
|
||||
<hr>
|
||||
<h4 style="margin-bottom:.5rem">Credentials <button type="button" class="btn btn-secondary btn-sm" onclick="addCredRow(this)">+ Add</button></h4>
|
||||
<div class="cred-rows">
|
||||
<div class="cred-row">
|
||||
<div class="form-group"><label class="form-label">Label</label><input class="form-control" name="cred_label[]" placeholder="e.g. Admin"></div>
|
||||
<div class="form-group"><label class="form-label">Username</label><input class="form-control" name="cred_username[]"></div>
|
||||
<div class="form-group"><label class="form-label">Password</label><input class="form-control" name="cred_password[]" type="password"></div>
|
||||
<div class="form-group"><label class="form-label"> </label><button type="button" class="btn btn-danger btn-sm" onclick="removeRow(this)">✕</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="closeModal('modal-create-site')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create Website</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Website Modal -->
|
||||
<div class="modal-overlay" id="modal-edit-site">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-header">
|
||||
<h3>Edit Website</h3>
|
||||
<button class="modal-close" onclick="closeModal('modal-edit-site')">✕</button>
|
||||
</div>
|
||||
<form method="post" id="edit-site-form">
|
||||
<div class="modal-body" id="edit-site-body">
|
||||
<div class="text-center text-muted">Loading…</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-ghost" onclick="closeModal('modal-edit-site')">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
function loadEditSite(id) {
|
||||
fetch('/admin/websites/' + id + '/detail')
|
||||
.then(r => r.json())
|
||||
.then(site => {
|
||||
const form = document.getElementById('edit-site-form');
|
||||
form.action = '/admin/websites/' + id + '/edit';
|
||||
|
||||
document.getElementById('edit-site-body').innerHTML = buildSiteForm(site);
|
||||
openModal('modal-edit-site');
|
||||
});
|
||||
}
|
||||
|
||||
function buildSiteForm(s) {
|
||||
const creds = (s.credentials || []).map(c => `
|
||||
<div class="cred-row">
|
||||
<input class="form-control" name="cred_label[]" placeholder="Label" value="${esc(c.label)}">
|
||||
<input class="form-control" name="cred_username[]" placeholder="Username" value="${esc(c.username)}">
|
||||
<input class="form-control" name="cred_password[]" placeholder="Password" type="password" value="${esc(c.password)}">
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeRow(this)">✕</button>
|
||||
</div>`).join('') || emptyCredRow();
|
||||
|
||||
return `
|
||||
<div class="form-group"><label class="form-label">Name *</label>
|
||||
<input class="form-control" name="name" value="${esc(s.name)}" required></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group flex-1"><label class="form-label">Check Type</label>
|
||||
<select class="form-control" name="check_type">
|
||||
<option value="daily" ${s.check_type==='daily'?'selected':''}>📅 Daily</option>
|
||||
<option value="weekly" ${s.check_type==='weekly'?'selected':''}>🗓 Weekly</option>
|
||||
</select></div>
|
||||
<div class="form-group flex-1"><label class="form-label">Visibility</label>
|
||||
<select class="form-control" name="visibility">
|
||||
<option value="all" ${s.visibility==='all'?'selected':''}>All users</option>
|
||||
<option value="assigned" ${s.visibility==='assigned'?'selected':''}>Assigned only</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<div class="form-group"><label class="form-label">URL *</label>
|
||||
<input class="form-control" name="url" type="url" value="${esc(s.url)}" required></div>
|
||||
<div class="form-group"><label class="form-label">Note</label>
|
||||
<textarea class="form-control" name="note" rows="2">${esc(s.note)}</textarea></div>
|
||||
<hr>
|
||||
<h4>Credentials <button type="button" class="btn btn-ghost btn-sm" onclick="addCredRow(this)">+ Add</button></h4>
|
||||
<div class="cred-rows">${creds}</div>`;
|
||||
}
|
||||
|
||||
function emptyCredRow() {
|
||||
return `<div class="cred-row">
|
||||
<input class="form-control" name="cred_label[]" placeholder="Label">
|
||||
<input class="form-control" name="cred_username[]" placeholder="Username">
|
||||
<input class="form-control" name="cred_password[]" placeholder="Password" type="password">
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeRow(this)">✕</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function addCredRow(btn) {
|
||||
btn.closest('.modal-body').querySelector('.cred-rows').insertAdjacentHTML('beforeend', emptyCredRow());
|
||||
}
|
||||
function removeRow(btn) { btn.closest('.cred-row').remove(); }
|
||||
function esc(s) { return (s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user