Initial skills documentation — 25 categories, all SKILL.md + references + scripts
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# Frontend Auth & Action Pattern — Ops Portal
|
||||
|
||||
Created: 2026-07-09
|
||||
Applies to: `/opt/ops-portal/static/` pages (index.html, servers.html, backups.html, services.html)
|
||||
|
||||
## Architecture
|
||||
|
||||
Each page is a self-contained HTML file served by FastAPI with:
|
||||
1. Inline login overlay (no separate login page)
|
||||
2. Inline nav (not fetched via nav.html partial — auth-aware logout button is tightly coupled)
|
||||
3. Single JS module: `/js/app.js` (exposed as `window.Ops`)
|
||||
|
||||
## JS Module (`/js/app.js`)
|
||||
|
||||
### Auth
|
||||
|
||||
```javascript
|
||||
// Login: stores JWT in localStorage['ops_token']
|
||||
await Ops.login('username', 'password');
|
||||
|
||||
// Check auth state
|
||||
if (Ops.isAuthenticated()) { ... }
|
||||
|
||||
// Logout — clears storage + page reload
|
||||
Ops.logout();
|
||||
```
|
||||
|
||||
### API Client
|
||||
|
||||
```javascript
|
||||
// All calls auto-attach Authorization header from stored token
|
||||
var data = await Ops.apiFetch('/api/status');
|
||||
var servers = await Ops.getServers();
|
||||
var audit = await Ops.getAuditLog(10);
|
||||
|
||||
// Service actions (all POST with JWT)
|
||||
await Ops.restartService('caddy');
|
||||
await Ops.stopService('hermes');
|
||||
await Ops.startService('ops-portal');
|
||||
|
||||
// Backup trigger
|
||||
await Ops.triggerBackup('hermes-vps-backups');
|
||||
|
||||
// Server reboot (by Hetzner server ID)
|
||||
await Ops.rebootServer(51140464);
|
||||
```
|
||||
|
||||
**401 handling:** `apiFetch()` automatically clears localStorage and calls `window.location.reload()` on 401 responses, showing the login overlay again.
|
||||
|
||||
### UI Components
|
||||
|
||||
```javascript
|
||||
// Toast notification (auto-dismiss 5s)
|
||||
Ops.toast('Server reboot initiated', 'success'); // 'success' | 'error' | 'info'
|
||||
|
||||
// Confirmation dialog (returns Promise<boolean>)
|
||||
var ok = await Ops.confirmDialog(
|
||||
'Reboot Server',
|
||||
'Are you sure you want to reboot wphost02?',
|
||||
'Reboot',
|
||||
'Cancel'
|
||||
);
|
||||
|
||||
// Button loading state
|
||||
Ops.spinning(document.getElementById('refreshBtn'), true);
|
||||
Ops.spinning(document.getElementById('refreshBtn'), false);
|
||||
|
||||
// Nav initialization (hamburger toggle + active link + logout)
|
||||
Ops.initNav();
|
||||
|
||||
// Formatting utilities
|
||||
Ops.fmtTime('2026-07-09T12:22:28-04:00'); // "Jul 9, 12:22 PM"
|
||||
Ops.fmtAge('...'); // "3h 12m ago"
|
||||
Ops.badgeHtml('ok'); // '<span class="badge badge-ok">✓ ok</span>'
|
||||
Ops.dotColor('running'); // 'dot-green'
|
||||
Ops.escapeHtml('<script>'); // '<script>'
|
||||
Ops.valOrDash(null); // '—'
|
||||
Ops.updateLastUpdated(); // Sets #lastUpdated textContent
|
||||
```
|
||||
|
||||
### Toast container (lazy-created)
|
||||
|
||||
If `#toastContainer` doesn't exist, `Ops.toast()` creates it as a `<div>` appended to `document.body`. Position: `fixed; top: 20px; right: 20px; z-index: 10001`. Styled via `ops.css` `.toast`, `.toast-success`, `.toast-error`, `.toast-info`.
|
||||
|
||||
## Page Template Pattern
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#0f1628">
|
||||
<title>Page — Operations Dashboard</title>
|
||||
<link rel="stylesheet" href="/css/ops.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Login Overlay -->
|
||||
<div class="login-overlay show" id="loginOverlay">
|
||||
<div class="login-card">
|
||||
<!-- logo, title, error div, form with #loginUser + #loginPass -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation (inline, not fetched) -->
|
||||
<nav class="ops-nav" id="opsNav">
|
||||
<!-- brand, nav-links with SVG icons, logout button -->
|
||||
</nav>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<header class="page-header">
|
||||
<h1>Page Title</h1>
|
||||
<div class="header-actions">
|
||||
<span class="last-updated" id="lastUpdated">—</span>
|
||||
<button class="refresh-btn" onclick="loadPageData()">Refresh</button>
|
||||
</div>
|
||||
</header>
|
||||
<!-- card sections with data -->
|
||||
</div>
|
||||
|
||||
<script src="/js/app.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Auth guard
|
||||
if (!Ops.isAuthenticated()) {
|
||||
document.getElementById('loginOverlay').classList.add('show');
|
||||
} else {
|
||||
document.getElementById('logoutBtn').classList.add('show');
|
||||
loadPageData();
|
||||
}
|
||||
|
||||
// Login handler
|
||||
window.handleLogin = async function(e) { ... await Ops.login(...) ... };
|
||||
|
||||
// Data loading
|
||||
window.loadPageData = async function() { ... };
|
||||
|
||||
// Action handlers with confirmation
|
||||
window.handleAction = async function(...) {
|
||||
if (await Ops.confirmDialog(...)) { ... await Ops.apiCall(...) ... Ops.toast(...) }
|
||||
};
|
||||
|
||||
Ops.initNav();
|
||||
// Auto-refresh
|
||||
setInterval(function() {
|
||||
if (Ops.isAuthenticated()) loadPageData();
|
||||
}, 60000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Files created Jul 9, 2026
|
||||
|
||||
| File | Size | Sections |
|
||||
|------|------|----------|
|
||||
| `/opt/ops-portal/static/index.html` | 19.3 KB | Health grid, Quick actions (restart/trigger), Servers summary, Cron jobs, Audit log |
|
||||
| `/opt/ops-portal/static/servers.html` | 15.0 KB | Quick specs (vCPUs/RAM), Sortable server table with click-to-expand details, Reboot button per server |
|
||||
| `/opt/ops-portal/static/backups.html` | 15.1 KB | Summary bar, Bucket grid with Trigger button, Schedule table |
|
||||
| `/opt/ops-portal/static/services.html` | 14.7 KB | Service control grid (Start/Stop/Restart per service), API health grid, Service audit log |
|
||||
| `/opt/ops-portal/static/css/ops.css` | 27.0 KB | Full theme with auth-overlay, confirm-dialog, toast, action-card, svc-card styles |
|
||||
| `/opt/ops-portal/static/js/app.js` | 11.6 KB | Auth module, API client, confirmation dialog, toast, utilities |
|
||||
|
||||
## CSS classes for auth UI (in ops.css)
|
||||
|
||||
- `.login-overlay` — full-screen fixed overlay, `display: none` by default, `.show` sets `display: flex`
|
||||
- `.login-card` — centered dialog, max-width 400px, dark card with logo + form
|
||||
- `.login-error` — red alert box, hidden by default, `.show` reveals it
|
||||
- `.confirm-overlay` — modal backdrop with blur, `.show` = `display: flex`
|
||||
- `.confirm-dialog` — centered card with title + message + action buttons
|
||||
- `.toast-container` — fixed top-right notification area
|
||||
- `.toast`, `.toast-success`, `.toast-error`, `.toast-info` — slide-in animations
|
||||
|
||||
## Key quirks
|
||||
|
||||
- **Login overlay is always in the HTML** — it's hidden by CSS `display: none`. The JS just toggles `.show` class. No page redirect on login; the overlay hides and the page stays.
|
||||
- **Auto-refresh stops when not authenticated** — every `setInterval` callback checks `Ops.isAuthenticated()` before fetching. Stale data stays visible until login.
|
||||
- **Caddy reload** after config change: `caddy reload --config /etc/caddy/Caddyfile` or `systemctl reload caddy`. Full restart (`systemctl stop/start`) only needed when adding new Caddy hosts, not modifying existing ones.
|
||||
Reference in New Issue
Block a user