patchcenter/app/templates/base.html
Khalid MOUTAOUAKIL e96d79aae3 QuickWin: prereq/snapshot services, referentiel, logs, correspondance
- Split quickwin services: prereq, snapshot, log services
- Add referentiel router and template
- QuickWin detail: prereq/snapshot terminal divs for production
- Server edit partial updates
- QuickWin correspondance and logs templates
- Base template updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 18:13:00 +02:00

128 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ app_name }} - {% block title %}{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="stylesheet" href="/static/css/tailwind.css">
<script src="/static/js/htmx.min.js"></script>
<script src="/static/js/alpine.min.js" defer></script>
<style>
body { background: #0a0e17; color: #e2e8f0; font-family: 'Segoe UI', system-ui, sans-serif; }
.sidebar { background: #111827; border-right: 1px solid #1e3a5f; }
.card { background: #111827; border: 1px solid #1e3a5f; border-radius: 8px; }
.btn-primary { background: #00d4ff; color: #0a0e17; font-weight: 600; border-radius: 6px; }
.btn-primary:hover { background: #00b8e6; }
.btn-danger { background: #ff3366; color: white; border-radius: 6px; }
.btn-sm { padding: 2px 10px; font-size: 0.75rem; border-radius: 4px; cursor: pointer; }
.table-cyber th { background: #1e3a5f; color: #00d4ff; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; }
.table-cyber td { border-bottom: 1px solid #1e3a5f; font-size: 0.8rem; }
.table-cyber tr:hover { background: #1a2332; cursor: pointer; }
.table-cyber tr.selected { background: #1e3a5f44; }
.badge { padding: 2px 8px; border-radius: 12px; font-size: 0.65rem; font-weight: 600; }
.badge-green { background: #00ff8822; color: #00ff88; }
.badge-red { background: #ff336622; color: #ff3366; }
.badge-yellow { background: #ffcc0022; color: #ffcc00; }
.badge-blue { background: #00d4ff22; color: #00d4ff; }
.badge-gray { background: #4a556822; color: #94a3b8; }
input, select, textarea { background: #0a0e17; border: 1px solid #1e3a5f; color: #e2e8f0; border-radius: 6px; padding: 6px 12px; font-size: 0.85rem; }
input:focus, select:focus, textarea:focus { outline: none; border-color: #00d4ff; box-shadow: 0 0 0 2px #00d4ff33; }
.panel-slide { transition: transform 0.3s ease, opacity 0.3s ease; }
.htmx-indicator { opacity: 0; transition: opacity 200ms; }
.htmx-request .htmx-indicator { opacity: 1; }
.inline-edit { background: transparent; border: 1px solid transparent; padding: 2px 4px; }
.inline-edit:hover { border-color: #1e3a5f; }
.inline-edit:focus { background: #0a0e17; border-color: #00d4ff; }
.toast { position: fixed; bottom: 20px; right: 20px; padding: 12px 24px; border-radius: 8px; z-index: 1000; animation: fadeIn 0.3s; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
</style>
</head>
<body class="min-h-screen" hx-headers='{"X-Requested-With": "htmx"}'>
{% if user %}
<div class="flex min-h-screen">
<aside class="sidebar w-52 flex-shrink-0 flex flex-col">
<div class="p-4 border-b border-cyber-border">
<div class="flex items-center gap-2">
<img src="/static/logo_sanef.jpg" alt="SANEF" class="h-8 rounded" style="opacity:0.85">
</div>
<h1 class="text-cyber-accent font-bold text-lg mt-1">PatchCenter</h1>
<p class="text-xs text-gray-500">v2.0 — SecOps</p>
</div>
<nav class="flex-1 p-3 space-y-1">
{% set p = perms if perms is defined else request.state.perms %}
{% if p.servers or p.qualys or p.audit or p.planning %}<a href="/dashboard" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'dashboard' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Dashboard</a>{% endif %}
{% if p.servers %}<a href="/servers" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if request.url.path == '/servers' or '/servers/' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Serveurs</a>{% endif %}
{% if p.specifics %}<a href="/specifics" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'specifics' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Specifiques</a>{% endif %}
{% if p.campaigns %}<a href="/campaigns" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'campaigns' in request.url.path and 'assignments' not in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Campagnes</a>{% endif %}
{% if p.campaigns in ('edit', 'admin') %}<a href="/assignments" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'assignments' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Assignations</a>{% endif %}
{% if p.qualys %}<a href="/qualys/search" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if '/qualys/' in request.url.path and 'tags' not in request.url.path and 'decoder' not in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Qualys</a>{% endif %}
{% if p.qualys %}<a href="/qualys/tags" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if '/qualys/tags' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Tags</a>{% endif %}
{% if p.qualys %}<a href="/qualys/decoder" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'decoder' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Décodeur</a>{% endif %}
{% if p.qualys %}<a href="/qualys/agents" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'agents' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Agents</a>{% endif %}
{% if p.campaigns %}<a href="/safe-patching" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'safe-patching' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Safe Patching</a>{% endif %}
{% if p.campaigns or p.quickwin %}<a href="/quickwin" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'quickwin' in request.url.path and 'safe' not in request.url.path and 'config' not in request.url.path and 'correspondance' not in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">QuickWin</a>{% endif %}
{% if p.campaigns or p.quickwin %}<a href="/quickwin/config" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if '/quickwin/config' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Config exclusions</a>{% endif %}
{% if p.campaigns or p.quickwin %}<a href="/quickwin/correspondance" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'correspondance' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Correspondance</a>{% endif %}
{% if p.planning %}<a href="/planning" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'planning' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Planning</a>{% endif %}
{% if p.audit %}<a href="/audit" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if request.url.path == '/audit' %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Audit</a>{% endif %}
{% if p.audit in ('edit', 'admin') %}<a href="/audit/specific" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'specific' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Spécifique</a>{% endif %}
{% if p.audit %}<a href="/audit-full" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if request.url.path == '/audit-full' or (request.url.path.startswith('/audit-full/') and 'patching' not in request.url.path and 'flow-map' not in request.url.path) %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Complet</a>{% endif %}
{% if p.audit %}<a href="/audit-full/patching" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'patching' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Patching</a>{% endif %}
{% if p.servers %}<a href="/contacts" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'contacts' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Contacts</a>{% endif %}
{% if p.users %}<a href="/users" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'users' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Utilisateurs</a>{% endif %}
{% if p.settings %}<a href="/settings" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'settings' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Settings</a>{% endif %}
{% if p.settings %}<a href="/referentiel" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'referentiel' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">R&eacute;f&eacute;rentiel</a>{% endif %}
</nav>
</aside>
<main class="flex-1 flex flex-col overflow-hidden">
<!-- Top bar -->
<header class="flex items-center justify-end px-6 py-2 border-b border-cyber-border bg-cyber-card">
<div class="flex items-center gap-3">
<span class="text-sm text-gray-400">{{ user.sub }}</span>
<span class="badge badge-blue">{{ user.role }}</span>
<a href="/logout" class="btn-sm bg-cyber-border text-gray-300 hover:bg-red-900/40 hover:text-cyber-red transition-colors">Deconnexion</a>
</div>
</header>
<div class="flex flex-1 overflow-hidden">
<div class="flex-1 p-6 overflow-auto" id="main-content">
{% block content %}{% endblock %}
</div>
<div id="detail-panel" class="w-0 overflow-hidden transition-all duration-300 border-l border-cyber-border bg-cyber-card">
</div>
</div>
</main>
</div>
{% else %}
{% block fullpage %}{% endblock %}
{% endif %}
<div id="toast-container"></div>
<!-- Overlay chargement -->
<div id="loading-overlay" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(10,14,23,0.85); z-index:9999; justify-content:center; align-items:center;">
<div style="text-align:center">
<div style="border:3px solid #1e3a5f; border-top:3px solid #00d4ff; border-radius:50%; width:40px; height:40px; animation:spin 1s linear infinite; margin:0 auto 16px"></div>
<div id="loading-msg" style="color:#00d4ff; font-size:14px; font-weight:600">Opération en cours...</div>
<div id="loading-sub" style="color:#94a3b8; font-size:12px; margin-top:6px"></div>
</div>
</div>
<style>@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }</style>
<script>
function showLoading(msg, sub) {
document.getElementById('loading-msg').textContent = msg || 'Opération en cours...';
document.getElementById('loading-sub').textContent = sub || '';
document.getElementById('loading-overlay').style.display = 'flex';
}
function hideLoading() { document.getElementById('loading-overlay').style.display = 'none'; }
// Auto-attach: tout bouton avec data-loading affiche l'overlay au clic
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('[data-loading]').forEach(function(btn) {
btn.addEventListener('click', function(e) {
var parts = (btn.dataset.loading || 'Opération en cours...|').split('|');
showLoading(parts[0], parts[1] || '');
});
});
});
</script>
</body>
</html>