Files
WebChecker--Web-app-/templates/user/dashboard.html
T
2026-05-24 17:27:45 -04:00

479 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}My Shifts — Website Checker{% endblock %}
{% block content %}
<!-- ── Page header ─────────────────────────────────────────── -->
<div class="page-header">
<div>
<h1>My Shift — Website Checklist</h1>
<div class="page-subtitle" id="today-date"></div>
</div>
<div class="flex gap-1 items-center flex-wrap">
{% for s in shifts %}
<span class="badge badge-accent">{{ s.name }}
{% if s.start_time and s.end_time %}
· {{ s.start_time|hhmm }}{{ s.end_time|hhmm }}
{% endif %}
</span>
{% endfor %}
<button class="btn btn-secondary btn-sm" onclick="location.reload()">↻ Refresh</button>
</div>
</div>
<!-- ── Progress bar ─────────────────────────────────────────── -->
<div class="progress-strip">
<div class="progress-strip-bar">
<div class="progress-strip-fill {% if pct == 100 %}fill-success{% elif pct >= 50 %}fill-warning{% else %}fill-danger{% endif %}"
style="width:{{ pct }}%"></div>
</div>
<div class="progress-strip-label">
<strong>{{ checked }} / {{ total }}</strong> checked
<span class="text-muted">&nbsp;·&nbsp; {{ pct }}%</span>
</div>
</div>
<!-- ── Action bar ───────────────────────────────────────────── -->
<div class="action-bar">
<!-- Search: magnifier on the RIGHT -->
<div class="search-wrap">
<input type="text" id="site-search" placeholder="Search sites…" autocomplete="off">
<span class="search-icon">🔍</span>
</div>
<button class="btn btn-secondary btn-sm" id="btn-select-all">☑ Select All Unchecked</button>
<button class="btn btn-success btn-sm" id="btn-bulk-check">✔ Mark Selected Checked</button>
</div>
<!-- ── Site list grouped by frequency ──────────────────────── -->
<div id="site-list">
{# Group sites by check_type #}
{% set daily_sites = sites | selectattr('check_type', 'equalto', 'daily') | list %}
{% set weekly_sites = sites | selectattr('check_type', 'equalto', 'weekly') | list %}
{% set other_sites = sites | rejectattr('check_type', 'in', ['daily','weekly']) | list %}
{% for group_name, group_sites in [('Daily', daily_sites), ('Weekly', weekly_sites), ('Other', other_sites)] %}
{% if group_sites %}
<!-- Group header (collapsible) -->
<div class="group-header" data-group="{{ group_name | lower }}" onclick="toggleGroup('{{ group_name | lower }}')">
<span class="group-toggle" id="toggle-{{ group_name | lower }}"></span>
<span class="group-label">{{ group_name }}</span>
<span class="group-count">
{{ group_sites | selectattr('check_id') | list | length }} / {{ group_sites | length }} checked
</span>
<div class="group-mini-bar">
{% set g_total = group_sites | length %}
{% set g_checked = group_sites | selectattr('check_id') | list | length %}
{% set g_pct = ((g_checked / g_total) * 100) | int if g_total > 0 else 0 %}
<div class="group-mini-fill
{% if g_pct == 100 %}fill-success{% elif g_pct >= 50 %}fill-warning{% else %}fill-danger{% endif %}"
style="width:{{ g_pct }}%"></div>
</div>
</div>
<!-- Sites in this group -->
<div class="site-group" id="group-{{ group_name | lower }}">
{% for site in group_sites %}
<div class="site-card {% if site.check_id %}is-checked{% endif %}"
data-id="{{ site.id }}"
data-group="{{ group_name | lower }}"
data-name="{{ site.name | lower }}"
data-url="{{ (site.url or '') | lower }}"
data-site-name="{{ site.name }}"
data-user-note="{{ site.user_note or '' }}">
<!-- Row 1: checkbox · status · info · actions · health -->
<div class="sc-row1">
<div class="sc-select">
<input type="checkbox" class="bulk-cb" data-id="{{ site.id }}"
{% if site.check_id %}disabled{% endif %}>
</div>
<div class="sc-status">
{% if site.check_id %}
<span class="status-dot dot-checked"
title="Checked at {{ site.checked_at.strftime('%H:%M') if site.checked_at else 'today' }}"></span>
{% else %}
<span class="status-dot dot-unchecked"></span>
{% endif %}
</div>
<div class="sc-body">
<div class="sc-title-row">
<a class="sc-name" href="{{ site.url }}" target="_blank" rel="noopener">{{ site.name }}</a>
<span class="badge {% if site.check_type == 'weekly' %}badge-warning{% else %}badge-muted{% endif %}">
{{ site.check_type | capitalize }}
</span>
{% if site.check_id and site.checked_at %}
<span class="sc-checked-time text-success">✔ {{ site.checked_at.strftime('%H:%M') }}</span>
{% endif %}
</div>
<div class="sc-url">{{ site.url }}</div>
<div class="sc-meta">
{% if site.site_note %}
<span class="sc-meta-item">📌 {{ site.site_note }}</span>
{% endif %}
{% if site.shift_names %}
<span class="sc-meta-item text-accent">🕐 {{ site.shift_names }}</span>
{% endif %}
</div>
</div>
<div class="sc-actions">
{% if site.check_id %}
<span class="btn btn-success btn-sm checked-indicator">✔ Checked</span>
<button class="btn btn-secondary btn-sm js-note"
data-id="{{ site.id }}"
data-note="{{ site.user_note or '' }}">📝 Note</button>
<button class="btn btn-secondary btn-sm js-creds"
data-id="{{ site.id }}">🔑 Creds</button>
<form method="post"
action="{{ url_for('user_dashboard.uncheck_site', website_id=site.id) }}"
style="display:inline">
<button class="btn btn-secondary btn-sm js-undo" type="button"
data-name="{{ site.name }}">↩ Undo</button>
</form>
{% else %}
<button class="btn btn-primary btn-sm js-check"
data-id="{{ site.id }}"
data-name="{{ site.name }}">✔ Mark Checked</button>
<button class="btn btn-secondary btn-sm js-note"
data-id="{{ site.id }}"
data-note="">📝 Note</button>
<button class="btn btn-secondary btn-sm js-creds"
data-id="{{ site.id }}">🔑 Creds</button>
{% endif %}
</div>
<div class="sc-health">
<span class="health-dot" id="health-{{ site.id }}" title="Checking…"></span>
</div>
</div><!-- /sc-row1 -->
<!-- Row 2: user note (only shown when a note exists) -->
{% if site.user_note %}
<div class="sc-row2">
<span class="sc-note-row">📝 {{ site.user_note }}</span>
</div>
{% endif %}
</div><!-- /site-card -->
{% endfor %}
</div><!-- /site-group -->
{% endif %}
{% endfor %}
{% if not sites %}
<div class="empty-state">
<div class="empty-icon"></div>
<p>No sites to check today, or you're all done!</p>
</div>
{% endif %}
</div><!-- /site-list -->
<!-- ── Mark Checked Modal ───────────────────────────────────── -->
<div class="modal-overlay" id="modal-check">
<div class="modal">
<div class="modal-header">
<span class="modal-title" id="check-modal-title">Mark Checked</span>
<button class="modal-close" onclick="closeModal('modal-check')"></button>
</div>
<form method="post" id="check-form">
<div class="modal-body">
<div class="form-group">
<label>Note (optional)</label>
<textarea name="user_note" id="check-note" rows="3"
placeholder="Any observations or notes…"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-check')">Cancel</button>
<button type="submit" class="btn btn-success">✔ Confirm Checked</button>
</div>
</form>
</div>
</div>
<!-- ── Note Modal ──────────────────────────────────────────── -->
<div class="modal-overlay" id="modal-note">
<div class="modal">
<div class="modal-header">
<span class="modal-title">Update Note</span>
<button class="modal-close" onclick="closeModal('modal-note')"></button>
</div>
<form method="post" id="note-form">
<div class="modal-body">
<div class="form-group">
<label>Note</label>
<textarea name="user_note" id="note-text" rows="3"
placeholder="Any observations or notes…"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-note')">Cancel</button>
<button type="submit" class="btn btn-primary">Save Note</button>
</div>
</form>
</div>
</div>
<!-- ── Credentials Modal ────────────────────────────────────── -->
<div class="modal-overlay" id="modal-creds">
<div class="modal">
<div class="modal-header">
<span class="modal-title">🔑 Credentials</span>
<button class="modal-close" onclick="closeModal('modal-creds')"></button>
</div>
<div class="modal-body" id="creds-body">
<div class="text-center text-muted">Loading…</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('modal-creds')">Close</button>
</div>
</div>
</div>
<script>
/* ── Date ──────────────────────────────────────────────────── */
document.getElementById('today-date').textContent =
new Date().toLocaleDateString('en-US', {weekday:'long',year:'numeric',month:'long',day:'numeric'});
/* ── Group collapse/expand ─────────────────────────────────── */
var _collapsed = {};
function toggleGroup(name) {
var group = document.getElementById('group-' + name);
var toggle = document.getElementById('toggle-' + name);
if (!group) return;
_collapsed[name] = !_collapsed[name];
group.style.display = _collapsed[name] ? 'none' : '';
toggle.textContent = _collapsed[name] ? '▸' : '▾';
}
/* ── Search — shows matching cards, auto-expands collapsed groups ── */
document.getElementById('site-search').addEventListener('input', function() {
var q = this.value.trim().toLowerCase();
document.querySelectorAll('.site-card').forEach(function(card) {
var match = !q || card.dataset.name.includes(q) || card.dataset.url.includes(q);
card.style.display = match ? '' : 'none';
// Auto-expand the group containing a matched card
if (match && q) {
var group = card.closest('.site-group');
if (group) {
group.style.display = '';
var name = group.id.replace('group-', '');
var toggle = document.getElementById('toggle-' + name);
if (toggle) toggle.textContent = '▾';
_collapsed[name] = false;
}
}
});
});
/* ── Bulk select ───────────────────────────────────────────── */
document.getElementById('btn-select-all').addEventListener('click', function() {
document.querySelectorAll('.bulk-cb:not(:disabled)').forEach(function(cb) {
var card = cb.closest('.site-card');
if (!card.classList.contains('is-checked') && card.style.display !== 'none')
cb.checked = true;
});
});
/* ── Bulk check ────────────────────────────────────────────── */
document.getElementById('btn-bulk-check').addEventListener('click', function() {
var selected = Array.from(document.querySelectorAll('.bulk-cb:checked')).map(function(cb){ return cb.dataset.id; });
if (!selected.length) { alert('No sites selected. Tick the checkboxes first.'); return; }
if (!confirm('Mark ' + selected.length + ' site(s) as checked?')) return;
Promise.all(selected.map(function(id) {
return fetch('/dashboard/check/' + id, {
method: 'POST', credentials: 'same-origin',
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrfToken()},
body: 'user_note='
});
})).then(function() { location.reload(); });
});
/* ── Delegated button handler ──────────────────────────────── */
document.getElementById('site-list').addEventListener('click', function(e) {
var btn = e.target.closest('button[class]');
if (!btn) return;
/* Mark Checked */
if (btn.classList.contains('js-check')) {
document.getElementById('check-modal-title').textContent = 'Mark Checked — ' + btn.dataset.name;
document.getElementById('check-form').action = '/dashboard/check/' + btn.dataset.id;
document.getElementById('check-note').value = '';
openModal('modal-check');
return;
}
/* Note */
if (btn.classList.contains('js-note')) {
document.getElementById('note-form').action = '/dashboard/note/' + btn.dataset.id;
document.getElementById('note-text').value = btn.dataset.note || '';
openModal('modal-note');
return;
}
/* Credentials */
if (btn.classList.contains('js-creds')) {
var body = document.getElementById('creds-body');
body.innerHTML = '<div class="text-center text-muted">Loading…</div>';
openModal('modal-creds');
fetch('/dashboard/credentials/' + btn.dataset.id)
.then(function(r){ return r.json(); })
.then(function(creds) {
if (!creds.length) {
body.innerHTML = '<p class="text-muted text-center">No credentials stored.</p>';
return;
}
body.innerHTML = creds.map(function(c, i) {
var uid = 'pw-' + i + '-' + btn.dataset.id;
return '<div class="' + (i > 0 ? 'mt-2' : '') + '">'
+ (c.label ? '<div class="cred-label-heading">' + esc(c.label) + '</div>' : '')
+ '<div class="cred-row"><span class="cred-key">Username</span>'
+ '<span class="cred-val">' + esc(c.username || '—') + '</span>'
+ '<button class="btn btn-secondary btn-sm" data-copy="' + esc(c.username) + '">📋</button></div>'
+ '<div class="cred-row"><span class="cred-key">Password</span>'
+ '<span class="cred-val pw-mask" id="' + uid + '">' + esc(c.password) + '</span>'
+ '<button class="btn btn-secondary btn-sm" data-toggle-pw="' + uid + '">👁</button>'
+ '<button class="btn btn-secondary btn-sm" data-copy="' + esc(c.password) + '">📋</button></div>'
+ '</div>'
+ (i < creds.length - 1 ? '<hr class="divider">' : '');
}).join('');
})
.catch(function(){ body.innerHTML = '<p class="alert alert-danger">Failed to load.</p>'; });
return;
}
/* Undo */
if (btn.classList.contains('js-undo')) {
if (confirm('Remove check for ' + btn.dataset.name + '?')) {
btn.closest('form').submit();
}
return;
}
});
/* ── Credential modal actions ──────────────────────────────── */
document.getElementById('modal-creds').addEventListener('click', function(e) {
var copyBtn = e.target.closest('[data-copy]');
if (copyBtn) { copyToClipboard(copyBtn.dataset.copy, copyBtn); return; }
var toggleBtn = e.target.closest('[data-toggle-pw]');
if (toggleBtn) {
var el = document.getElementById(toggleBtn.dataset.togglePw);
if (el) {
var masked = el.classList.toggle('pw-mask');
toggleBtn.textContent = masked ? '👁' : '🙈';
}
}
});
/* ── HTML escape ───────────────────────────────────────────── */
function esc(str) {
return String(str || '').replace(/[&<>"']/g, function(c) {
return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
});
}
/* ── Health dots ───────────────────────────────────────────── */
document.querySelectorAll('.site-card').forEach(function(card) {
var id = card.dataset.id;
var url = card.dataset.url;
var dot = document.getElementById('health-' + id);
if (!dot || !url) return;
var img = new Image();
var t0 = Date.now();
img.onload = function() {
var ms = Date.now() - t0;
dot.style.color = ms > 3000 ? '#d97706' : '#16a34a';
dot.title = ms > 3000 ? 'Slow (' + ms + 'ms)' : 'Reachable (' + ms + 'ms)';
};
img.onerror = function() {
var ms = Date.now() - t0;
dot.style.color = ms < 6000 ? '#d97706' : '#dc2626';
dot.title = ms < 6000 ? 'Reachable (restricted)' : 'Unreachable';
};
img.src = 'https://www.google.com/s2/favicons?domain=' + encodeURIComponent(url) + '&t=' + Date.now();
});
</script>
<style>
/* ── Group header ───────────────────────────────────────────── */
.group-header {
display:flex; align-items:center; gap:.65rem;
padding:.6rem 1rem; margin-bottom:2px; margin-top:.75rem;
background:var(--bg-subtle); border:1px solid var(--border);
border-radius:var(--r); cursor:pointer;
transition:background var(--t);
user-select:none;
}
.group-header:first-child { margin-top:0; }
.group-header:hover { background:var(--bg-hover); }
.group-toggle { font-size:.8rem; color:var(--text-muted); width:12px; flex-shrink:0; }
.group-label { font-size:.85rem; font-weight:700; color:var(--text);
text-transform:uppercase; letter-spacing:.05em; }
.group-count { font-size:.8rem; color:var(--text-muted); margin-left:.25rem; }
.group-mini-bar {
flex:1; height:5px; background:var(--border); border-radius:999px;
overflow:hidden; max-width:120px;
}
.group-mini-fill {
height:100%; border-radius:999px; background:var(--accent);
transition:width .4s ease;
}
.group-mini-fill.fill-success { background:var(--success); }
.group-mini-fill.fill-warning { background:var(--warning); }
.group-mini-fill.fill-danger { background:var(--danger); }
/* ── Site group wrapper ─────────────────────────────────────── */
.site-group { display:flex; flex-direction:column; gap:.45rem; margin-bottom:.5rem; }
/* ── Site card ──────────────────────────────────────────────── */
.site-card {
background:var(--bg-white); border:1px solid var(--border);
border-radius:var(--r-lg); box-shadow:var(--shadow-sm);
transition:box-shadow var(--t), border-color var(--t);
overflow:hidden;
}
.site-card:hover { box-shadow:var(--shadow); border-color:var(--border-strong); }
.site-card.is-checked { border-color:var(--success-border); background:#fafffe; }
.site-card.is-checked:hover { border-color:#86efac; }
/* ── Row 1: main content ────────────────────────────────────── */
.sc-row1 {
display:flex; align-items:center; gap:.85rem; padding:.95rem 1rem;
}
/* ── Row 2: note row ────────────────────────────────────────── */
.sc-row2 {
padding:.45rem 1rem .6rem calc(1rem + 15px + .85rem + 26px + .85rem);
/* indent to align with sc-body, past checkbox + status */
border-top:1px dashed var(--border);
}
.sc-note-row {
font-size:.875rem; color:var(--warning);
background:var(--warning-bg); border:1px solid var(--warning-border);
border-radius:var(--r-sm); padding:.25rem .65rem;
display:inline-block; line-height:1.5;
}
/* ── Font size bumps ─────────────────────────────────────────── */
.sc-name { font-size:1rem; }
.sc-url { font-size:.76rem; }
.sc-meta-item{ font-size:.82rem; }
.sc-checked-time { font-size:.78rem; color:var(--success); font-weight:600; }
@media(max-width:700px) {
.sc-row1 { flex-wrap:wrap; }
.sc-actions { margin-left:0; flex-direction:column; align-items:flex-start; }
.sc-url { display:none; }
.sc-row2 { padding-left:1rem; }
}
</style>
{% endblock %}