Illustrations/web/templates/settings/home.html

428 lines
18 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" %}
{% load static %}
{% block body_class %}{% endblock %}
{% block content %}
<div class="container settings-console">
<!-- Header -->
<div class="cc-header">
<h1 class="cc-title">Command Center</h1>
</div>
<!-- 3-Column Grid -->
<div class="cc-grid">
<!-- Appearance -->
<section class="card cc-panel">
<div class="cc-panel-head">
<div class="cc-kicker">Appearance</div>
<h2 class="cc-panel-title">Theme</h2>
</div>
<div class="cc-panel-body">
{# Form wraps the tiles and the Save button. A hidden input carries the choice. #}
<form method="post" action="{% url 'set_theme' %}" class="cc-form" onsubmit="return saveTheme()">
{% csrf_token %}
<input type="hidden" id="theme-hidden" name="theme" value="{{ request.session.theme|default:'classic' }}">
<div class="swatch-grid">
{% for t in available_themes %}
<button type="button" class="swatch" data-theme="{{ t }}" aria-label="Preview {{ t|capfirst }}">
<span class="swatch-name">{{ t|capfirst }}</span>
</button>
{% endfor %}
</div>
<div class="cc-actions">
<button class="btn btn-primary">Save</button>
</div>
</form>
</div>
</section>
<!-- Reading -->
<section class="card cc-panel">
<div class="cc-panel-head">
<div class="cc-kicker">Reading</div>
</div>
<div class="cc-panel-body">
<div class="cc-row">
<label class="cc-label">Highlight search hits</label>
<label class="switch">
<input id="highlightHitsToggle" type="checkbox">
<span class="slider"></span>
</label>
</div>
<!-- Swipe navigation (client-side pref) -->
<div class="cc-row" style="margin-top:10px;">
<label class="cc-label">Swipe to navigate entries (mobile)</label>
<label class="switch">
<input id="swipeNavToggle" type="checkbox">
<span class="slider"></span>
</label>
</div>
</div>
</section>
<!-- Privacy -->
<section class="card cc-panel">
<div class="cc-panel-head">
<div class="cc-kicker">Privacy</div>
</div>
<div class="cc-panel-body">
<button id="clear-history-btn" class="btn btn-danger">Clear My History</button>
</div>
</section>
{% if user.is_authenticated and user.is_superuser %}
<!-- Security (accented) -->
<section class="card cc-panel cc-sec">
<div class="cc-sec-bg"></div>
<div class="cc-panel-head">
<div class="cc-kicker cc-kicker-sec">Security</div>
<h2 class="cc-panel-title">Monitoring</h2>
</div>
<div class="cc-panel-body">
<div class="sec-grid">
<a class="sec-tile" href="{% url 'login_attempts' %}">
<div class="sec-icon">
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path fill="currentColor" d="M12 2l7 3v6c0 5-3.8 9.7-7 11-3.2-1.3-7-6-7-11V5l7-3z"/></svg>
</div>
<div class="sec-meta">
<div class="sec-title">Login Attempts</div>
<div class="sec-sub">Last 7 days</div>
</div>
<div class="sec-cta">Open →</div>
</a>
<a class="sec-tile" href="{% url 'audit_log' %}">
<div class="sec-icon">
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path fill="currentColor" d="M3 12h3l2 7 4-14 3 10h6"/></svg>
</div>
<div class="sec-meta">
<div class="sec-title">Audit Log</div>
<div class="sec-sub">Latest 100</div>
</div>
<div class="sec-cta">Open →</div>
</a>
</div>
</div>
</section>
<!-- Release Announcement -->
<section class="card cc-panel">
<div class="cc-panel-head">
<div class="cc-kicker">Comms</div>
<h2 class="cc-panel-title">Release Announcement</h2>
</div>
<div class="cc-panel-body">
<form id="announcement-form" method="post" action="{% url 'announcement_tools' %}" class="cc-form">
{% csrf_token %}
<label class="cc-label">Title</label>
<input type="text" name="title" class="tool-input" id="annc-title" />
<label class="cc-label">Message</label>
<textarea name="message" rows="5" class="tool-input" id="annc-message" placeholder="Whats new…"></textarea>
<!-- Always active -->
<input type="hidden" name="is_active" value="on" />
<div class="cc-actions">
<button type="button" id="annc-preview-btn" class="btn">Preview</button>
<button class="btn btn-primary">Publish</button>
</div>
</form>
{% if announcements_recent %}
<div class="cc-table-wrap">
<table class="table small">
<thead>
<tr><th>ID</th><th>Title</th><th>Active?</th><th>Window</th><th>Created</th></tr>
</thead>
<tbody>
{% for a in announcements_recent %}
<tr>
<td>#{{ a.id }}</td>
<td>{{ a.title|default:"(untitled)" }}</td>
<td>{{ a.is_current|yesno:"✅,—" }}</td>
<td>
{{ a.start_at|date:"Y-m-d H:i" }}
{% if a.end_at %} → {{ a.end_at|date:"Y-m-d H:i" }}{% endif %}
</td>
<td>{{ a.created_at|date:"Y-m-d H:i" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</section>
<!-- Publication Codes (JSON) superusers only -->
<section class="card cc-panel">
<div class="cc-panel-head">
<div class="cc-kicker">Comms</div>
<h2 class="cc-panel-title">Publication Codes (JSON)</h2>
</div>
<div class="cc-panel-body">
<div class="cc-row">
<label class="cc-label">Edit <code>web/static/data/wol-pub-codes.v1.json</code></label>
<textarea id="pubCodesEditor" class="tool-input" rows="14"
style="width:100%; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;"></textarea>
</div>
<div class="cc-actions">
<button type="button" id="pubCodesReloadBtn" class="btn">Reload</button>
<button type="button" id="pubCodesSaveBtn" class="btn btn-primary">Save</button>
</div>
<div id="pubCodesStatus" class="tiny" style="margin-top:6px; color:#64748b;"></div>
</div>
</section>
{% endif %}
</div>
</div>
<!-- Announcement Preview Modal -->
<div id="annc-preview-modal" style="position:fixed;inset:0;display:none;align-items:center;justify-content:center;z-index:10000;background:rgba(15,23,42,.35);backdrop-filter:blur(2px);">
<div role="dialog" aria-modal="true" aria-labelledby="annc-prev-title" class="card" style="max-width:680px;width:min(92vw,680px);padding:18px 18px 12px;border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,.25);background:#fff;position:relative;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
<div style="font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:#64748b;font-weight:700;">Announcement Preview</div>
<button type="button" id="annc-prev-close" class="btn btn-secondary">Close</button>
</div>
<div style="border:1px solid #e5e7eb;border-radius:12px;padding:14px;background:linear-gradient(180deg,#ffffff, #fbfdff);">
<div id="annc-prev-title" style="font-weight:800;font-size:18px;color:#0f172a;margin-bottom:6px;"></div>
<div id="annc-prev-message" style="font-size:15px;line-height:1.45;color:#111827;white-space:pre-wrap;"></div>
</div>
</div>
</div>
<!-- Toast for Clear History -->
<div id="toast-clear-history"
style="position:fixed; right:16px; bottom:16px; padding:10px 14px; border-radius:10px;
background:#111827; color:#fff; box-shadow:0 6px 20px rgba(0,0,0,.25);
opacity:0; pointer-events:none; transition:opacity .25s;">
History cleared.
</div>
<style>
/* (unchanged styles from your file) */
.settings-console .cc-title{margin:0;font-size:24px;font-weight:700;color:#0f172a}
.settings-console .cc-subtitle{color:#64748b;margin-top:6px}
.cc-header{margin:6px 0 14px}
.cc-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;}
@media (max-width: 1020px){ .cc-grid{ grid-template-columns: 1fr 1fr; } }
@media (max-width: 720px){ .cc-grid{ grid-template-columns: 1fr; } }
.cc-panel{padding:16px 16px 14px}
.cc-panel-head{margin-bottom:8px}
.cc-kicker{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#64748b}
.cc-kicker-sec{color:#22c55e}
.cc-panel-title{margin:2px 0 0;font-size:18px;font-weight:700;color:#0f172a}
.cc-panel-body{margin-top:8px}
.cc-label{display:block;font-weight:600;margin:8px 0 6px;color:#0f172a}
.cc-actions{display:flex;align-items:center;gap:10px;margin-top:10px}
.cc-form .tool-input{border:1px solid var(--border,#d1d5db);border-radius:10px;padding:8px 10px;font-size:14px}
/* make announcement inputs full-width */
.cc-form .tool-input{width:100%;}
textarea.tool-input{min-height:140px;resize:vertical;width:100%;}
/* Switch */
.switch{position:relative;display:inline-block;width:46px;height:26px}
.switch input{display:none}
.slider{position:absolute;inset:0;background:#e5e7eb;border-radius:999px;transition:all .15s;}
.slider:before{content:"";position:absolute;height:20px;width:20px;left:3px;top:3px;background:white;border-radius:50%;transition:all .15s;box-shadow:0 2px 6px rgba(0,0,0,.15);}
.switch input:checked + .slider{background:#22c55e;}
.switch input:checked + .slider:before{transform:translateX(20px);}
/* Swatches */
.swatch-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:12px;}
@media (max-width:720px){.swatch-grid{grid-template-columns:repeat(2,minmax(0,1fr));}}
.swatch{position:relative;display:flex;align-items:flex-end;justify-content:flex-start;height:64px;border:1px solid var(--border,#e5e7eb);border-radius:12px;padding:8px;background:linear-gradient(135deg,#eef2f7,#dfe7f2);cursor:pointer;}
.swatch[data-theme="classic"]{background:linear-gradient(110deg,#d7b592 0%,#e7e3db 35%,#8fbfe0 100%);}
.swatch[data-theme="dawn"]{background:linear-gradient(135deg,#ffd9a0 0%,#ffb6b9 40%,#a3d5ff 100%);}
.swatch[data-theme="midnight"]{background:linear-gradient(135deg,#0b1220,#1c2741);}
.swatch[data-theme="forest"]{background:linear-gradient(135deg,#d7f3e2,#92c7a3);}
.swatch[data-theme="sandstone"]{background:linear-gradient(135deg,#f7efe4,#e4d2b6);}
.swatch-name{background:rgba(255,255,255,.8);padding:2px 6px;border-radius:8px;font-size:12px;color:#0f172a;}
/* Security accent */
.cc-sec{position:relative;overflow:hidden;color:#0f172a;}
.cc-sec .cc-panel-title{color:#0f172a;}
.cc-sec .cc-kicker{color:#16a34a;}
.cc-sec-bg{position:absolute;inset:-1px;z-index:0;background:radial-gradient(800px 300px at 90% -20%,rgba(34,197,94,.25),transparent 55%),linear-gradient(135deg,rgba(34,197,94,.10),rgba(59,130,246,.06));pointer-events:none;}
.cc-sec > *{position:relative;z-index:1;}
.sec-grid{display:grid;grid-template-columns:1fr;gap:10px;}
.sec-tile{display:flex;align-items:center;gap:12px;padding:12px;border:1px solid rgba(34,197,94,.25);border-radius:12px;background:rgba(255,255,255,.8);text-decoration:none;color:inherit;transition:transform .12s,box-shadow .12s,border-color .12s;}
.sec-tile:hover{transform:translateY(-2px);box-shadow:0 10px 24px rgba(16,24,40,.08);border-color:rgba(34,197,94,.45);}
.sec-icon{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:10px;background:rgba(34,197,94,.18);color:#065f46;}
.sec-title{font-weight:700;}
.sec-sub{font-size:12px;color:#64748b;}
.sec-meta{flex:1;}
.sec-cta{font-size:13px;color:#0f172a;opacity:.7}
.tiny{font-size:12px}
</style>
<script>
(function(){
// Helpers already present in your file (unchanged)
function getCookie(name){ const m=document.cookie.match("(^|;)\\s*"+name+"\\s*=\\s*([^;]+)"); return m?m.pop():""; }
function setTheme(name){
var link=document.getElementById('theme-css');
if(link) link.href='{% static "themes/" %}'+name+'.css';
document.documentElement.setAttribute('data-theme',name);
try{ localStorage.setItem('theme',name); }catch(e){}
if(name==='classic'){ document.body.classList.add('themed-bg'); } else { document.body.classList.remove('themed-bg'); }
var hidden=document.getElementById('theme-hidden'); if(hidden) hidden.value=name;
}
// Clear history (unchanged)
(function(){
var btn=document.getElementById("clear-history-btn");
var toast=document.getElementById("toast-clear-history");
if(!btn) return;
btn.addEventListener("click",function(){
fetch("{% url 'clear_history' %}",{method:"POST",headers:{"X-CSRFToken":getCookie("csrftoken")}})
.then(r=>r.ok?r.json():Promise.reject())
.then(()=>{
const keys=["recent_searches","recent_entries","recent_viewed","recently_viewed","recent_results","recentSearches","recentEntries"];
try{ keys.forEach(k=>{localStorage.removeItem(k);sessionStorage.removeItem(k);document.cookie=k+"=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";}); }catch(e){}
if(toast){ toast.style.opacity="1"; setTimeout(()=>toast.style.opacity="0",1500); }
}).catch(()=>{});
});
})();
// Save from tiles (unchanged)
window.saveTheme = function(){ return true; };
// Swatch instant preview (unchanged)
document.querySelectorAll('.swatch').forEach(btn=>{
btn.addEventListener('click',()=>{
const name=btn.getAttribute('data-theme');
setTheme(name);
});
});
// Highlight toggle (server-backed, unchanged)
(async function(){
const toggle=document.getElementById("highlightHitsToggle");
if(!toggle) return;
try{
const res=await fetch("{% url 'api_get_prefs' %}");
const data=await res.json();
if(typeof data.highlight_search_hits!=="undefined") toggle.checked=!!data.highlight_search_hits;
}catch(e){}
toggle.addEventListener("change",()=>{
const form=new FormData();
form.append("enabled", toggle.checked ? "true" : "false");
fetch("{% url 'api_set_highlight_hits' %}", { method:"POST", body:form, headers:{ "X-CSRFToken": getCookie("csrftoken") }})
.catch(()=>alert("Could not save the setting. Please try again."));
});
})();
// === Announcement Preview ===
(function(){
const form = document.getElementById('announcement-form');
const btn = document.getElementById('annc-preview-btn');
const modal = document.getElementById('annc-preview-modal');
const close = document.getElementById('annc-prev-close');
const titleI = document.getElementById('annc-title');
const msgI = document.getElementById('annc-message');
const titleO = document.getElementById('annc-prev-title');
const msgO = document.getElementById('annc-prev-message');
if (!form || !btn || !modal) return;
function openModal(){
modal.style.display = 'flex';
document.documentElement.style.overflow = 'hidden';
}
function closeModal(){
modal.style.display = 'none';
document.documentElement.style.overflow = '';
}
btn.addEventListener('click', function(){
const t = (titleI?.value || '').trim();
const m = (msgI?.value || '').trim();
if (!m){
alert('Please enter a Message to preview.');
return;
}
titleO.textContent = t || 'Release Notes';
msgO.textContent = m;
openModal();
});
(close||modal).addEventListener('click', (ev)=>{
if (ev.target === modal || ev.target === close) closeModal();
});
window.addEventListener('keydown', (e)=>{ if (e.key === 'Escape' && modal.style.display === 'flex') closeModal(); });
})();
// === Publication Codes Editor (superuser) ===
(function(){
const ta = document.getElementById('pubCodesEditor');
const btnR = document.getElementById('pubCodesReloadBtn');
const btnS = document.getElementById('pubCodesSaveBtn');
const stat = document.getElementById('pubCodesStatus');
if (!ta || !btnR || !btnS) return; // not superuser or card missing
function setStatus(msg, ok=true){
if (!stat) return;
stat.textContent = msg;
stat.style.color = ok ? '#64748b' : '#b91c1c';
}
async function reloadJSON(){
try{
setStatus('Loading…');
const r = await fetch('/static/data/wol-pub-codes.v1.json', { cache:'no-store' });
if (!r.ok) throw new Error('HTTP '+r.status);
const data = await r.json();
ta.value = JSON.stringify(data, null, 2);
setStatus('Loaded.');
}catch(err){
setStatus('Failed to load JSON: ' + (err?.message||err), false);
}
}
async function saveJSON(){
let parsed;
try{
parsed = JSON.parse(ta.value);
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.pub_codes))
throw new Error('JSON must be an object with a "pub_codes" array.');
}catch(err){
setStatus('Invalid JSON: ' + (err?.message||err), false);
return;
}
try{
setStatus('Saving…');
const fd = new FormData();
fd.append('json', JSON.stringify(parsed));
const r = await fetch("{% url 'api_update_pub_codes' %}", {
method: 'POST',
body: fd,
credentials: 'same-origin',
headers: { 'X-CSRFToken': (document.cookie.match(/(^|;)\s*csrftoken\s*=\s*([^;]+)/)||[]).pop() || '' }
});
if (!r.ok) {
const txt = await r.text().catch(()=>String(r.status));
throw new Error(txt.slice(0,200));
}
const out = await r.json().catch(()=>({ok:false}));
if (!out.ok) throw new Error('Server rejected the update.');
setStatus('Saved. ' + out.count + ' codes.');
}catch(err){
setStatus('Save failed: ' + (err?.message||err), false);
}
}
btnR.addEventListener('click', reloadJSON);
btnS.addEventListener('click', saveJSON);
reloadJSON(); // initial load
})();
})();
</script>
<!-- NEW: client-side swipe preference wiring -->
<script src="{% static 'js/settings-swipe.js' %}"></script>
{% endblock %}