feat: admin panel, tracks CRUD, operator HUD improvements, mood badges
This commit is contained in:
parent
271f933b6e
commit
167bcb15a9
9 changed files with 1835 additions and 6 deletions
314
admin.html
Normal file
314
admin.html
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JAESWIFT // ADMIN</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="css/admin.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Login Screen -->
|
||||||
|
<div class="admin-login" id="loginScreen">
|
||||||
|
<div class="login-box">
|
||||||
|
<div class="login-header">
|
||||||
|
<div class="login-icon">⬡</div>
|
||||||
|
<h1 class="login-title">JAESWIFT</h1>
|
||||||
|
<span class="login-subtitle">ADMIN // CONTROL PANEL</span>
|
||||||
|
</div>
|
||||||
|
<form class="login-form" id="loginForm">
|
||||||
|
<div class="login-field">
|
||||||
|
<label class="login-label">OPERATOR ID</label>
|
||||||
|
<input type="text" id="loginUser" class="login-input" autocomplete="username" placeholder="username" required>
|
||||||
|
</div>
|
||||||
|
<div class="login-field">
|
||||||
|
<label class="login-label">ACCESS KEY</label>
|
||||||
|
<input type="password" id="loginPass" class="login-input" autocomplete="current-password" placeholder="••••••••" required>
|
||||||
|
</div>
|
||||||
|
<div class="login-error" id="loginError"></div>
|
||||||
|
<button type="submit" class="login-btn">AUTHENTICATE →</button>
|
||||||
|
</form>
|
||||||
|
<div class="login-footer">
|
||||||
|
<span class="login-status">SECURE CHANNEL // AES-256</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin Dashboard -->
|
||||||
|
<div class="admin-app" id="adminApp" style="display:none;">
|
||||||
|
|
||||||
|
<!-- Top Bar -->
|
||||||
|
<header class="admin-topbar">
|
||||||
|
<div class="topbar-left">
|
||||||
|
<span class="topbar-logo">⬡ JAESWIFT</span>
|
||||||
|
<span class="topbar-divider">//</span>
|
||||||
|
<span class="topbar-section" id="topbarSection">DASHBOARD</span>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-right">
|
||||||
|
<span class="topbar-user" id="topbarUser">jae</span>
|
||||||
|
<button class="topbar-btn" onclick="AdminApp.logout()">LOGOUT</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="admin-sidebar">
|
||||||
|
<a href="#" class="sidebar-link active" data-section="dashboard" onclick="AdminApp.showSection('dashboard')">
|
||||||
|
<span class="sidebar-icon">◈</span> DASHBOARD
|
||||||
|
</a>
|
||||||
|
<a href="#" class="sidebar-link" data-section="posts" onclick="AdminApp.showSection('posts')">
|
||||||
|
<span class="sidebar-icon">▤</span> POSTS
|
||||||
|
</a>
|
||||||
|
<a href="#" class="sidebar-link" data-section="editor" onclick="AdminApp.showSection('editor')">
|
||||||
|
<span class="sidebar-icon">✎</span> NEW POST
|
||||||
|
</a>
|
||||||
|
<a href="#" class="sidebar-link" data-section="tracks" onclick="AdminApp.showSection('tracks')">
|
||||||
|
<span class="sidebar-icon">♫</span> TRACKS
|
||||||
|
</a>
|
||||||
|
<a href="#" class="sidebar-link" data-section="settings" onclick="AdminApp.showSection('settings')">
|
||||||
|
<span class="sidebar-icon">⚙</span> SETTINGS
|
||||||
|
</a>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<a href="/" class="sidebar-link">
|
||||||
|
<span class="sidebar-icon">←</span> VIEW SITE
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="admin-main">
|
||||||
|
|
||||||
|
<!-- Dashboard Section -->
|
||||||
|
<section class="admin-section" id="section-dashboard">
|
||||||
|
<h2 class="section-heading">SYSTEM OVERVIEW</h2>
|
||||||
|
<div class="dash-grid">
|
||||||
|
<div class="dash-card">
|
||||||
|
<span class="dash-card-label">TOTAL POSTS</span>
|
||||||
|
<span class="dash-card-value" id="dashPosts">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="dash-card">
|
||||||
|
<span class="dash-card-label">TOTAL WORDS</span>
|
||||||
|
<span class="dash-card-value" id="dashWords">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="dash-card">
|
||||||
|
<span class="dash-card-label">TRACKS</span>
|
||||||
|
<span class="dash-card-value" id="dashTracks">--</span>
|
||||||
|
</div>
|
||||||
|
<div class="dash-card">
|
||||||
|
<span class="dash-card-label">SERVER CPU</span>
|
||||||
|
<span class="dash-card-value" id="dashCPU">--%</span>
|
||||||
|
</div>
|
||||||
|
<div class="dash-card">
|
||||||
|
<span class="dash-card-label">MEMORY</span>
|
||||||
|
<span class="dash-card-value" id="dashMem">--%</span>
|
||||||
|
</div>
|
||||||
|
<div class="dash-card">
|
||||||
|
<span class="dash-card-label">DISK</span>
|
||||||
|
<span class="dash-card-value" id="dashDisk">--%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3 class="section-subheading">SERVICES STATUS</h3>
|
||||||
|
<div class="services-grid" id="dashServices">Loading...</div>
|
||||||
|
<h3 class="section-subheading">RECENT THREATS (CVE)</h3>
|
||||||
|
<div class="threats-list" id="dashThreats">Loading...</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Posts Section -->
|
||||||
|
<section class="admin-section" id="section-posts" style="display:none;">
|
||||||
|
<div class="section-heading-row">
|
||||||
|
<h2 class="section-heading">BLOG POSTS</h2>
|
||||||
|
<button class="action-btn" onclick="AdminApp.showSection('editor')">+ NEW POST</button>
|
||||||
|
</div>
|
||||||
|
<div class="posts-table-wrap">
|
||||||
|
<table class="posts-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>DATE</th>
|
||||||
|
<th>TITLE</th>
|
||||||
|
<th>TAGS</th>
|
||||||
|
<th>WORDS</th>
|
||||||
|
<th>THREAT</th>
|
||||||
|
<th>ACTIONS</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="postsTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Editor Section -->
|
||||||
|
<section class="admin-section" id="section-editor" style="display:none;">
|
||||||
|
<h2 class="section-heading" id="editorHeading">NEW TRANSMISSION</h2>
|
||||||
|
<form class="editor-form" id="editorForm">
|
||||||
|
<input type="hidden" id="editSlugOriginal">
|
||||||
|
|
||||||
|
<div class="editor-row">
|
||||||
|
<div class="editor-field full">
|
||||||
|
<label class="editor-label">TITLE</label>
|
||||||
|
<input type="text" id="edTitle" class="editor-input" placeholder="Post title..." required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-row">
|
||||||
|
<div class="editor-field">
|
||||||
|
<label class="editor-label">SLUG</label>
|
||||||
|
<input type="text" id="edSlug" class="editor-input" placeholder="auto-generated">
|
||||||
|
</div>
|
||||||
|
<div class="editor-field">
|
||||||
|
<label class="editor-label">DATE</label>
|
||||||
|
<input type="date" id="edDate" class="editor-input">
|
||||||
|
</div>
|
||||||
|
<div class="editor-field">
|
||||||
|
<label class="editor-label">TIME</label>
|
||||||
|
<input type="time" id="edTime" class="editor-input" value="00:00">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-row">
|
||||||
|
<div class="editor-field">
|
||||||
|
<label class="editor-label">TAGS (comma-separated)</label>
|
||||||
|
<input type="text" id="edTags" class="editor-input" placeholder="security, infrastructure">
|
||||||
|
</div>
|
||||||
|
<div class="editor-field">
|
||||||
|
<label class="editor-label">EXCERPT</label>
|
||||||
|
<input type="text" id="edExcerpt" class="editor-input" placeholder="Short description...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cyberpunk Health Stats -->
|
||||||
|
<div class="editor-hud-section">
|
||||||
|
<h3 class="editor-hud-title">◈ OPERATOR STATUS</h3>
|
||||||
|
<div class="editor-hud-grid">
|
||||||
|
<div class="hud-field">
|
||||||
|
<label class="editor-label">MOOD</label>
|
||||||
|
<select id="edMood" class="editor-input editor-select">
|
||||||
|
<option value="focused">Focused</option>
|
||||||
|
<option value="creative">Creative</option>
|
||||||
|
<option value="productive">Productive</option>
|
||||||
|
<option value="tired">Tired</option>
|
||||||
|
<option value="wired">Wired</option>
|
||||||
|
<option value="chaotic">Chaotic</option>
|
||||||
|
<option value="locked-in">Locked In</option>
|
||||||
|
<option value="zen">Zen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="hud-field">
|
||||||
|
<label class="editor-label">ENERGY (1-5)</label>
|
||||||
|
<input type="range" id="edEnergy" class="editor-range" min="1" max="5" value="3">
|
||||||
|
<span class="range-val" id="edEnergyVal">3</span>
|
||||||
|
</div>
|
||||||
|
<div class="hud-field">
|
||||||
|
<label class="editor-label">MOTIVATION (1-5)</label>
|
||||||
|
<input type="range" id="edMotivation" class="editor-range" min="1" max="5" value="3">
|
||||||
|
<span class="range-val" id="edMotivationVal">3</span>
|
||||||
|
</div>
|
||||||
|
<div class="hud-field">
|
||||||
|
<label class="editor-label">FOCUS (1-5)</label>
|
||||||
|
<input type="range" id="edFocus" class="editor-range" min="1" max="5" value="3">
|
||||||
|
<span class="range-val" id="edFocusVal">3</span>
|
||||||
|
</div>
|
||||||
|
<div class="hud-field">
|
||||||
|
<label class="editor-label">DIFFICULTY (1-5)</label>
|
||||||
|
<input type="range" id="edDifficulty" class="editor-range" min="1" max="5" value="3">
|
||||||
|
<span class="range-val" id="edDifficultyVal">3</span>
|
||||||
|
</div>
|
||||||
|
<div class="hud-field">
|
||||||
|
<label class="editor-label">COFFEE ☕</label>
|
||||||
|
<input type="number" id="edCoffee" class="editor-input editor-small" min="0" max="10" value="2">
|
||||||
|
</div>
|
||||||
|
<div class="hud-field">
|
||||||
|
<label class="editor-label">BPM ♥</label>
|
||||||
|
<input type="number" id="edBPM" class="editor-input editor-small" min="40" max="200" value="72">
|
||||||
|
</div>
|
||||||
|
<div class="hud-field">
|
||||||
|
<label class="editor-label">THREAT LEVEL</label>
|
||||||
|
<select id="edThreat" class="editor-input editor-select">
|
||||||
|
<option value="low">LOW</option>
|
||||||
|
<option value="medium">MEDIUM</option>
|
||||||
|
<option value="high">HIGH</option>
|
||||||
|
<option value="critical">CRITICAL</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Content Editor -->
|
||||||
|
<div class="editor-field full">
|
||||||
|
<label class="editor-label">CONTENT (Markdown)</label>
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<button type="button" class="tb-btn" onclick="AdminApp.insertMD('**','**')" title="Bold"><b>B</b></button>
|
||||||
|
<button type="button" class="tb-btn" onclick="AdminApp.insertMD('*','*')" title="Italic"><i>I</i></button>
|
||||||
|
<button type="button" class="tb-btn" onclick="AdminApp.insertMD('`','`')" title="Code"></></button>
|
||||||
|
<button type="button" class="tb-btn" onclick="AdminApp.insertMD('\n```\n','\n```\n')" title="Code Block">[ ]</button>
|
||||||
|
<button type="button" class="tb-btn" onclick="AdminApp.insertMD('## ','')" title="Heading">H2</button>
|
||||||
|
<button type="button" class="tb-btn" onclick="AdminApp.insertMD('### ','')" title="Heading">H3</button>
|
||||||
|
<button type="button" class="tb-btn" onclick="AdminApp.insertMD('[','](url)')" title="Link">🔗</button>
|
||||||
|
<button type="button" class="tb-btn" onclick="AdminApp.insertMD('---\n','')" title="Divider">—</button>
|
||||||
|
<span class="tb-wordcount" id="edWordCount">0 words</span>
|
||||||
|
</div>
|
||||||
|
<textarea id="edContent" class="editor-textarea" rows="20" placeholder="Write your transmission in markdown..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div class="editor-field full">
|
||||||
|
<label class="editor-label">PREVIEW</label>
|
||||||
|
<div class="editor-preview" id="edPreview"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-actions">
|
||||||
|
<button type="submit" class="action-btn">TRANSMIT POST →</button>
|
||||||
|
<button type="button" class="action-btn secondary" onclick="AdminApp.clearEditor()">CLEAR</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tracks Section -->
|
||||||
|
<section class="admin-section" id="section-tracks" style="display:none;">
|
||||||
|
<div class="section-heading-row">
|
||||||
|
<h2 class="section-heading">TRACK LIBRARY</h2>
|
||||||
|
<button class="action-btn" onclick="AdminApp.showAddTrack()">+ ADD TRACK</button>
|
||||||
|
</div>
|
||||||
|
<div class="track-add-form" id="trackAddForm" style="display:none;">
|
||||||
|
<input type="text" id="trackArtist" class="editor-input" placeholder="Artist">
|
||||||
|
<input type="text" id="trackTitle" class="editor-input" placeholder="Track">
|
||||||
|
<input type="text" id="trackAlbum" class="editor-input" placeholder="Album">
|
||||||
|
<button class="action-btn" onclick="AdminApp.addTrack()">ADD</button>
|
||||||
|
</div>
|
||||||
|
<div class="tracks-list" id="tracksList"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Settings Section -->
|
||||||
|
<section class="admin-section" id="section-settings" style="display:none;">
|
||||||
|
<h2 class="section-heading">CONFIGURATION</h2>
|
||||||
|
<form class="settings-form" id="settingsForm">
|
||||||
|
<div class="settings-group">
|
||||||
|
<h3 class="settings-group-title">WIDGET TOGGLES</h3>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" id="setWeather" checked>
|
||||||
|
<span class="toggle-label">Weather Widget</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" id="setNowPlaying" checked>
|
||||||
|
<span class="toggle-label">Now Playing Widget</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" id="setThreats" checked>
|
||||||
|
<span class="toggle-label">CVE Threat Feed</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<input type="checkbox" id="setTerminal" checked>
|
||||||
|
<span class="toggle-label">Terminal Emulator</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="action-btn">SAVE SETTINGS</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="js/admin.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
api/app.py
29
api/app.py
|
|
@ -221,6 +221,35 @@ def now_playing():
|
||||||
t = random.choice(tracks)
|
t = random.choice(tracks)
|
||||||
return jsonify(t)
|
return jsonify(t)
|
||||||
|
|
||||||
|
# ─── Tracks CRUD ─────────────────────────────────────
|
||||||
|
@app.route('/api/tracks')
|
||||||
|
def get_tracks():
|
||||||
|
return jsonify(load_json('tracks.json'))
|
||||||
|
|
||||||
|
@app.route('/api/tracks', methods=['POST'])
|
||||||
|
@require_auth
|
||||||
|
def add_track():
|
||||||
|
d = request.get_json(force=True)
|
||||||
|
tracks = load_json('tracks.json')
|
||||||
|
tracks.append({
|
||||||
|
'artist': d.get('artist', ''),
|
||||||
|
'track': d.get('track', ''),
|
||||||
|
'album': d.get('album', ''),
|
||||||
|
})
|
||||||
|
save_json('tracks.json', tracks)
|
||||||
|
return jsonify(tracks[-1]), 201
|
||||||
|
|
||||||
|
@app.route('/api/tracks/<int:index>', methods=['DELETE'])
|
||||||
|
@require_auth
|
||||||
|
def delete_track(index):
|
||||||
|
tracks = load_json('tracks.json')
|
||||||
|
if 0 <= index < len(tracks):
|
||||||
|
removed = tracks.pop(index)
|
||||||
|
save_json('tracks.json', tracks)
|
||||||
|
return jsonify({'ok': True, 'removed': removed})
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
# ─── Git Activity (from Gitea API) ───────────────────
|
# ─── Git Activity (from Gitea API) ───────────────────
|
||||||
@app.route('/api/git-activity')
|
@app.route('/api/git-activity')
|
||||||
def git_activity():
|
def git_activity():
|
||||||
|
|
|
||||||
850
css/admin.css
Normal file
850
css/admin.css
Normal file
|
|
@ -0,0 +1,850 @@
|
||||||
|
/* ===================================================
|
||||||
|
JAESWIFT — Admin Panel Styles
|
||||||
|
=================================================== */
|
||||||
|
:root {
|
||||||
|
--bg: #0a0e17;
|
||||||
|
--bg2: #0f1420;
|
||||||
|
--bg3: #151b2b;
|
||||||
|
--accent: #00ffc8;
|
||||||
|
--accent-dim: rgba(0, 255, 200, 0.15);
|
||||||
|
--text: #c8d6e5;
|
||||||
|
--text-dim: rgba(200, 214, 229, 0.5);
|
||||||
|
--danger: #ff4757;
|
||||||
|
--warn: #ffa502;
|
||||||
|
--success: #00ffc8;
|
||||||
|
--border: rgba(0, 255, 200, 0.08);
|
||||||
|
--sidebar-w: 220px;
|
||||||
|
--topbar-h: 50px;
|
||||||
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
|
--font-display: 'Orbitron', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Login Screen ─── */
|
||||||
|
.admin-login {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: radial-gradient(ellipse at center, rgba(0,255,200,0.03) 0%, transparent 70%), var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
width: 380px;
|
||||||
|
max-width: 95vw;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg2);
|
||||||
|
padding: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: var(--accent);
|
||||||
|
text-shadow: 0 0 20px rgba(0,255,200,0.3);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-subtitle {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-field {
|
||||||
|
margin-bottom: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.7rem 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
.login-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 0 8px rgba(0,255,200,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-error {
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
min-height: 1.2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
padding: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.login-btn:hover {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
box-shadow: 0 0 15px rgba(0,255,200,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-status {
|
||||||
|
font-size: 0.55rem;
|
||||||
|
color: rgba(200,214,229,0.25);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── App Layout ─── */
|
||||||
|
.admin-app {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--sidebar-w) 1fr;
|
||||||
|
grid-template-rows: var(--topbar-h) 1fr;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Top Bar ─── */
|
||||||
|
.admin-topbar {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
background: var(--bg2);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-logo {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-divider {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-section {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-user {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid rgba(255,71,87,0.3);
|
||||||
|
color: var(--danger);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.topbar-btn:hover {
|
||||||
|
background: rgba(255,71,87,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Sidebar ─── */
|
||||||
|
.admin-sidebar {
|
||||||
|
background: var(--bg2);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.7rem 1.2rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
}
|
||||||
|
.sidebar-link:hover {
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(0,255,200,0.03);
|
||||||
|
}
|
||||||
|
.sidebar-link.active {
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(0,255,200,0.05);
|
||||||
|
border-left-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-icon {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
width: 1.2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Main Content ─── */
|
||||||
|
.admin-main {
|
||||||
|
padding: 2rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: calc(100vh - var(--topbar-h));
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-section {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.section-heading-row .section-heading {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subheading {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin: 2rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Action Buttons ─── */
|
||||||
|
.action-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.action-btn:hover {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
box-shadow: 0 0 10px rgba(0,255,200,0.15);
|
||||||
|
}
|
||||||
|
.action-btn.secondary {
|
||||||
|
border-color: var(--text-dim);
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
.action-btn.secondary:hover {
|
||||||
|
background: rgba(200,214,229,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-sm {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.6rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.action-sm.edit:hover { color: var(--accent); border-color: var(--accent); }
|
||||||
|
.action-sm.delete:hover { color: var(--danger); border-color: var(--danger); }
|
||||||
|
|
||||||
|
/* ─── Dashboard Grid ─── */
|
||||||
|
.dash-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash-card {
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 1.2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash-card-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash-card-value {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 10px rgba(0,255,200,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Services Grid ─── */
|
||||||
|
.services-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
background: var(--bg3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.service-card.online .service-dot { color: var(--accent); }
|
||||||
|
.service-card.offline .service-dot { color: var(--danger); }
|
||||||
|
.service-name { flex: 1; color: var(--text); }
|
||||||
|
.service-ms { color: var(--text-dim); font-size: 0.6rem; }
|
||||||
|
|
||||||
|
/* ─── Threats ─── */
|
||||||
|
.threat-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid rgba(0,255,200,0.04);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
.threat-id {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
.threat-summary {
|
||||||
|
flex: 1;
|
||||||
|
color: var(--text-dim);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.threat-cvss {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
min-width: 35px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.cvss-crit { color: #ff0040; text-shadow: 0 0 8px rgba(255,0,64,0.4); }
|
||||||
|
.cvss-high { color: var(--danger); }
|
||||||
|
.cvss-med { color: var(--warn); }
|
||||||
|
.cvss-low { color: var(--accent); }
|
||||||
|
|
||||||
|
/* ─── Posts Table ─── */
|
||||||
|
.posts-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-table th {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-table td {
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
border-bottom: 1px solid rgba(0,255,200,0.03);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-table tr:hover td {
|
||||||
|
background: rgba(0,255,200,0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-link {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.post-link:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
.tag-pill {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.55rem;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-right: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.threat-badge {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
padding: 0.15rem 0.5rem;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.threat-low { color: var(--accent); border: 1px solid rgba(0,255,200,0.2); }
|
||||||
|
.threat-medium { color: var(--warn); border: 1px solid rgba(255,165,2,0.2); }
|
||||||
|
.threat-high { color: var(--danger); border: 1px solid rgba(255,71,87,0.2); }
|
||||||
|
.threat-critical { color: #ff0040; border: 1px solid rgba(255,0,64,0.3); animation: critPulse 1.5s ease-in-out infinite; }
|
||||||
|
@keyframes critPulse { 0%,100%{opacity:1;} 50%{opacity:0.5;} }
|
||||||
|
|
||||||
|
.actions-cell {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Editor Form ─── */
|
||||||
|
.editor-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
.editor-field.full { grid-column: 1 / -1; }
|
||||||
|
|
||||||
|
.editor-label {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-input {
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: #fff;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.5rem 0.8rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
.editor-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.editor-small {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-range {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
background: rgba(0,255,200,0.1);
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.editor-range::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 0 8px rgba(0,255,200,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-val {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Operator Status (HUD) Section in Editor ─── */
|
||||||
|
.editor-hud-section {
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 1.2rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-hud-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-hud-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hud-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Editor Toolbar ─── */
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: var(--bg3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-bottom: none;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.tb-btn:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tb-wordcount {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-textarea {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: 'Share Tech Mono', var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
padding: 1.2rem;
|
||||||
|
outline: none;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
.editor-textarea:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-preview {
|
||||||
|
background: var(--bg3);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 1.5rem;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-preview .post-rendered h1,
|
||||||
|
.editor-preview .post-rendered h2,
|
||||||
|
.editor-preview .post-rendered h3 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
color: #fff;
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
}
|
||||||
|
.editor-preview .post-rendered h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
.editor-preview .post-rendered strong { color: #fff; }
|
||||||
|
.editor-preview .post-rendered em { color: var(--accent); }
|
||||||
|
.editor-preview .post-rendered pre {
|
||||||
|
background: rgba(0,0,0,0.4);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
.editor-preview .post-rendered code {
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.editor-preview .post-rendered .inline-code {
|
||||||
|
background: rgba(0,255,200,0.06);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Tracks ─── */
|
||||||
|
.track-add-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.8rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.track-add-form .editor-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.5rem 0.8rem;
|
||||||
|
border-bottom: 1px solid rgba(0,255,200,0.04);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.track-row:hover {
|
||||||
|
background: rgba(0,255,200,0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-num {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
min-width: 25px;
|
||||||
|
}
|
||||||
|
.track-title {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
.track-artist {
|
||||||
|
color: var(--accent);
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
.track-album {
|
||||||
|
color: var(--text-dim);
|
||||||
|
flex: 2;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Settings ─── */
|
||||||
|
.settings-form {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group {
|
||||||
|
background: var(--bg2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
padding: 1.2rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-group-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--accent);
|
||||||
|
letter-spacing: 2px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-row input[type="checkbox"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 36px;
|
||||||
|
height: 18px;
|
||||||
|
background: rgba(200,214,229,0.1);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.toggle-row input[type="checkbox"]::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: var(--text-dim);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.toggle-row input[type="checkbox"]:checked {
|
||||||
|
background: rgba(0,255,200,0.15);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.toggle-row input[type="checkbox"]:checked::after {
|
||||||
|
left: 20px;
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 6px rgba(0,255,200,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Notification ─── */
|
||||||
|
.admin-notification {
|
||||||
|
position: fixed;
|
||||||
|
top: -60px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
padding: 0.7rem 2rem;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
background: var(--bg2);
|
||||||
|
color: var(--accent);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: top 0.4s ease;
|
||||||
|
}
|
||||||
|
.admin-notification.show {
|
||||||
|
top: 15px;
|
||||||
|
}
|
||||||
|
.admin-notification.error {
|
||||||
|
border-color: var(--danger);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Responsive ─── */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-app {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.admin-sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.admin-main {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.dash-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
.editor-hud-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
css/post.css
16
css/post.css
|
|
@ -277,6 +277,22 @@
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-mood-badge {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
border: 1px solid;
|
||||||
|
text-shadow: 0 0 8px currentColor;
|
||||||
|
animation: moodGlow 2s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes moodGlow {
|
||||||
|
0% { opacity: 0.8; }
|
||||||
|
100% { opacity: 1; text-shadow: 0 0 12px currentColor; }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.stat-pips {
|
.stat-pips {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|
|
||||||
102
css/style.css
102
css/style.css
|
|
@ -1632,3 +1632,105 @@ a:hover { color: #fff; text-shadow: 0 0 10px var(--accent-glow); }
|
||||||
background: rgba(0, 255, 200, 0.08) !important;
|
background: rgba(0, 255, 200, 0.08) !important;
|
||||||
box-shadow: 0 0 15px rgba(0, 255, 200, 0.15);
|
box-shadow: 0 0 15px rgba(0, 255, 200, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Weather Display ─── */
|
||||||
|
.weather-display {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
.weather-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.weather-temp {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 15px rgba(0, 255, 200, 0.3);
|
||||||
|
}
|
||||||
|
.weather-cond {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: rgba(0, 255, 200, 0.6);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.weather-details {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.weather-detail {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: rgba(200, 214, 229, 0.4);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border: 1px solid rgba(0, 255, 200, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Now Playing Display ─── */
|
||||||
|
.np-display {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
.np-track {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.np-artist {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--accent, #00ffc8);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.np-album {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: rgba(200, 214, 229, 0.35);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
.np-visualiser {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 18px;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
.np-bar {
|
||||||
|
width: 4px;
|
||||||
|
background: var(--accent, #00ffc8);
|
||||||
|
border-radius: 1px;
|
||||||
|
animation: npBounce 0.8s ease-in-out infinite alternate;
|
||||||
|
box-shadow: 0 0 4px rgba(0, 255, 200, 0.3);
|
||||||
|
}
|
||||||
|
.np-bar:nth-child(1) { height: 40%; animation-delay: 0s; }
|
||||||
|
.np-bar:nth-child(2) { height: 70%; animation-delay: 0.1s; }
|
||||||
|
.np-bar:nth-child(3) { height: 50%; animation-delay: 0.2s; }
|
||||||
|
.np-bar:nth-child(4) { height: 90%; animation-delay: 0.05s; }
|
||||||
|
.np-bar:nth-child(5) { height: 60%; animation-delay: 0.15s; }
|
||||||
|
.np-bar:nth-child(6) { height: 80%; animation-delay: 0.25s; }
|
||||||
|
.np-bar:nth-child(7) { height: 45%; animation-delay: 0.1s; }
|
||||||
|
.np-bar:nth-child(8) { height: 65%; animation-delay: 0.2s; }
|
||||||
|
@keyframes npBounce {
|
||||||
|
0% { transform: scaleY(0.3); }
|
||||||
|
100% { transform: scaleY(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
#npStatus {
|
||||||
|
color: var(--accent, #00ffc8);
|
||||||
|
animation: statusPulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes statusPulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
|
|
||||||
38
index.html
38
index.html
|
|
@ -214,6 +214,44 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Weather + Now Playing -->
|
||||||
|
<div class="panel hud-data-panel hud-row-panels">
|
||||||
|
<div class="hud-mini-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">WEATHER</span>
|
||||||
|
<span class="panel-status-dot status-green">● LIVE</span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content weather-display">
|
||||||
|
<div class="weather-main">
|
||||||
|
<span class="weather-temp" id="weatherTemp">--°C</span>
|
||||||
|
<span class="weather-cond" id="weatherCond">LOADING...</span>
|
||||||
|
</div>
|
||||||
|
<div class="weather-details">
|
||||||
|
<span class="weather-detail" id="weatherFeels">FEELS --°C</span>
|
||||||
|
<span class="weather-detail" id="weatherWind">-- KPH</span>
|
||||||
|
<span class="weather-detail" id="weatherHumidity">--% RH</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hud-mini-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">NOW PLAYING</span>
|
||||||
|
<span class="panel-status-dot" id="npStatus">● STREAMING</span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-content np-display">
|
||||||
|
<div class="np-track" id="npTrack">---</div>
|
||||||
|
<div class="np-artist" id="npArtist">---</div>
|
||||||
|
<div class="np-album" id="npAlbum">---</div>
|
||||||
|
<div class="np-visualiser">
|
||||||
|
<span class="np-bar"></span><span class="np-bar"></span><span class="np-bar"></span>
|
||||||
|
<span class="np-bar"></span><span class="np-bar"></span><span class="np-bar"></span>
|
||||||
|
<span class="np-bar"></span><span class="np-bar"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Network Graph -->
|
<!-- Network Graph -->
|
||||||
<div class="panel hud-data-panel">
|
<div class="panel hud-data-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
|
|
|
||||||
461
js/admin.js
Normal file
461
js/admin.js
Normal file
|
|
@ -0,0 +1,461 @@
|
||||||
|
/* ===================================================
|
||||||
|
JAESWIFT — Admin Panel JS
|
||||||
|
=================================================== */
|
||||||
|
const AdminApp = (() => {
|
||||||
|
const API = window.location.hostname === 'localhost' ? 'http://localhost:5000' : '/api';
|
||||||
|
let token = localStorage.getItem('jaeswift_token') || '';
|
||||||
|
|
||||||
|
// ─── Auth ───
|
||||||
|
function headers() {
|
||||||
|
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const user = document.getElementById('loginUser').value;
|
||||||
|
const pass = document.getElementById('loginPass').value;
|
||||||
|
const errEl = document.getElementById('loginError');
|
||||||
|
errEl.textContent = '';
|
||||||
|
try {
|
||||||
|
const r = await fetch(API + '/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: user, password: pass })
|
||||||
|
});
|
||||||
|
const d = await r.json();
|
||||||
|
if (d.token) {
|
||||||
|
token = d.token;
|
||||||
|
localStorage.setItem('jaeswift_token', token);
|
||||||
|
showApp();
|
||||||
|
} else {
|
||||||
|
errEl.textContent = 'ACCESS DENIED: ' + (d.error || 'Invalid credentials');
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errEl.textContent = 'CONNECTION FAILED';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
token = '';
|
||||||
|
localStorage.removeItem('jaeswift_token');
|
||||||
|
document.getElementById('adminApp').style.display = 'none';
|
||||||
|
document.getElementById('loginScreen').style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAuth() {
|
||||||
|
if (!token) return false;
|
||||||
|
try {
|
||||||
|
const r = await fetch(API + '/auth/check', { headers: headers() });
|
||||||
|
const d = await r.json();
|
||||||
|
return d.valid === true;
|
||||||
|
} catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showApp() {
|
||||||
|
document.getElementById('loginScreen').style.display = 'none';
|
||||||
|
document.getElementById('adminApp').style.display = '';
|
||||||
|
loadDashboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Section Switching ───
|
||||||
|
function showSection(name) {
|
||||||
|
document.querySelectorAll('.admin-section').forEach(s => s.style.display = 'none');
|
||||||
|
const target = document.getElementById('section-' + name);
|
||||||
|
if (target) target.style.display = '';
|
||||||
|
|
||||||
|
document.querySelectorAll('.sidebar-link').forEach(l => l.classList.remove('active'));
|
||||||
|
const link = document.querySelector('.sidebar-link[data-section="' + name + '"]');
|
||||||
|
if (link) link.classList.add('active');
|
||||||
|
|
||||||
|
const titles = { dashboard: 'DASHBOARD', posts: 'POSTS', editor: 'EDITOR', tracks: 'TRACKS', settings: 'SETTINGS' };
|
||||||
|
document.getElementById('topbarSection').textContent = titles[name] || name.toUpperCase();
|
||||||
|
|
||||||
|
if (name === 'dashboard') loadDashboard();
|
||||||
|
if (name === 'posts') loadPosts();
|
||||||
|
if (name === 'tracks') loadTracks();
|
||||||
|
if (name === 'settings') loadSettings();
|
||||||
|
if (name === 'editor') {
|
||||||
|
if (!document.getElementById('editSlugOriginal').value) {
|
||||||
|
document.getElementById('editorHeading').textContent = 'NEW TRANSMISSION';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Dashboard ───
|
||||||
|
async function loadDashboard() {
|
||||||
|
try {
|
||||||
|
const [posts, tracks, stats, services, threats] = await Promise.all([
|
||||||
|
fetch(API + '/posts').then(r => r.json()),
|
||||||
|
fetch(API + '/nowplaying').then(r => r.json()).catch(() => null),
|
||||||
|
fetch(API + '/stats').then(r => r.json()),
|
||||||
|
fetch(API + '/services').then(r => r.json()).catch(() => []),
|
||||||
|
fetch(API + '/threats').then(r => r.json()).catch(() => []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Fetch tracks count
|
||||||
|
let trackCount = '--';
|
||||||
|
try {
|
||||||
|
const tr = await fetch(API + '/tracks');
|
||||||
|
if (tr.ok) { trackCount = (await tr.json()).length; }
|
||||||
|
} catch {
|
||||||
|
// tracks endpoint might not exist, try loading from data
|
||||||
|
trackCount = '~35';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('dashPosts').textContent = posts.length;
|
||||||
|
document.getElementById('dashWords').textContent = posts.reduce((sum, p) => sum + (p.word_count || 0), 0).toLocaleString();
|
||||||
|
document.getElementById('dashTracks').textContent = trackCount;
|
||||||
|
document.getElementById('dashCPU').textContent = Math.round(stats.cpu_percent) + '%';
|
||||||
|
document.getElementById('dashMem').textContent = Math.round(stats.memory_percent) + '%';
|
||||||
|
document.getElementById('dashDisk').textContent = stats.disk_percent + '%';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
const svcEl = document.getElementById('dashServices');
|
||||||
|
svcEl.innerHTML = services.map(s => `
|
||||||
|
<div class="service-card ${s.status}">
|
||||||
|
<span class="service-dot">${s.status === 'online' ? '●' : '○'}</span>
|
||||||
|
<span class="service-name">${s.name}</span>
|
||||||
|
<span class="service-ms">${s.response_time_ms}ms</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Threats
|
||||||
|
const threatEl = document.getElementById('dashThreats');
|
||||||
|
threatEl.innerHTML = threats.length ? threats.map(t => `
|
||||||
|
<div class="threat-row">
|
||||||
|
<span class="threat-id">${t.id}</span>
|
||||||
|
<span class="threat-summary">${t.summary}</span>
|
||||||
|
<span class="threat-cvss cvss-${t.cvss >= 9 ? 'crit' : t.cvss >= 7 ? 'high' : t.cvss >= 4 ? 'med' : 'low'}">${t.cvss || '?'}</span>
|
||||||
|
</div>
|
||||||
|
`).join('') : '<span class="no-data">No threats detected</span>';
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Dashboard load error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Posts ───
|
||||||
|
async function loadPosts() {
|
||||||
|
const posts = await fetch(API + '/posts').then(r => r.json());
|
||||||
|
posts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||||
|
const tbody = document.getElementById('postsTableBody');
|
||||||
|
tbody.innerHTML = posts.map(p => `
|
||||||
|
<tr>
|
||||||
|
<td>${p.date || '--'}</td>
|
||||||
|
<td><a href="post.html?slug=${p.slug}" target="_blank" class="post-link">${p.title}</a></td>
|
||||||
|
<td>${(p.tags || []).map(t => '<span class="tag-pill">' + t + '</span>').join(' ')}</td>
|
||||||
|
<td>${p.word_count || 0}</td>
|
||||||
|
<td><span class="threat-badge threat-${p.threat_level || 'low'}">${(p.threat_level || 'low').toUpperCase()}</span></td>
|
||||||
|
<td class="actions-cell">
|
||||||
|
<button class="action-sm edit" onclick="AdminApp.editPost('${p.slug}')">EDIT</button>
|
||||||
|
<button class="action-sm delete" onclick="AdminApp.deletePost('${p.slug}')">DEL</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editPost(slug) {
|
||||||
|
const post = await fetch(API + '/posts/' + slug).then(r => r.json());
|
||||||
|
document.getElementById('editSlugOriginal').value = slug;
|
||||||
|
document.getElementById('edTitle').value = post.title || '';
|
||||||
|
document.getElementById('edSlug').value = post.slug || '';
|
||||||
|
document.getElementById('edDate').value = post.date || '';
|
||||||
|
document.getElementById('edTime').value = post.time || '00:00';
|
||||||
|
document.getElementById('edTags').value = (post.tags || []).join(', ');
|
||||||
|
document.getElementById('edExcerpt').value = post.excerpt || '';
|
||||||
|
document.getElementById('edContent').value = post.content || '';
|
||||||
|
document.getElementById('edMood').value = post.mood || 'focused';
|
||||||
|
document.getElementById('edEnergy').value = post.energy || 3;
|
||||||
|
document.getElementById('edEnergyVal').textContent = post.energy || 3;
|
||||||
|
document.getElementById('edMotivation').value = post.motivation || 3;
|
||||||
|
document.getElementById('edMotivationVal').textContent = post.motivation || 3;
|
||||||
|
document.getElementById('edFocus').value = post.focus || 3;
|
||||||
|
document.getElementById('edFocusVal').textContent = post.focus || 3;
|
||||||
|
document.getElementById('edDifficulty').value = post.difficulty || 3;
|
||||||
|
document.getElementById('edDifficultyVal').textContent = post.difficulty || 3;
|
||||||
|
document.getElementById('edCoffee').value = post.coffee || 2;
|
||||||
|
document.getElementById('edBPM').value = post.bpm || post.heart_rate || 72;
|
||||||
|
document.getElementById('edThreat').value = post.threat_level || 'low';
|
||||||
|
document.getElementById('editorHeading').textContent = 'EDIT TRANSMISSION';
|
||||||
|
updateWordCount();
|
||||||
|
updatePreview();
|
||||||
|
showSection('editor');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePost(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const slugOrig = document.getElementById('editSlugOriginal').value;
|
||||||
|
const title = document.getElementById('edTitle').value.trim();
|
||||||
|
const slug = document.getElementById('edSlug').value.trim() || title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
||||||
|
const data = {
|
||||||
|
title,
|
||||||
|
slug,
|
||||||
|
date: document.getElementById('edDate').value || new Date().toISOString().split('T')[0],
|
||||||
|
time: document.getElementById('edTime').value || '00:00',
|
||||||
|
time_written: document.getElementById('edTime').value || '00:00',
|
||||||
|
tags: document.getElementById('edTags').value.split(',').map(t => t.trim()).filter(Boolean),
|
||||||
|
excerpt: document.getElementById('edExcerpt').value.trim(),
|
||||||
|
content: document.getElementById('edContent').value,
|
||||||
|
mood: document.getElementById('edMood').value,
|
||||||
|
energy: parseInt(document.getElementById('edEnergy').value),
|
||||||
|
motivation: parseInt(document.getElementById('edMotivation').value),
|
||||||
|
focus: parseInt(document.getElementById('edFocus').value),
|
||||||
|
difficulty: parseInt(document.getElementById('edDifficulty').value),
|
||||||
|
coffee: parseInt(document.getElementById('edCoffee').value),
|
||||||
|
bpm: parseInt(document.getElementById('edBPM').value),
|
||||||
|
heart_rate: parseInt(document.getElementById('edBPM').value),
|
||||||
|
threat_level: document.getElementById('edThreat').value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const method = slugOrig ? 'PUT' : 'POST';
|
||||||
|
const url = slugOrig ? API + '/posts/' + slugOrig : API + '/posts';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(url, { method, headers: headers(), body: JSON.stringify(data) });
|
||||||
|
if (r.ok) {
|
||||||
|
clearEditor();
|
||||||
|
showSection('posts');
|
||||||
|
showNotification('TRANSMISSION ' + (slugOrig ? 'UPDATED' : 'SENT'));
|
||||||
|
} else {
|
||||||
|
const err = await r.json();
|
||||||
|
showNotification('ERROR: ' + (err.message || r.status), 'error');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showNotification('TRANSMISSION FAILED: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePost(slug) {
|
||||||
|
if (!confirm('Delete post "' + slug + '"? This cannot be undone.')) return;
|
||||||
|
const r = await fetch(API + '/posts/' + slug, { method: 'DELETE', headers: headers() });
|
||||||
|
if (r.ok) {
|
||||||
|
loadPosts();
|
||||||
|
showNotification('POST DELETED');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearEditor() {
|
||||||
|
document.getElementById('editSlugOriginal').value = '';
|
||||||
|
document.getElementById('editorForm').reset();
|
||||||
|
document.getElementById('edEnergyVal').textContent = '3';
|
||||||
|
document.getElementById('edMotivationVal').textContent = '3';
|
||||||
|
document.getElementById('edFocusVal').textContent = '3';
|
||||||
|
document.getElementById('edDifficultyVal').textContent = '3';
|
||||||
|
document.getElementById('edWordCount').textContent = '0 words';
|
||||||
|
document.getElementById('edPreview').innerHTML = '';
|
||||||
|
document.getElementById('editorHeading').textContent = 'NEW TRANSMISSION';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Markdown Helpers ───
|
||||||
|
function insertMD(before, after) {
|
||||||
|
const ta = document.getElementById('edContent');
|
||||||
|
const start = ta.selectionStart;
|
||||||
|
const end = ta.selectionEnd;
|
||||||
|
const sel = ta.value.substring(start, end);
|
||||||
|
ta.value = ta.value.substring(0, start) + before + sel + after + ta.value.substring(end);
|
||||||
|
ta.focus();
|
||||||
|
ta.selectionStart = start + before.length;
|
||||||
|
ta.selectionEnd = start + before.length + sel.length;
|
||||||
|
updateWordCount();
|
||||||
|
updatePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWordCount() {
|
||||||
|
const text = document.getElementById('edContent').value.trim();
|
||||||
|
const count = text ? text.split(/\s+/).length : 0;
|
||||||
|
document.getElementById('edWordCount').textContent = count + ' words';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
const md = document.getElementById('edContent').value;
|
||||||
|
document.getElementById('edPreview').innerHTML = renderMarkdown(md);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkdown(md) {
|
||||||
|
let html = md
|
||||||
|
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
|
||||||
|
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
|
||||||
|
.replace(/^#### (.+)$/gm, '<h4>$1</h4>')
|
||||||
|
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
|
||||||
|
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
||||||
|
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
||||||
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||||
|
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||||
|
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
||||||
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
||||||
|
.replace(/^---$/gm, '<hr>')
|
||||||
|
.replace(/\n\n/g, '</p><p>')
|
||||||
|
.replace(/\n/g, '<br>');
|
||||||
|
return '<div class="post-rendered"><p>' + html + '</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tracks ───
|
||||||
|
async function loadTracks() {
|
||||||
|
try {
|
||||||
|
// Try fetching from tracks API endpoint
|
||||||
|
let tracks;
|
||||||
|
try {
|
||||||
|
const r = await fetch(API + '/tracks');
|
||||||
|
if (r.ok) {
|
||||||
|
tracks = await r.json();
|
||||||
|
} else throw new Error();
|
||||||
|
} catch {
|
||||||
|
// Fallback: load from data file directly
|
||||||
|
tracks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = document.getElementById('tracksList');
|
||||||
|
if (!tracks.length) {
|
||||||
|
list.innerHTML = '<span class="no-data">No tracks loaded — API endpoint /api/tracks may need adding.</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
list.innerHTML = tracks.map((t, i) => `
|
||||||
|
<div class="track-row">
|
||||||
|
<span class="track-num">${String(i + 1).padStart(2, '0')}</span>
|
||||||
|
<span class="track-title">${t.track}</span>
|
||||||
|
<span class="track-artist">${t.artist}</span>
|
||||||
|
<span class="track-album">${t.album || ''}</span>
|
||||||
|
<button class="action-sm delete" onclick="AdminApp.deleteTrack(${i})">✕</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Track load error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddTrack() {
|
||||||
|
const f = document.getElementById('trackAddForm');
|
||||||
|
f.style.display = f.style.display === 'none' ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addTrack() {
|
||||||
|
const artist = document.getElementById('trackArtist').value.trim();
|
||||||
|
const track = document.getElementById('trackTitle').value.trim();
|
||||||
|
const album = document.getElementById('trackAlbum').value.trim();
|
||||||
|
if (!artist || !track) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await fetch(API + '/tracks', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: headers(),
|
||||||
|
body: JSON.stringify({ artist, track, album })
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
document.getElementById('trackArtist').value = '';
|
||||||
|
document.getElementById('trackTitle').value = '';
|
||||||
|
document.getElementById('trackAlbum').value = '';
|
||||||
|
loadTracks();
|
||||||
|
showNotification('TRACK ADDED');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showNotification('Failed to add track', 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteTrack(index) {
|
||||||
|
try {
|
||||||
|
const r = await fetch(API + '/tracks/' + index, { method: 'DELETE', headers: headers() });
|
||||||
|
if (r.ok) loadTracks();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Settings ───
|
||||||
|
async function loadSettings() {
|
||||||
|
try {
|
||||||
|
const settings = await fetch(API + '/settings').then(r => r.json());
|
||||||
|
if (settings.weather !== undefined) document.getElementById('setWeather').checked = settings.weather;
|
||||||
|
if (settings.nowplaying !== undefined) document.getElementById('setNowPlaying').checked = settings.nowplaying;
|
||||||
|
if (settings.threats !== undefined) document.getElementById('setThreats').checked = settings.threats;
|
||||||
|
if (settings.terminal !== undefined) document.getElementById('setTerminal').checked = settings.terminal;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettings(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const data = {
|
||||||
|
weather: document.getElementById('setWeather').checked,
|
||||||
|
nowplaying: document.getElementById('setNowPlaying').checked,
|
||||||
|
threats: document.getElementById('setThreats').checked,
|
||||||
|
terminal: document.getElementById('setTerminal').checked,
|
||||||
|
};
|
||||||
|
await fetch(API + '/settings', { method: 'PUT', headers: headers(), body: JSON.stringify(data) });
|
||||||
|
showNotification('SETTINGS SAVED');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Notifications ───
|
||||||
|
function showNotification(msg, type = 'success') {
|
||||||
|
let notif = document.querySelector('.admin-notification');
|
||||||
|
if (!notif) {
|
||||||
|
notif = document.createElement('div');
|
||||||
|
notif.className = 'admin-notification';
|
||||||
|
document.body.appendChild(notif);
|
||||||
|
}
|
||||||
|
notif.textContent = msg;
|
||||||
|
notif.className = 'admin-notification ' + type + ' show';
|
||||||
|
setTimeout(() => notif.classList.remove('show'), 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Init ───
|
||||||
|
async function init() {
|
||||||
|
// Check if already authed
|
||||||
|
if (await checkAuth()) {
|
||||||
|
showApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login form
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', login);
|
||||||
|
|
||||||
|
// Editor form
|
||||||
|
document.getElementById('editorForm').addEventListener('submit', savePost);
|
||||||
|
|
||||||
|
// Settings form
|
||||||
|
document.getElementById('settingsForm').addEventListener('submit', saveSettings);
|
||||||
|
|
||||||
|
// Auto slug from title
|
||||||
|
document.getElementById('edTitle').addEventListener('input', (e) => {
|
||||||
|
const slugField = document.getElementById('edSlug');
|
||||||
|
if (!document.getElementById('editSlugOriginal').value) {
|
||||||
|
slugField.value = e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Content updates
|
||||||
|
document.getElementById('edContent').addEventListener('input', () => {
|
||||||
|
updateWordCount();
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Energy range display
|
||||||
|
document.getElementById('edEnergy').addEventListener('input', (e) => {
|
||||||
|
document.getElementById('edEnergyVal').textContent = e.target.value;
|
||||||
|
});
|
||||||
|
document.getElementById('edMotivation').addEventListener('input', (e) => {
|
||||||
|
document.getElementById('edMotivationVal').textContent = e.target.value;
|
||||||
|
});
|
||||||
|
document.getElementById('edFocus').addEventListener('input', (e) => {
|
||||||
|
document.getElementById('edFocusVal').textContent = e.target.value;
|
||||||
|
});
|
||||||
|
document.getElementById('edDifficulty').addEventListener('input', (e) => {
|
||||||
|
document.getElementById('edDifficultyVal').textContent = e.target.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set today's date default
|
||||||
|
document.getElementById('edDate').value = new Date().toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
return {
|
||||||
|
showSection,
|
||||||
|
logout,
|
||||||
|
editPost,
|
||||||
|
deletePost,
|
||||||
|
clearEditor,
|
||||||
|
insertMD,
|
||||||
|
showAddTrack,
|
||||||
|
addTrack,
|
||||||
|
deleteTrack,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
@ -866,6 +866,8 @@
|
||||||
if (feelsEl) feelsEl.textContent = 'FEELS ' + d.feels_like + '°C';
|
if (feelsEl) feelsEl.textContent = 'FEELS ' + d.feels_like + '°C';
|
||||||
const windEl = document.getElementById('weatherWind');
|
const windEl = document.getElementById('weatherWind');
|
||||||
if (windEl) windEl.textContent = d.wind_kph + ' KPH ' + d.wind_dir;
|
if (windEl) windEl.textContent = d.wind_kph + ' KPH ' + d.wind_dir;
|
||||||
|
const humEl = document.getElementById('weatherHumidity');
|
||||||
|
if (humEl) humEl.textContent = d.humidity + '% RH';
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
29
js/post.js
29
js/post.js
|
|
@ -102,18 +102,35 @@
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Mood Icons Map ───
|
||||||
|
const moodIcons = {
|
||||||
|
'focused': '◎', 'creative': '✦', 'productive': '⚡',
|
||||||
|
'tired': '◡', 'wired': '⚡', 'chaotic': '✸',
|
||||||
|
'locked-in': '◉', 'zen': '☯'
|
||||||
|
};
|
||||||
|
const moodColors = {
|
||||||
|
'focused': '#00ffc8', 'creative': '#a855f7', 'productive': '#22d3ee',
|
||||||
|
'tired': '#6b7280', 'wired': '#f59e0b', 'chaotic': '#ff4757',
|
||||||
|
'locked-in': '#00ff88', 'zen': '#818cf8'
|
||||||
|
};
|
||||||
|
|
||||||
// ─── Render Operator Stats ───
|
// ─── Render Operator Stats ───
|
||||||
function renderStats(post) {
|
function renderStats(post) {
|
||||||
const el = document.getElementById('postStats');
|
const el = document.getElementById('postStats');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
const coffee = '☕'.repeat(post.coffee || 0) +
|
const moodStr = (post.mood || 'focused').toLowerCase();
|
||||||
'<span style="opacity:0.2">' + '☕'.repeat(5 - (post.coffee || 0)) + '</span>';
|
const moodIcon = moodIcons[moodStr] || '◈';
|
||||||
|
const moodColor = moodColors[moodStr] || '#00ffc8';
|
||||||
|
const hr = post.heart_rate || post.bpm || 72;
|
||||||
|
|
||||||
|
const coffee = '☕'.repeat(Math.min(post.coffee || 0, 10)) +
|
||||||
|
'<span style="opacity:0.2">' + '☕'.repeat(Math.max(0, 5 - (post.coffee || 0))) + '</span>';
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="stat-row">
|
<div class="stat-row mood-row">
|
||||||
<span class="stat-label">MOOD</span>
|
<span class="stat-label">MOOD</span>
|
||||||
${buildPips(post.mood)}
|
<span class="stat-mood-badge" style="color:${moodColor};border-color:${moodColor}">${moodIcon} ${moodStr.toUpperCase()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-row">
|
<div class="stat-row">
|
||||||
<span class="stat-label">ENERGY</span>
|
<span class="stat-label">ENERGY</span>
|
||||||
|
|
@ -134,7 +151,7 @@
|
||||||
<div class="stat-divider"></div>
|
<div class="stat-divider"></div>
|
||||||
<div class="stat-big-row">
|
<div class="stat-big-row">
|
||||||
<div class="stat-bpm-display">
|
<div class="stat-bpm-display">
|
||||||
<span class="stat-bpm-value" id="bpmValue">${post.heart_rate || '---'}</span>
|
<span class="stat-bpm-value" id="bpmValue">${hr}</span>
|
||||||
<span class="stat-bpm-unit">BPM</span>
|
<span class="stat-bpm-unit">BPM</span>
|
||||||
<div class="heartbeat-line" id="heartbeatLine"></div>
|
<div class="heartbeat-line" id="heartbeatLine"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -146,7 +163,7 @@
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Animate heartbeat
|
// Animate heartbeat
|
||||||
animateHeartbeat(post.heart_rate || 72);
|
animateHeartbeat(hr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Heartbeat Animation ───
|
// ─── Heartbeat Animation ───
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue