423 lines
16 KiB
Twig
423 lines
16 KiB
Twig
<!DOCTYPE html>
|
|
<html lang="fr" data-bs-theme="dark">
|
|
<head>
|
|
<link rel="icon" type="image/svg+xml" data-favicon="yb" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 64 64%22%3E%3Ctitle%3EServerControl%3C/title%3E%3Crect width=%2264%22 height=%2264%22 rx=%2214%22 fill=%22#0d6efd%22/%3E%3Cg fill=%22#fff%22%3E%3Crect x=%2213%22 y=%2212%22 width=%2238%22 height=%2211%22 rx=%223.5%22/%3E%3Crect x=%2213%22 y=%2226.5%22 width=%2238%22 height=%2211%22 rx=%223.5%22/%3E%3Crect x=%2213%22 y=%2241%22 width=%2238%22 height=%2211%22 rx=%223.5%22/%3E%3C/g%3E%3Cg fill=%22#0d6efd%22%3E%3Ccircle cx=%2219.5%22 cy=%2217.5%22 r=%222%22/%3E%3Ccircle cx=%2219.5%22 cy=%2232%22 r=%222%22/%3E%3Ccircle cx=%2219.5%22 cy=%2246.5%22 r=%222%22/%3E%3Crect x=%2234%22 y=%2216%22 width=%2212%22 height=%223%22 rx=%221.5%22/%3E%3Crect x=%2234%22 y=%2230.5%22 width=%2212%22 height=%223%22 rx=%221.5%22/%3E%3C/g%3E%3Ccircle cx=%2240%22 cy=%2246.5%22 r=%222.4%22 fill=%22#22c55e%22/%3E%3Ccircle cx=%2246%22 cy=%2246.5%22 r=%222.4%22 fill=%22#0d6efd%22/%3E%3C/svg%3E">
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}ServerControl{% endblock %}</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--text: #e8ede8;
|
|
--background: #0b0f0c;
|
|
--primary: #b3c7b4;
|
|
--secondary: #4b435b;
|
|
--accent: #997595;
|
|
--sidebar-bg: #0d1210;
|
|
--card-bg: #111711;
|
|
--border: #1e2820;
|
|
--muted: #56695a;
|
|
--input-bg: #0d1210;
|
|
}
|
|
@media (prefers-color-scheme: light) {
|
|
:root {
|
|
--text: #121712;
|
|
--background: #f0f4f1;
|
|
--primary: #384c39;
|
|
--secondary: #aca4bc;
|
|
--accent: #8a6686;
|
|
--sidebar-bg: #e7ece8;
|
|
--card-bg: #ffffff;
|
|
--border: #d2dad3;
|
|
--muted: #7a8c7b;
|
|
--input-bg: #ffffff;
|
|
}
|
|
}
|
|
[data-bs-theme="dark"] {
|
|
--text: #e8ede8;
|
|
--background: #0b0f0c;
|
|
--primary: #b3c7b4;
|
|
--secondary: #4b435b;
|
|
--accent: #997595;
|
|
--sidebar-bg: #0d1210;
|
|
--card-bg: #111711;
|
|
--border: #1e2820;
|
|
--muted: #56695a;
|
|
--input-bg: #0d1210;
|
|
}
|
|
[data-bs-theme="light"] {
|
|
--text: #121712;
|
|
--background: #f0f4f1;
|
|
--primary: #384c39;
|
|
--secondary: #aca4bc;
|
|
--accent: #8a6686;
|
|
--sidebar-bg: #e7ece8;
|
|
--card-bg: #ffffff;
|
|
--border: #d2dad3;
|
|
--muted: #7a8c7b;
|
|
--input-bg: #ffffff;
|
|
}
|
|
|
|
body { background: var(--background); color: var(--text); }
|
|
|
|
.app-wrapper { display: flex; min-height: 100vh; }
|
|
.main-column { flex: 1; display: flex; flex-direction: column; min-height: 100vh; overflow-x: hidden; }
|
|
|
|
.sidebar {
|
|
width: 220px;
|
|
flex-shrink: 0;
|
|
min-height: 100vh;
|
|
padding: 1rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--sidebar-bg);
|
|
border-right: 1px solid var(--border);
|
|
}
|
|
.sidebar .brand {
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
text-decoration: none;
|
|
display: block;
|
|
margin-bottom: .75rem;
|
|
padding: .35rem .65rem;
|
|
border-radius: 6px;
|
|
color: var(--primary);
|
|
letter-spacing: -.01em;
|
|
}
|
|
.sidebar-top {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: .1rem .5rem;
|
|
margin-bottom: 1rem;
|
|
gap: .35rem;
|
|
}
|
|
.user-name {
|
|
font-size: .82rem;
|
|
font-weight: 500;
|
|
color: var(--muted);
|
|
}
|
|
.sidebar-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .3rem;
|
|
}
|
|
.theme-switch-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .25rem;
|
|
font-size: .68rem;
|
|
}
|
|
.theme-switch {
|
|
position: relative;
|
|
display: inline-block;
|
|
cursor: pointer;
|
|
margin: 0;
|
|
line-height: 0;
|
|
}
|
|
#theme-checkbox {
|
|
opacity: 0;
|
|
position: absolute;
|
|
z-index: -1;
|
|
width: 0;
|
|
height: 0;
|
|
margin: 0;
|
|
}
|
|
.theme-track {
|
|
display: inline-block;
|
|
width: 34px;
|
|
height: 20px;
|
|
border-radius: 20px;
|
|
background: var(--muted);
|
|
position: relative;
|
|
transition: background .22s ease-in-out;
|
|
vertical-align: middle;
|
|
}
|
|
.theme-track::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 3px;
|
|
left: 3px;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
background: #fff;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,.35);
|
|
transition: left .22s ease-in-out, transform .22s ease-in-out;
|
|
}
|
|
#theme-checkbox:checked + .theme-track { background: var(--accent); }
|
|
#theme-checkbox:checked + .theme-track::after {
|
|
left: calc(100% - 3px);
|
|
transform: translateX(-100%);
|
|
}
|
|
.theme-icon-sun { color: #c8a83a; }
|
|
.theme-icon-moon { color: var(--muted); }
|
|
|
|
.btn-logout {
|
|
width: 26px;
|
|
height: 26px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 6px;
|
|
text-decoration: none;
|
|
font-size: .82rem;
|
|
color: var(--muted);
|
|
transition: background .12s, color .12s;
|
|
flex-shrink: 0;
|
|
}
|
|
.btn-logout:hover { background: rgba(220,60,60,.1); color: #e05a5a; }
|
|
|
|
.sidebar-divider {
|
|
height: 1px;
|
|
background: var(--border);
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.sidebar .nav-link {
|
|
border-radius: 6px;
|
|
padding: .45rem .75rem;
|
|
font-size: .87rem;
|
|
color: var(--muted);
|
|
transition: background .12s, color .12s;
|
|
text-decoration: none;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.sidebar .nav-link:hover { color: var(--primary); background: rgba(179,199,180,.07); }
|
|
.sidebar .nav-link.active {
|
|
color: var(--accent);
|
|
background: rgba(153,117,149,.13);
|
|
box-shadow: inset 3px 0 0 var(--accent);
|
|
font-weight: 500;
|
|
}
|
|
.sidebar-section-label {
|
|
font-size: .62rem;
|
|
font-weight: 700;
|
|
letter-spacing: .1em;
|
|
text-transform: uppercase;
|
|
padding: .3rem .75rem .1rem;
|
|
display: block;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.card { background: var(--card-bg) !important; border-color: var(--border) !important; box-shadow: 0 2px 8px rgba(0,0,0,.2); }
|
|
.card-header { background: rgba(0,0,0,.15) !important; border-bottom-color: var(--border) !important; color: var(--primary); }
|
|
.card-footer { background: rgba(0,0,0,.1) !important; border-top-color: var(--border) !important; }
|
|
[data-bs-theme="light"] .card-header { background: rgba(0,0,0,.03) !important; }
|
|
[data-bs-theme="light"] .card-footer { background: rgba(0,0,0,.02) !important; }
|
|
|
|
thead th { border-bottom: 2px solid var(--border); font-size: .82rem; color: var(--muted); }
|
|
.table { color: var(--text); }
|
|
.table td, .table th { border-color: var(--border); }
|
|
[data-bs-theme="dark"] .table-light,
|
|
[data-bs-theme="dark"] .table-light th,
|
|
[data-bs-theme="dark"] .table-light td { color: #212529 !important; }
|
|
[data-bs-theme="light"] .text-white-50 { color: rgba(0,0,0,.45) !important; }
|
|
|
|
.form-control, .form-select { background-color: var(--input-bg); border-color: var(--border); color: var(--text); }
|
|
.form-control:focus, .form-select:focus {
|
|
background-color: var(--input-bg);
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px rgba(153,117,149,.2);
|
|
color: var(--text);
|
|
}
|
|
.input-group-text { background: var(--input-bg); border-color: var(--border); color: var(--muted); }
|
|
.form-section-title {
|
|
font-size: .7rem;
|
|
font-weight: 700;
|
|
letter-spacing: .1em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
margin-bottom: .75rem;
|
|
padding-bottom: .4rem;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
.field-icon { color: var(--muted); }
|
|
|
|
.modal-content { background: var(--card-bg); border-color: var(--border); color: var(--text); }
|
|
.modal-header, .modal-footer { border-color: var(--border); }
|
|
|
|
.status-online { color: #4caf78; }
|
|
.status-offline { color: #e05a5a; }
|
|
.status-unknown { color: var(--muted); }
|
|
|
|
[data-bs-theme="dark"] .btn-close { filter: invert(1); }
|
|
|
|
.terminal-titlebar {
|
|
background: #161b1a;
|
|
padding: 7px 14px;
|
|
border-radius: 8px 8px 0 0;
|
|
display: flex;
|
|
align-items: center;
|
|
user-select: none;
|
|
border-bottom: 1px solid #21312a;
|
|
}
|
|
.terminal-host { color: var(--primary); font-size: .78rem; font-family: 'Courier New', monospace; }
|
|
.terminal-body {
|
|
padding: 12px 14px;
|
|
height: 380px;
|
|
overflow-y: auto;
|
|
color: #c9d9c9;
|
|
background: #080e09;
|
|
font-size: .82rem;
|
|
line-height: 1.65;
|
|
}
|
|
.terminal-body::-webkit-scrollbar { width: 5px; }
|
|
.terminal-body::-webkit-scrollbar-thumb { background: #2a3a2d; border-radius: 3px; }
|
|
.term-line { white-space: pre-wrap; word-break: break-all; }
|
|
.term-prompt { color: #7ab87a; }
|
|
.term-success { color: #56d364; }
|
|
.term-error { color: #e06060; }
|
|
.term-info { color: #7a9e8a; font-style: italic; }
|
|
.term-warn { color: #c8a83a; }
|
|
.terminal-input-row {
|
|
display: flex;
|
|
align-items: center;
|
|
background: #080e09;
|
|
border-top: 1px solid #1a2a1d;
|
|
padding: 8px 14px;
|
|
border-radius: 0 0 8px 8px;
|
|
}
|
|
.terminal-ps1 { color: #7ab87a; font-family: 'Courier New', monospace; font-size: .82rem; white-space: nowrap; margin-right: 6px; }
|
|
.terminal-cmd {
|
|
flex: 1;
|
|
background: transparent;
|
|
border: none;
|
|
outline: none;
|
|
color: #c9d9c9;
|
|
font-family: 'Courier New', Courier, monospace;
|
|
font-size: .82rem;
|
|
caret-color: #7ab87a;
|
|
}
|
|
.terminal-cmd::placeholder { color: #2e4030; }
|
|
#terminal-card { display: flex; flex-direction: column; }
|
|
#terminal-card .card-body { display: flex; flex-direction: column; background: #080e09 !important; padding: 0 !important; }
|
|
|
|
.service-card { border: 1px solid var(--border) !important; box-shadow: 0 1px 3px rgba(0,0,0,.15) !important; }
|
|
.service-card .card-footer { background: transparent !important; border-top: 1px solid var(--border) !important; }
|
|
|
|
.role-badge { font-size: .75rem; padding: .3em .6em; }
|
|
|
|
.server-search-wrap { position: relative; }
|
|
.server-search-wrap .bi-search { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--muted); pointer-events: none; }
|
|
#server-search { padding-left: 2rem; }
|
|
#server-list { max-height: 340px; overflow-y: auto; }
|
|
#server-list::-webkit-scrollbar { width: 5px; }
|
|
#server-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
.server-item { transition: background .12s; }
|
|
.server-item.hidden { display: none !important; }
|
|
.grant-form {
|
|
display: none;
|
|
background: rgba(153,117,149,.05);
|
|
border-top: 1px solid rgba(153,117,149,.15);
|
|
padding: .75rem 1rem;
|
|
gap: .5rem;
|
|
}
|
|
.server-item.open .grant-form { display: flex; flex-wrap: wrap; align-items: flex-end; }
|
|
.grant-form .form-label { font-size: .78rem; font-weight: 600; margin-bottom: .2rem; }
|
|
.grant-form .form-control { font-size: .82rem; }
|
|
</style>
|
|
{% block stylesheets %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<div class="app-wrapper">
|
|
{% if app.user %}
|
|
<nav class="sidebar">
|
|
<a class="brand" href="{{ path('app_server_index') }}">
|
|
<i class="bi bi-server me-2"></i>ServerControl
|
|
</a>
|
|
<div class="sidebar-top">
|
|
<span class="user-name">{{ app.user.pseudo }}</span>
|
|
<div class="sidebar-actions">
|
|
<div class="theme-switch-wrap" title="Changer de thème">
|
|
<i class="bi bi-sun-fill theme-icon-sun"></i>
|
|
<label class="theme-switch">
|
|
<input type="checkbox" id="theme-checkbox" onchange="toggleTheme()">
|
|
<span class="theme-track"></span>
|
|
</label>
|
|
<i class="bi bi-moon-stars-fill theme-icon-moon"></i>
|
|
</div>
|
|
<a href="{{ path('app_logout') }}" class="btn-logout" title="Déconnexion">
|
|
<i class="bi bi-box-arrow-right"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="sidebar-divider"></div>
|
|
<ul class="nav flex-column gap-1">
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if app.request.attributes.get('_route') starts with 'app_server' %}active{% endif %}"
|
|
href="{{ path('app_server_index') }}">
|
|
<i class="bi bi-hdd-stack me-2"></i>Servers
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if app.request.attributes.get('_route') starts with 'app_log' %}active{% endif %}"
|
|
href="{{ path('app_log_index') }}">
|
|
<i class="bi bi-journal-text me-2"></i>Logs
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if app.request.attributes.get('_route') == 'app_profile' %}active{% endif %}"
|
|
href="{{ path('app_profile') }}">
|
|
<i class="bi bi-person-gear me-2"></i>Mon profil
|
|
</a>
|
|
</li>
|
|
{% if is_granted('ROLE_ADMIN') %}
|
|
<li class="nav-item mt-2">
|
|
<span class="sidebar-section-label">Admin</span>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link {% if app.request.attributes.get('_route') starts with 'app_admin' %}active{% endif %}"
|
|
href="{{ path('app_admin_users') }}">
|
|
<i class="bi bi-people me-2"></i>Users
|
|
</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" href="{{ path('app_server_new') }}">
|
|
<i class="bi bi-plus-circle me-2"></i>Add Server
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
|
|
<div class="main-column">
|
|
<main class="p-4 flex-grow-1">
|
|
{% for label, messages in app.flashes %}
|
|
{% for message in messages %}
|
|
<div class="alert alert-{{ label == 'error' ? 'danger' : label }} alert-dismissible fade show">
|
|
{{ message }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
|
|
{% block body %}{% endblock %}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
const saved = localStorage.getItem('sc-theme') || 'dark';
|
|
applyTheme(saved);
|
|
})();
|
|
|
|
function applyTheme(theme) {
|
|
document.documentElement.setAttribute('data-bs-theme', theme);
|
|
localStorage.setItem('sc-theme', theme);
|
|
const cb = document.getElementById('theme-checkbox');
|
|
if (cb) cb.checked = (theme === 'dark');
|
|
}
|
|
|
|
function toggleTheme() {
|
|
const current = document.documentElement.getAttribute('data-bs-theme');
|
|
applyTheme(current === 'dark' ? 'light' : 'dark');
|
|
}
|
|
</script>
|
|
{% block javascripts %}{% endblock %}
|
|
</body>
|
|
</html>
|