Commit 72556b0da726f41b4d0bcc15c19e4e4179c28848

Authored by Willen Borges Coelho
1 parent 09cef093
Exists in main

URLs amigáveis e redesign visual completo

- .htaccess: esconde a extensão .php (RewriteRule mapeando /rota pro
  arquivo .php correspondente, redirect 301 de .php -> sem extensão),
  bloqueia acesso a dotfiles e nega acesso via URL a src/config/storage/sql
- Todos os links, forms e redirects internos atualizados pras novas rotas
- CSS reescrito do zero: tema claro/escuro automático (prefers-color-scheme),
  tipografia Inter, cards com sombra e cantos arredondados, botões com
  gradiente, badges e alerts mais refinados
- Tabelas ficam responsivas: em telas estreitas (<720px) cada linha vira
  um cartão empilhado em vez de rolar horizontalmente
- Login com subtítulo e leve animação de entrada

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@@ -48,10 +48,17 @@ e resetar senha, com controle de acesso gerenciado pela própria aplicação. @@ -48,10 +48,17 @@ e resetar senha, com controle de acesso gerenciado pela própria aplicação.
48 - `sql/schema.sql` — schema do banco (admins, settings, audit_log, login_attempts). 48 - `sql/schema.sql` — schema do banco (admins, settings, audit_log, login_attempts).
49 - `storage/` — log de auditoria em arquivo (complementar ao banco). 49 - `storage/` — log de auditoria em arquivo (complementar ao banco).
50 50
  51 +## URLs amigáveis
  52 +
  53 +O `public_html/.htaccess` esconde a extensão `.php` (requer `mod_rewrite` e
  54 +`AllowOverride FileInfo`/`Limit` no vhost, já presentes na config desse site):
  55 +`/dashboard`, `/admins`, `/auditoria`, `/settings`, `/logout` etc. em vez de
  56 +`dashboard.php`, `admins.php`... Acessar o `.php` direto redireciona pra URL limpa.
  57 +
51 ## Papéis 58 ## Papéis
52 59
53 -- **admin**: tudo que operador pode, mais gerenciar `admins.php` e `settings.php`.  
54 -- **operador**: só habilita/desabilita aluno e reseta senha no `dashboard.php`. 60 +- **admin**: tudo que operador pode, mais gerenciar administradores e configurações.
  61 +- **operador**: só habilita/desabilita aluno e reseta senha no dashboard.
55 62
56 A autenticação de quem usa o sistema é sempre feita com login e senha do próprio AD 63 A autenticação de quem usa o sistema é sempre feita com login e senha do próprio AD
57 (bind LDAP) — o banco **não armazena nenhuma senha**. A tabela `admins` só guarda 64 (bind LDAP) — o banco **não armazena nenhuma senha**. A tabela `admins` só guarda
public_html/.htaccess 0 → 100644
@@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
  1 +RewriteEngine On
  2 +RewriteBase /
  3 +
  4 +# Esconde a extensão .php: se alguém acessar /dashboard.php direto, redireciona
  5 +# pra /dashboard (sem expor o nome do arquivo em links/histórico).
  6 +RewriteCond %{THE_REQUEST} \s/+([^.\s]+)\.php[\s?] [NC]
  7 +RewriteRule ^ /%1 [R=301,L]
  8 +
  9 +# Bloqueia acesso a qualquer coisa fora de public_html através de URL,
  10 +# mesmo sem rewrite (defesa extra, já que src/config/storage ficam um
  11 +# nível acima e não deveriam ser alcançáveis mesmo sem isso).
  12 +RewriteRule ^(src|config|storage|sql)(/|$) - [F,L]
  13 +
  14 +# Roteia /algumacoisa para algumacoisa.php quando o arquivo existir e
  15 +# nenhum arquivo/diretório real com esse nome exista.
  16 +RewriteCond %{REQUEST_FILENAME} !-f
  17 +RewriteCond %{REQUEST_FILENAME} !-d
  18 +RewriteCond %{DOCUMENT_ROOT}/$1.php -f
  19 +RewriteRule ^([a-zA-Z0-9_-]+)$ $1.php [L,QSA]
  20 +
  21 +# Nunca serve .htaccess/.env ou outro arquivo de config por engano.
  22 +# (Options -Indexes não entra aqui de propósito: o AllowOverride do vhost
  23 +# não libera o contexto "Options", só FileInfo/AuthConfig/Limit -- colocar
  24 +# a diretiva aqui derrubaria o site com "Options not allowed here".)
  25 +<FilesMatch "^\.">
  26 + Require all denied
  27 +</FilesMatch>
public_html/action_status.php
@@ -6,7 +6,7 @@ require __DIR__ . &#39;/../src/bootstrap.php&#39;; @@ -6,7 +6,7 @@ require __DIR__ . &#39;/../src/bootstrap.php&#39;;
6 Auth::requireLogin(); 6 Auth::requireLogin();
7 7
8 if ($_SERVER['REQUEST_METHOD'] !== 'POST') { 8 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
9 - header('Location: dashboard.php'); 9 + header('Location: dashboard');
10 exit; 10 exit;
11 } 11 }
12 12
@@ -14,7 +14,7 @@ $user = trim((string) ($_POST[&#39;user&#39;] ?? &#39;&#39;)); @@ -14,7 +14,7 @@ $user = trim((string) ($_POST[&#39;user&#39;] ?? &#39;&#39;));
14 $enable = ($_POST['enable'] ?? '') === '1'; 14 $enable = ($_POST['enable'] ?? '') === '1';
15 15
16 if ($user === '') { 16 if ($user === '') {
17 - header('Location: dashboard.php?erro=1&msg=' . urlencode('Usuário inválido.')); 17 + header('Location: dashboard?erro=1&msg=' . urlencode('Usuário inválido.'));
18 exit; 18 exit;
19 } 19 }
20 20
@@ -39,9 +39,9 @@ try { @@ -39,9 +39,9 @@ try {
39 39
40 AuditLog::record($quemFez, $enable ? 'habilitou_aluno' : 'desabilitou_aluno', $user); 40 AuditLog::record($quemFez, $enable ? 'habilitou_aluno' : 'desabilitou_aluno', $user);
41 41
42 - header('Location: dashboard.php?ok=1'); 42 + header('Location: dashboard?ok=1');
43 exit; 43 exit;
44 } catch (Throwable $e) { 44 } catch (Throwable $e) {
45 - header('Location: dashboard.php?erro=1&msg=' . urlencode($e->getMessage())); 45 + header('Location: dashboard?erro=1&msg=' . urlencode($e->getMessage()));
46 exit; 46 exit;
47 } 47 }
public_html/admins.php
@@ -86,7 +86,7 @@ View::header(&#39;Administradores&#39;); @@ -86,7 +86,7 @@ View::header(&#39;Administradores&#39;);
86 <?php if ($erro): ?><div class="alert error"><?= View::escape($erro) ?></div><?php endif; ?> 86 <?php if ($erro): ?><div class="alert error"><?= View::escape($erro) ?></div><?php endif; ?>
87 <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?> 87 <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?>
88 88
89 - <form method="post" action="admins.php"> 89 + <form method="post" action="admins">
90 <input type="hidden" name="acao" value="adicionar"> 90 <input type="hidden" name="acao" value="adicionar">
91 <label for="login">Login do AD (sem o domínio <?= View::escape(Config::get('ldap.domain_suffix', '')) ?>)</label> 91 <label for="login">Login do AD (sem o domínio <?= View::escape(Config::get('ldap.domain_suffix', '')) ?>)</label>
92 <input type="text" id="login" name="login" required> 92 <input type="text" id="login" name="login" required>
@@ -108,11 +108,11 @@ View::header(&#39;Administradores&#39;); @@ -108,11 +108,11 @@ View::header(&#39;Administradores&#39;);
108 </tr> 108 </tr>
109 <?php foreach ($admins as $a): ?> 109 <?php foreach ($admins as $a): ?>
110 <tr> 110 <tr>
111 - <td><?= View::escape($a['login']) ?></td>  
112 - <td><?= View::escape($a['nome']) ?></td>  
113 - <td> 111 + <td data-label="Login"><?= View::escape($a['login']) ?></td>
  112 + <td data-label="Nome"><?= View::escape($a['nome']) ?></td>
  113 + <td data-label="Papel">
114 <?php if ($a['ativo']): ?> 114 <?php if ($a['ativo']): ?>
115 - <form method="post" action="admins.php" style="display:flex;gap:6px;align-items:center;margin:0"> 115 + <form method="post" action="admins" style="display:flex;gap:6px;align-items:center;margin:0;justify-content:flex-end">
116 <input type="hidden" name="acao" value="atualizar_papel"> 116 <input type="hidden" name="acao" value="atualizar_papel">
117 <input type="hidden" name="id" value="<?= (int) $a['id'] ?>"> 117 <input type="hidden" name="id" value="<?= (int) $a['id'] ?>">
118 <select name="papel" style="margin:0"> 118 <select name="papel" style="margin:0">
@@ -125,15 +125,15 @@ View::header(&#39;Administradores&#39;); @@ -125,15 +125,15 @@ View::header(&#39;Administradores&#39;);
125 <?= View::escape($a['papel']) ?> 125 <?= View::escape($a['papel']) ?>
126 <?php endif; ?> 126 <?php endif; ?>
127 </td> 127 </td>
128 - <td> 128 + <td data-label="Status">
129 <?php if ($a['ativo']): ?> 129 <?php if ($a['ativo']): ?>
130 <span class="badge ok">Ativo</span> 130 <span class="badge ok">Ativo</span>
131 <?php else: ?> 131 <?php else: ?>
132 <span class="badge off">Inativo</span> 132 <span class="badge off">Inativo</span>
133 <?php endif; ?> 133 <?php endif; ?>
134 </td> 134 </td>
135 - <td>  
136 - <form method="post" action="admins.php" style="display:inline"> 135 + <td data-label="Ações">
  136 + <form method="post" action="admins" style="display:inline">
137 <input type="hidden" name="id" value="<?= (int) $a['id'] ?>"> 137 <input type="hidden" name="id" value="<?= (int) $a['id'] ?>">
138 <?php if ($a['ativo']): ?> 138 <?php if ($a['ativo']): ?>
139 <input type="hidden" name="acao" value="desativar"> 139 <input type="hidden" name="acao" value="desativar">
public_html/assets/style.css
1 :root { 1 :root {
2 color-scheme: light; 2 color-scheme: light;
3 - --bg: #f4f5f7; 3 + --bg: #f1f3f8;
  4 + --bg-pattern: radial-gradient(circle at 10% -10%, #dbe4ff 0%, transparent 45%),
  5 + radial-gradient(circle at 110% 10%, #e3f3ff 0%, transparent 40%);
4 --card: #ffffff; 6 --card: #ffffff;
5 - --text: #1f2430;  
6 - --muted: #6b7280;  
7 - --border: #e2e4e9;  
8 - --accent: #2563eb; 7 + --card-border: #e7e9f0;
  8 + --text: #1a1d29;
  9 + --text-muted: #6b7280;
  10 + --border: #e7e9f0;
  11 + --accent: #4f46e5;
  12 + --accent-2: #4338ca;
  13 + --accent-soft: #eef0ff;
9 --accent-contrast: #ffffff; 14 --accent-contrast: #ffffff;
10 --danger: #dc2626; 15 --danger: #dc2626;
  16 + --danger-soft: #fee2e2;
11 --success: #16a34a; 17 --success: #16a34a;
  18 + --success-soft: #dcfce7;
  19 + --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  20 + --shadow-md: 0 8px 24px -8px rgba(16, 24, 40, 0.18), 0 2px 6px rgba(16, 24, 40, 0.06);
  21 + --radius: 14px;
  22 + --radius-sm: 8px;
  23 +}
  24 +
  25 +@media (prefers-color-scheme: dark) {
  26 + :root:not([data-theme="light"]) {
  27 + color-scheme: dark;
  28 + --bg: #0f1117;
  29 + --bg-pattern: radial-gradient(circle at 10% -10%, #1e2140 0%, transparent 45%),
  30 + radial-gradient(circle at 110% 10%, #10233a 0%, transparent 40%);
  31 + --card: #171923;
  32 + --card-border: #272b3a;
  33 + --text: #eef0f5;
  34 + --text-muted: #9aa0b2;
  35 + --border: #272b3a;
  36 + --accent: #6d66f0;
  37 + --accent-2: #857ef5;
  38 + --accent-soft: #211f3d;
  39 + --accent-contrast: #ffffff;
  40 + --danger: #f87171;
  41 + --danger-soft: #3a1a1c;
  42 + --success: #4ade80;
  43 + --success-soft: #11301f;
  44 + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  45 + --shadow-md: 0 12px 32px -12px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.35);
  46 + }
  47 +}
  48 +
  49 +:root[data-theme="dark"] {
  50 + color-scheme: dark;
  51 + --bg: #0f1117;
  52 + --bg-pattern: radial-gradient(circle at 10% -10%, #1e2140 0%, transparent 45%),
  53 + radial-gradient(circle at 110% 10%, #10233a 0%, transparent 40%);
  54 + --card: #171923;
  55 + --card-border: #272b3a;
  56 + --text: #eef0f5;
  57 + --text-muted: #9aa0b2;
  58 + --border: #272b3a;
  59 + --accent: #6d66f0;
  60 + --accent-2: #857ef5;
  61 + --accent-soft: #211f3d;
  62 + --accent-contrast: #ffffff;
  63 + --danger: #f87171;
  64 + --danger-soft: #3a1a1c;
  65 + --success: #4ade80;
  66 + --success-soft: #11301f;
  67 + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  68 + --shadow-md: 0 12px 32px -12px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.35);
12 } 69 }
13 70
14 * { box-sizing: border-box; } 71 * { box-sizing: border-box; }
15 72
  73 +html { -webkit-text-size-adjust: 100%; }
  74 +
16 body { 75 body {
17 margin: 0; 76 margin: 0;
18 - font-family: -apple-system, Segoe UI, Roboto, Arial, sans-serif;  
19 - background: var(--bg); 77 + min-height: 100vh;
  78 + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  79 + background-color: var(--bg);
  80 + background-image: var(--bg-pattern);
  81 + background-attachment: fixed;
20 color: var(--text); 82 color: var(--text);
  83 + -webkit-font-smoothing: antialiased;
21 } 84 }
22 85
  86 +/* Topbar */
  87 +
23 .topbar { 88 .topbar {
24 display: flex; 89 display: flex;
25 justify-content: space-between; 90 justify-content: space-between;
26 align-items: center; 91 align-items: center;
  92 + gap: 16px;
27 padding: 14px 24px; 93 padding: 14px 24px;
28 - background: var(--card); 94 + background: color-mix(in srgb, var(--card) 88%, transparent);
  95 + backdrop-filter: blur(10px);
29 border-bottom: 1px solid var(--border); 96 border-bottom: 1px solid var(--border);
  97 + position: sticky;
  98 + top: 0;
  99 + z-index: 20;
  100 + flex-wrap: wrap;
  101 +}
  102 +
  103 +.topbar strong {
  104 + font-weight: 700;
  105 + letter-spacing: -0.01em;
  106 + font-size: 15px;
  107 + white-space: nowrap;
  108 +}
  109 +
  110 +.topbar nav {
  111 + display: flex;
  112 + align-items: center;
  113 + gap: 4px;
  114 + flex-wrap: wrap;
  115 +}
  116 +
  117 +.topbar nav span {
  118 + color: var(--text-muted);
  119 + font-size: 13px;
  120 + margin-right: 10px;
  121 + padding-right: 10px;
  122 + border-right: 1px solid var(--border);
30 } 123 }
31 124
32 .topbar nav a { 125 .topbar nav a {
33 - margin-left: 16px;  
34 - color: var(--text); 126 + color: var(--text-muted);
35 text-decoration: none; 127 text-decoration: none;
36 font-size: 14px; 128 font-size: 14px;
  129 + font-weight: 500;
  130 + padding: 7px 12px;
  131 + border-radius: 999px;
  132 + transition: background-color 0.15s ease, color 0.15s ease;
  133 + white-space: nowrap;
37 } 134 }
38 135
39 -.topbar nav a:hover { color: var(--accent); } 136 +.topbar nav a:hover {
  137 + color: var(--accent);
  138 + background: var(--accent-soft);
  139 +}
  140 +
  141 +/* Layout */
40 142
41 .container { 143 .container {
42 - max-width: 960px;  
43 - margin: 32px auto;  
44 - padding: 0 16px; 144 + max-width: 1000px;
  145 + margin: 0 auto;
  146 + padding: 28px 16px 48px;
45 } 147 }
46 148
47 .card { 149 .card {
48 background: var(--card); 150 background: var(--card);
49 - border: 1px solid var(--border);  
50 - border-radius: 10px;  
51 - padding: 24px; 151 + border: 1px solid var(--card-border);
  152 + border-radius: var(--radius);
  153 + padding: 28px;
  154 + box-shadow: var(--shadow-sm);
52 } 155 }
53 156
54 .login-wrap { 157 .login-wrap {
@@ -56,46 +159,210 @@ body { @@ -56,46 +159,210 @@ body {
56 min-height: 100vh; 159 min-height: 100vh;
57 align-items: center; 160 align-items: center;
58 justify-content: center; 161 justify-content: center;
  162 + padding: 16px;
  163 +}
  164 +
  165 +.login-wrap .card {
  166 + width: 100%;
  167 + max-width: 380px;
  168 + box-shadow: var(--shadow-md);
  169 + animation: rise 0.35s ease;
59 } 170 }
60 171
61 -.login-wrap .card { width: 100%; max-width: 360px; } 172 +.login-wrap .card h1 {
  173 + text-align: center;
  174 + margin-bottom: 4px;
  175 +}
62 176
63 -h1 { font-size: 20px; margin: 0 0 16px; } 177 +.login-sub {
  178 + text-align: center;
  179 + color: var(--text-muted);
  180 + font-size: 13px;
  181 + margin: 0 0 22px;
  182 +}
64 183
65 -label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; } 184 +@keyframes rise {
  185 + from { opacity: 0; transform: translateY(8px); }
  186 + to { opacity: 1; transform: translateY(0); }
  187 +}
66 188
67 -input[type=text], input[type=password], select { 189 +h1 {
  190 + font-size: 21px;
  191 + font-weight: 700;
  192 + letter-spacing: -0.01em;
  193 + margin: 0 0 18px;
  194 +}
  195 +
  196 +label {
  197 + display: block;
  198 + font-size: 13px;
  199 + font-weight: 500;
  200 + color: var(--text-muted);
  201 + margin-bottom: 6px;
  202 +}
  203 +
  204 +input[type=text],
  205 +input[type=password],
  206 +select {
68 width: 100%; 207 width: 100%;
69 - padding: 8px 10px;  
70 - margin-bottom: 14px; 208 + padding: 10px 12px;
  209 + margin-bottom: 16px;
71 border: 1px solid var(--border); 210 border: 1px solid var(--border);
72 - border-radius: 6px; 211 + border-radius: var(--radius-sm);
73 font-size: 14px; 212 font-size: 14px;
  213 + background: var(--bg);
  214 + color: var(--text);
  215 + transition: border-color 0.15s ease, box-shadow 0.15s ease;
  216 +}
  217 +
  218 +input[type=text]:focus,
  219 +input[type=password]:focus,
  220 +select:focus {
  221 + outline: none;
  222 + border-color: var(--accent);
  223 + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
74 } 224 }
75 225
76 button, .btn { 226 button, .btn {
77 - display: inline-block; 227 + display: inline-flex;
  228 + align-items: center;
  229 + justify-content: center;
  230 + gap: 6px;
78 background: var(--accent); 231 background: var(--accent);
  232 + background-image: linear-gradient(135deg, var(--accent), var(--accent-2));
79 color: var(--accent-contrast); 233 color: var(--accent-contrast);
80 border: none; 234 border: none;
81 - padding: 8px 14px;  
82 - border-radius: 6px; 235 + padding: 9px 16px;
  236 + border-radius: var(--radius-sm);
83 font-size: 14px; 237 font-size: 14px;
  238 + font-weight: 600;
84 cursor: pointer; 239 cursor: pointer;
85 text-decoration: none; 240 text-decoration: none;
  241 + box-shadow: var(--shadow-sm);
  242 + transition: transform 0.1s ease, box-shadow 0.15s ease, opacity 0.15s ease;
  243 +}
  244 +
  245 +button:hover, .btn:hover { opacity: 0.92; }
  246 +button:active, .btn:active { transform: translateY(1px); }
  247 +
  248 +button.danger, .btn.danger {
  249 + background-image: none;
  250 + background: var(--danger);
  251 +}
  252 +
  253 +button.secondary, .btn.secondary {
  254 + background-image: none;
  255 + background: transparent;
  256 + color: var(--text-muted);
  257 + border: 1px solid var(--border);
  258 + box-shadow: none;
  259 +}
  260 +
  261 +button.secondary:hover, .btn.secondary:hover {
  262 + color: var(--text);
  263 + border-color: var(--text-muted);
  264 + opacity: 1;
  265 +}
  266 +
  267 +/* Tabela / responsiva */
  268 +
  269 +table {
  270 + width: 100%;
  271 + border-collapse: collapse;
  272 + margin-top: 16px;
  273 +}
  274 +
  275 +th, td {
  276 + text-align: left;
  277 + padding: 12px 10px;
  278 + border-bottom: 1px solid var(--border);
  279 + font-size: 14px;
  280 + vertical-align: middle;
  281 +}
  282 +
  283 +th {
  284 + color: var(--text-muted);
  285 + font-weight: 600;
  286 + font-size: 12px;
  287 + text-transform: uppercase;
  288 + letter-spacing: 0.03em;
  289 +}
  290 +
  291 +tr:last-child td { border-bottom: none; }
  292 +
  293 +.badge {
  294 + display: inline-block;
  295 + padding: 3px 10px;
  296 + border-radius: 999px;
  297 + font-size: 12px;
  298 + font-weight: 600;
86 } 299 }
87 300
88 -button.danger, .btn.danger { background: var(--danger); }  
89 -button.secondary, .btn.secondary { background: var(--muted); } 301 +.badge.ok { background: var(--success-soft); color: var(--success); }
  302 +.badge.off { background: var(--danger-soft); color: var(--danger); }
90 303
91 -table { width: 100%; border-collapse: collapse; margin-top: 12px; }  
92 -th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); font-size: 14px; }  
93 -th { color: var(--muted); font-weight: 600; } 304 +.alert {
  305 + padding: 11px 16px;
  306 + border-radius: var(--radius-sm);
  307 + margin-bottom: 18px;
  308 + font-size: 14px;
  309 + border: 1px solid transparent;
  310 +}
94 311
95 -.badge { padding: 2px 8px; border-radius: 999px; font-size: 12px; }  
96 -.badge.ok { background: #dcfce7; color: var(--success); }  
97 -.badge.off { background: #fee2e2; color: var(--danger); } 312 +.alert.error { background: var(--danger-soft); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 30%, transparent); }
  313 +.alert.success { background: var(--success-soft); color: var(--success); border-color: color-mix(in srgb, var(--success) 30%, transparent); }
98 314
99 -.alert { padding: 10px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 14px; }  
100 -.alert.error { background: #fee2e2; color: var(--danger); }  
101 -.alert.success { background: #dcfce7; color: var(--success); } 315 +.hint {
  316 + color: var(--text-muted);
  317 + font-size: 13px;
  318 +}
  319 +
  320 +code {
  321 + background: var(--accent-soft);
  322 + color: var(--accent);
  323 + padding: 2px 6px;
  324 + border-radius: 4px;
  325 + font-size: 12.5px;
  326 +}
  327 +
  328 +/* Responsivo: tabela vira "cards" empilhados no celular */
  329 +
  330 +@media (max-width: 720px) {
  331 + .container { padding: 20px 12px 40px; }
  332 + .card { padding: 18px; border-radius: 12px; }
  333 +
  334 + .topbar { padding: 12px 16px; }
  335 + .topbar nav span { border-right: none; padding-right: 0; margin-right: 0; width: 100%; order: -1; }
  336 +
  337 + table, thead, tbody, th, td, tr { display: block; }
  338 + thead { display: none; }
  339 +
  340 + tr {
  341 + border: 1px solid var(--border);
  342 + border-radius: var(--radius-sm);
  343 + margin-bottom: 12px;
  344 + padding: 10px 12px;
  345 + }
  346 +
  347 + td {
  348 + border-bottom: none;
  349 + padding: 6px 0;
  350 + display: flex;
  351 + justify-content: space-between;
  352 + align-items: center;
  353 + gap: 12px;
  354 + text-align: right;
  355 + }
  356 +
  357 + td::before {
  358 + content: attr(data-label);
  359 + font-size: 12px;
  360 + font-weight: 600;
  361 + color: var(--text-muted);
  362 + text-transform: uppercase;
  363 + letter-spacing: 0.03em;
  364 + text-align: left;
  365 + }
  366 +
  367 + td form { justify-content: flex-end; }
  368 +}
public_html/auditoria.php
@@ -40,7 +40,7 @@ View::header(&#39;Auditoria&#39;); @@ -40,7 +40,7 @@ View::header(&#39;Auditoria&#39;);
40 ?> 40 ?>
41 <div class="card"> 41 <div class="card">
42 <h1>Auditoria</h1> 42 <h1>Auditoria</h1>
43 - <p style="color:#6b7280;font-size:13px"> 43 + <p class="hint">
44 Histórico de logins (inclusive tentativas com senha incorreta), alterações em 44 Histórico de logins (inclusive tentativas com senha incorreta), alterações em
45 alunos, administradores e configurações. Mostrando <?= count($logs) ?> de <?= $total ?> registros. 45 alunos, administradores e configurações. Mostrando <?= count($logs) ?> de <?= $total ?> registros.
46 </p> 46 </p>
@@ -56,12 +56,12 @@ View::header(&#39;Auditoria&#39;); @@ -56,12 +56,12 @@ View::header(&#39;Auditoria&#39;);
56 </tr> 56 </tr>
57 <?php foreach ($logs as $log): ?> 57 <?php foreach ($logs as $log): ?>
58 <tr> 58 <tr>
59 - <td><?= View::escape($log['criado_em']) ?></td>  
60 - <td><?= View::escape($log['usuario']) ?></td>  
61 - <td><?= View::escape($acaoLabels[$log['acao']] ?? $log['acao']) ?></td>  
62 - <td><?= View::escape($log['alvo'] ?? '') ?></td>  
63 - <td><?= View::escape($log['detalhe'] ?? '') ?></td>  
64 - <td><?= View::escape($log['ip'] ?? '') ?></td> 59 + <td data-label="Data/hora"><?= View::escape($log['criado_em']) ?></td>
  60 + <td data-label="Usuário"><?= View::escape($log['usuario']) ?></td>
  61 + <td data-label="Ação"><?= View::escape($acaoLabels[$log['acao']] ?? $log['acao']) ?></td>
  62 + <td data-label="Alvo"><?= View::escape($log['alvo'] ?? '') ?></td>
  63 + <td data-label="Detalhe"><?= View::escape($log['detalhe'] ?? '') ?></td>
  64 + <td data-label="IP"><?= View::escape($log['ip'] ?? '') ?></td>
65 </tr> 65 </tr>
66 <?php endforeach; ?> 66 <?php endforeach; ?>
67 <?php if (!$logs): ?> 67 <?php if (!$logs): ?>
@@ -75,7 +75,7 @@ View::header(&#39;Auditoria&#39;); @@ -75,7 +75,7 @@ View::header(&#39;Auditoria&#39;);
75 <?php if ($p === $page): ?> 75 <?php if ($p === $page): ?>
76 <strong><?= $p ?></strong> 76 <strong><?= $p ?></strong>
77 <?php else: ?> 77 <?php else: ?>
78 - <a href="auditoria.php?p=<?= $p ?>"><?= $p ?></a> 78 + <a href="auditoria?p=<?= $p ?>"><?= $p ?></a>
79 <?php endif; ?> 79 <?php endif; ?>
80 <?php if ($p < $totalPages): ?> | <?php endif; ?> 80 <?php if ($p < $totalPages): ?> | <?php endif; ?>
81 <?php endfor; ?> 81 <?php endfor; ?>
public_html/dashboard.php
@@ -61,24 +61,24 @@ View::header(&#39;Alunos&#39;); @@ -61,24 +61,24 @@ View::header(&#39;Alunos&#39;);
61 </tr> 61 </tr>
62 <?php foreach ($alunos as $aluno): ?> 62 <?php foreach ($alunos as $aluno): ?>
63 <tr> 63 <tr>
64 - <td><?= View::escape($aluno['nome']) ?></td>  
65 - <td> 64 + <td data-label="Aluno"><?= View::escape($aluno['nome']) ?></td>
  65 + <td data-label="Status">
66 <?php if ($aluno['habilitado']): ?> 66 <?php if ($aluno['habilitado']): ?>
67 <span class="badge ok">Habilitado</span> 67 <span class="badge ok">Habilitado</span>
68 <?php else: ?> 68 <?php else: ?>
69 <span class="badge off">Desabilitado</span> 69 <span class="badge off">Desabilitado</span>
70 <?php endif; ?> 70 <?php endif; ?>
71 </td> 71 </td>
72 - <td><?= View::escape($aluno['descricao']) ?></td>  
73 - <td>  
74 - <form method="post" action="action_status.php" style="display:inline"> 72 + <td data-label="Última alteração por"><?= View::escape($aluno['descricao']) ?></td>
  73 + <td data-label="Ações">
  74 + <form method="post" action="action_status" style="display:inline">
75 <input type="hidden" name="user" value="<?= View::escape($aluno['cn']) ?>"> 75 <input type="hidden" name="user" value="<?= View::escape($aluno['cn']) ?>">
76 <input type="hidden" name="enable" value="<?= $aluno['habilitado'] ? '0' : '1' ?>"> 76 <input type="hidden" name="enable" value="<?= $aluno['habilitado'] ? '0' : '1' ?>">
77 <button type="submit" class="<?= $aluno['habilitado'] ? 'danger' : '' ?>"> 77 <button type="submit" class="<?= $aluno['habilitado'] ? 'danger' : '' ?>">
78 <?= $aluno['habilitado'] ? 'Desabilitar' : 'Habilitar' ?> 78 <?= $aluno['habilitado'] ? 'Desabilitar' : 'Habilitar' ?>
79 </button> 79 </button>
80 </form> 80 </form>
81 - <a class="btn secondary" href="newpasswd.php?user=<?= urlencode($aluno['cn']) ?>">Nova senha</a> 81 + <a class="btn secondary" href="newpasswd?user=<?= urlencode($aluno['cn']) ?>">Nova senha</a>
82 </td> 82 </td>
83 </tr> 83 </tr>
84 <?php endforeach; ?> 84 <?php endforeach; ?>
public_html/index.php
@@ -5,7 +5,7 @@ declare(strict_types=1); @@ -5,7 +5,7 @@ declare(strict_types=1);
5 require __DIR__ . '/../src/bootstrap.php'; 5 require __DIR__ . '/../src/bootstrap.php';
6 6
7 if (!empty($_SESSION['login'])) { 7 if (!empty($_SESSION['login'])) {
8 - header('Location: dashboard.php'); 8 + header('Location: dashboard');
9 exit; 9 exit;
10 } 10 }
11 11
@@ -20,7 +20,7 @@ if ($_SERVER[&#39;REQUEST_METHOD&#39;] === &#39;POST&#39;) { @@ -20,7 +20,7 @@ if ($_SERVER[&#39;REQUEST_METHOD&#39;] === &#39;POST&#39;) {
20 } else { 20 } else {
21 $result = Auth::attempt($login, $senha); 21 $result = Auth::attempt($login, $senha);
22 if ($result['ok']) { 22 if ($result['ok']) {
23 - header('Location: dashboard.php'); 23 + header('Location: dashboard');
24 exit; 24 exit;
25 } 25 }
26 $erro = $result['error']; 26 $erro = $result['error'];
@@ -33,17 +33,22 @@ if ($_SERVER[&#39;REQUEST_METHOD&#39;] === &#39;POST&#39;) { @@ -33,17 +33,22 @@ if ($_SERVER[&#39;REQUEST_METHOD&#39;] === &#39;POST&#39;) {
33 <html lang="pt-br"> 33 <html lang="pt-br">
34 <head> 34 <head>
35 <meta charset="utf-8"> 35 <meta charset="utf-8">
  36 + <meta name="viewport" content="width=device-width, initial-scale=1">
36 <title>Gestão de Alunos Lab. Informática</title> 37 <title>Gestão de Alunos Lab. Informática</title>
  38 + <link rel="preconnect" href="https://fonts.googleapis.com">
  39 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  40 + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
37 <link rel="stylesheet" href="assets/style.css"> 41 <link rel="stylesheet" href="assets/style.css">
38 </head> 42 </head>
39 <body> 43 <body>
40 <div class="login-wrap"> 44 <div class="login-wrap">
41 <div class="card"> 45 <div class="card">
42 - <h1>Gestão de Alunos Lab. Informática</h1> 46 + <h1>Gestão de Alunos</h1>
  47 + <p class="login-sub">Lab. Informática</p>
43 <?php if ($erro): ?> 48 <?php if ($erro): ?>
44 <div class="alert error"><?= htmlspecialchars($erro, ENT_QUOTES) ?></div> 49 <div class="alert error"><?= htmlspecialchars($erro, ENT_QUOTES) ?></div>
45 <?php endif; ?> 50 <?php endif; ?>
46 - <form method="post" action="index.php"> 51 + <form method="post" action="">
47 <label for="login">Login</label> 52 <label for="login">Login</label>
48 <input type="text" id="login" name="login" autocomplete="username" required autofocus> 53 <input type="text" id="login" name="login" autocomplete="username" required autofocus>
49 54
public_html/logout.php
@@ -5,5 +5,5 @@ declare(strict_types=1); @@ -5,5 +5,5 @@ declare(strict_types=1);
5 require __DIR__ . '/../src/bootstrap.php'; 5 require __DIR__ . '/../src/bootstrap.php';
6 6
7 Auth::logout(); 7 Auth::logout();
8 -header('Location: index.php'); 8 +header('Location: /');
9 exit; 9 exit;
public_html/newpasswd.php
@@ -52,7 +52,7 @@ View::header(&#39;Nova senha&#39;); @@ -52,7 +52,7 @@ View::header(&#39;Nova senha&#39;);
52 <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?> 52 <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?>
53 53
54 <?php if (!$ok): ?> 54 <?php if (!$ok): ?>
55 - <form method="post" action="newpasswd.php"> 55 + <form method="post" action="newpasswd">
56 <input type="hidden" name="user" value="<?= View::escape($user) ?>"> 56 <input type="hidden" name="user" value="<?= View::escape($user) ?>">
57 <label for="passwd1">Nova senha</label> 57 <label for="passwd1">Nova senha</label>
58 <input type="password" id="passwd1" name="passwd1" required minlength="8"> 58 <input type="password" id="passwd1" name="passwd1" required minlength="8">
@@ -62,7 +62,7 @@ View::header(&#39;Nova senha&#39;); @@ -62,7 +62,7 @@ View::header(&#39;Nova senha&#39;);
62 </form> 62 </form>
63 <?php endif; ?> 63 <?php endif; ?>
64 64
65 - <p><a href="dashboard.php">&larr; Voltar</a></p> 65 + <p><a href="dashboard">&larr; Voltar</a></p>
66 </div> 66 </div>
67 <?php 67 <?php
68 View::footer(); 68 View::footer();
public_html/settings.php
@@ -29,14 +29,14 @@ View::header(&#39;Configurações&#39;); @@ -29,14 +29,14 @@ View::header(&#39;Configurações&#39;);
29 <?php if ($erro): ?><div class="alert error"><?= View::escape($erro) ?></div><?php endif; ?> 29 <?php if ($erro): ?><div class="alert error"><?= View::escape($erro) ?></div><?php endif; ?>
30 <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?> 30 <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?>
31 31
32 - <form method="post" action="settings.php"> 32 + <form method="post" action="settings">
33 <label for="group_aluno_filter">Grupo (CN) de alunos monitorado no dashboard</label> 33 <label for="group_aluno_filter">Grupo (CN) de alunos monitorado no dashboard</label>
34 <input type="text" id="group_aluno_filter" name="group_aluno_filter" 34 <input type="text" id="group_aluno_filter" name="group_aluno_filter"
35 value="<?= View::escape($settings['group_aluno_filter'] ?? '') ?>" required> 35 value="<?= View::escape($settings['group_aluno_filter'] ?? '') ?>" required>
36 <button type="submit">Salvar</button> 36 <button type="submit">Salvar</button>
37 </form> 37 </form>
38 38
39 - <p style="color:#6b7280;font-size:13px"> 39 + <p class="hint">
40 As demais configurações de conexão (servidor LDAP, conta de serviço, credenciais de banco) 40 As demais configurações de conexão (servidor LDAP, conta de serviço, credenciais de banco)
41 ficam em <code>config/local.php</code>, fora do versionamento, por segurança. 41 ficam em <code>config/local.php</code>, fora do versionamento, por segurança.
42 </p> 42 </p>
@@ -81,7 +81,7 @@ final class Auth @@ -81,7 +81,7 @@ final class Auth
81 { 81 {
82 self::start(); 82 self::start();
83 if (empty($_SESSION['login'])) { 83 if (empty($_SESSION['login'])) {
84 - header('Location: index.php?erro=sessao_expirada'); 84 + header('Location: /?erro=sessao_expirada');
85 exit; 85 exit;
86 } 86 }
87 } 87 }
@@ -8,7 +8,11 @@ final class View @@ -8,7 +8,11 @@ final class View
8 <html lang="pt-br"> 8 <html lang="pt-br">
9 <head> 9 <head>
10 <meta charset="utf-8"> 10 <meta charset="utf-8">
  11 + <meta name="viewport" content="width=device-width, initial-scale=1">
11 <title><?= htmlspecialchars($title, ENT_QUOTES) ?> - Gestão de Alunos Lab. Informática</title> 12 <title><?= htmlspecialchars($title, ENT_QUOTES) ?> - Gestão de Alunos Lab. Informática</title>
  13 + <link rel="preconnect" href="https://fonts.googleapis.com">
  14 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  15 + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
12 <link rel="stylesheet" href="assets/style.css"> 16 <link rel="stylesheet" href="assets/style.css">
13 </head> 17 </head>
14 <body> 18 <body>
@@ -16,13 +20,13 @@ final class View @@ -16,13 +20,13 @@ final class View
16 <strong>Gestão de Alunos Lab. Informática</strong> 20 <strong>Gestão de Alunos Lab. Informática</strong>
17 <nav> 21 <nav>
18 <span><?= htmlspecialchars(Auth::currentName() ?? '', ENT_QUOTES) ?></span> 22 <span><?= htmlspecialchars(Auth::currentName() ?? '', ENT_QUOTES) ?></span>
19 - <a href="dashboard.php">Alunos</a> 23 + <a href="dashboard">Alunos</a>
20 <?php if (Auth::isAdmin()): ?> 24 <?php if (Auth::isAdmin()): ?>
21 - <a href="admins.php">Administradores</a>  
22 - <a href="auditoria.php">Auditoria</a>  
23 - <a href="settings.php">Configurações</a> 25 + <a href="admins">Administradores</a>
  26 + <a href="auditoria">Auditoria</a>
  27 + <a href="settings">Configurações</a>
24 <?php endif; ?> 28 <?php endif; ?>
25 - <a href="logout.php">Sair</a> 29 + <a href="logout">Sair</a>
26 </nav> 30 </nav>
27 </div> 31 </div>
28 <div class="container"> 32 <div class="container">