diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..4ff0955 --- /dev/null +++ b/admin.html @@ -0,0 +1,314 @@ + + + + + + JAESWIFT // ADMIN + + + + + + + +
+
+ + + +
+
+ + + + + + + diff --git a/api/app.py b/api/app.py index 8339b1b..2a46823 100644 --- a/api/app.py +++ b/api/app.py @@ -221,6 +221,35 @@ def now_playing(): t = random.choice(tracks) 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/', 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) ─────────────────── @app.route('/api/git-activity') def git_activity(): diff --git a/css/admin.css b/css/admin.css new file mode 100644 index 0000000..7b090b0 --- /dev/null +++ b/css/admin.css @@ -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); + } +} diff --git a/css/post.css b/css/post.css index f4beb73..468c125 100644 --- a/css/post.css +++ b/css/post.css @@ -277,6 +277,22 @@ 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 { display: flex; gap: 4px; diff --git a/css/style.css b/css/style.css index 5e00f6c..c5bd892 100644 --- a/css/style.css +++ b/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; 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; } +} diff --git a/index.html b/index.html index 29ca372..a4b9de6 100644 --- a/index.html +++ b/index.html @@ -214,6 +214,44 @@ + +
+
+
+ WEATHER + ● LIVE +
+
+
+ --°C + LOADING... +
+
+ FEELS --°C + -- KPH + --% RH +
+
+
+
+
+ NOW PLAYING + ● STREAMING +
+
+
---
+
---
+
---
+
+ + + +
+
+
+
+ +
diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..ced6d9a --- /dev/null +++ b/js/admin.js @@ -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 => ` +
+ ${s.status === 'online' ? '●' : '○'} + ${s.name} + ${s.response_time_ms}ms +
+ `).join(''); + + // Threats + const threatEl = document.getElementById('dashThreats'); + threatEl.innerHTML = threats.length ? threats.map(t => ` +
+ ${t.id} + ${t.summary} + ${t.cvss || '?'} +
+ `).join('') : 'No threats detected'; + + } 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 => ` + + ${p.date || '--'} + ${p.title} + ${(p.tags || []).map(t => '' + t + '').join(' ')} + ${p.word_count || 0} + ${(p.threat_level || 'low').toUpperCase()} + + + + + + `).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, '
$1
') + .replace(/`([^`]+)`/g, '$1') + .replace(/^#### (.+)$/gm, '

$1

') + .replace(/^### (.+)$/gm, '

$1

') + .replace(/^## (.+)$/gm, '

$1

') + .replace(/^# (.+)$/gm, '

$1

') + .replace(/\*\*(.+?)\*\*/g, '$1') + .replace(/\*(.+?)\*/g, '$1') + .replace(/^- (.+)$/gm, '
  • $1
  • ') + .replace(/(
  • .*<\/li>)/s, '
      $1
    ') + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') + .replace(/^---$/gm, '
    ') + .replace(/\n\n/g, '

    ') + .replace(/\n/g, '
    '); + return '

    ' + html + '

    '; + } + + // ─── 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 = 'No tracks loaded — API endpoint /api/tracks may need adding.'; + return; + } + list.innerHTML = tracks.map((t, i) => ` +
    + ${String(i + 1).padStart(2, '0')} + ${t.track} + ${t.artist} + ${t.album || ''} + +
    + `).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, + }; +})(); diff --git a/js/main.js b/js/main.js index 949f5ff..7f3f08e 100644 --- a/js/main.js +++ b/js/main.js @@ -866,6 +866,8 @@ if (feelsEl) feelsEl.textContent = 'FEELS ' + d.feels_like + '°C'; const windEl = document.getElementById('weatherWind'); if (windEl) windEl.textContent = d.wind_kph + ' KPH ' + d.wind_dir; + const humEl = document.getElementById('weatherHumidity'); + if (humEl) humEl.textContent = d.humidity + '% RH'; }) .catch(() => {}); } diff --git a/js/post.js b/js/post.js index 046cc39..0ec109c 100644 --- a/js/post.js +++ b/js/post.js @@ -102,18 +102,35 @@ }, { 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 ─── function renderStats(post) { const el = document.getElementById('postStats'); if (!el) return; - const coffee = '☕'.repeat(post.coffee || 0) + - '' + '☕'.repeat(5 - (post.coffee || 0)) + ''; + const moodStr = (post.mood || 'focused').toLowerCase(); + 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)) + + '' + '☕'.repeat(Math.max(0, 5 - (post.coffee || 0))) + ''; el.innerHTML = ` -
    +
    MOOD - ${buildPips(post.mood)} + ${moodIcon} ${moodStr.toUpperCase()}
    ENERGY @@ -134,7 +151,7 @@
    - ${post.heart_rate || '---'} + ${hr} BPM
    @@ -146,7 +163,7 @@ `; // Animate heartbeat - animateHeartbeat(post.heart_rate || 72); + animateHeartbeat(hr); } // ─── Heartbeat Animation ───