From 74229b4471eb72917e74d42f0e9ffd55f5c76836 Mon Sep 17 00:00:00 2001 From: st Date: Wed, 8 Jul 2026 15:34:08 -0500 Subject: [PATCH] first commit --- .gitignore | 4 + index.html | 121 +++++++++ main.js | 134 ++++++++++ style.css | 708 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 967 insertions(+) create mode 100644 .gitignore create mode 100644 index.html create mode 100644 main.js create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42a5b80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +admin.html +admin.js +API_REFERENCE.md +tachyon_merged_dynamic.html \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..cd075c9 --- /dev/null +++ b/index.html @@ -0,0 +1,121 @@ + + + + + + tachyon + + + + +
+
+
+ initializing tachyon + please wait +
+ +
+
+ + + +
+
+
+
+
+
news
+
+ +

Loading news…

+
+
+
+ +
+
+
services
+
+
service one
+
service two
+
service three
+
service four
+
service five
+
+
+
+ +
+
+
overview
+
+ +
+
current users
+
Loading…
+
+ +
+
recent activity
+
Loading…
+
+ +
+
general statistics
+
+

loading…

+
+
+ +
+
+
+ +
+ + +
+
+
+
media
+
+

loading media…

+
+
+
+
+ +
+
+ + + + + + + diff --git a/main.js b/main.js new file mode 100644 index 0000000..e3cc346 --- /dev/null +++ b/main.js @@ -0,0 +1,134 @@ +window.addEventListener('DOMContentLoaded', () => { + bootstrapAdmin(); +}); + +const API_BASE = "http://10.0.0.66:7455/api"; + +// Small helper: fetch JSON, throw on non-2xx so callers can catch() it. +async function getJSON(path) { + const res = await fetch(`${API_BASE}${path}`); + if (!res.ok) throw new Error(`${path} responded ${res.status}`); + return res.json(); +} + +function timeAgo(isoString) { + const diffMs = Date.now() - new Date(isoString).getTime(); + const mins = Math.floor(diffMs / 60000); + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + return `${Math.floor(hours / 24)}d ago`; +} + +function escapeHtml(str) { + const div = document.createElement("div"); + div.textContent = str; + return div.innerHTML; +} + +// ---------- News ---------- +async function loadNews() { + const el = document.getElementById("news-list"); + try { + const posts = await getJSON("/news?limit=10"); + if (posts.length === 0) { + el.innerHTML = `

No news yet.

`; + return; + } + el.innerHTML = posts.map(post => ` +
+

${escapeHtml(post.title)}${timeAgo(post.created_at)}

+

${escapeHtml(post.content)}

+
+ `).join(""); + } catch (err) { + console.error("Failed to load news:", err); + el.innerHTML = `

Couldn't load news right now.

`; + } +} + +// ---------- Media ---------- +async function loadMedia() { + const el = document.getElementById("media-grid"); + try { + const items = await getJSON("/media?limit=8"); + if (items.length === 0) { + el.innerHTML = `

No media yet.

`; + return; + } + el.innerHTML = items.map(item => ` +
+

${escapeHtml(item.title)}

+ ${item.media_type === "video" + ? `` + : `${escapeHtml(item.title)}` + } +
+ `).join(""); + } catch (err) { + console.error("Failed to load media:", err); + el.innerHTML = `

Couldn't load media right now.

`; + } +} + +// ---------- Overview (current users / recent activity / general stats) ---------- +async function loadOverview() { + const usersEl = document.getElementById("current-users"); + const activityEl = document.getElementById("recent-activity"); + const generalEl = document.getElementById("general-stats"); + + try { + const data = await getJSON("/stats/overview"); + + usersEl.innerHTML = data.currentUsers.length + ? data.currentUsers.map(u => ` +
${escapeHtml(u.username)}now
+ `).join("") + : `
No one online
`; + + activityEl.innerHTML = data.recentActivity.length + ? data.recentActivity.map(a => ` +
${escapeHtml(a.description)}${timeAgo(a.created_at)}
+ `).join("") + : `
No recent activity
`; + + const { users, reports, build } = data.general; + generalEl.innerHTML = ` +
+

Users

+

Total: ${users.total}
Recent: ${users.mostRecent ? escapeHtml(users.mostRecent.username) : "—"}

+
+
+

Reports

+

Named: ${reports.named}
Associated: ${reports.associated}
Logged: ${reports.logged} time(s)

+
+
+

Build

+

Pipeline: ${build.pipeline}
Build: ${build.buildNumber}
Version: ${build.version}

+
+ `; + } catch (err) { + console.error("Failed to load overview stats:", err); + usersEl.innerHTML = `
no active users!
`; + activityEl.innerHTML = `
no activity!
`; + generalEl.innerHTML = `

no statistics!

`; + } +} + +function loadAll() { + loadNews(); + loadMedia(); + loadOverview(); +} + +async function bootstrapAdmin() { + try { + await loadAll(); + } finally { + document.body.classList.remove('is-loading'); + } + + setInterval(loadOverview, 30000); +} +// Keep "current users" / "recent activity" feeling live without a full reload. +setInterval(loadOverview, 30000); \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..0990c3c --- /dev/null +++ b/style.css @@ -0,0 +1,708 @@ +:root { + --accent: rgb(151, 110, 255); + --accent-dark: rgba(151, 110, 255, 0.5); + + /* ImGui window chrome, straight from the original snippet */ + --outer-fill: rgb(30, 30, 30); + --inner-fill: rgb(15, 15, 15); + --border-black: rgb(0, 0, 0); + --border-grey: rgb(46, 46, 46); + --outline: rgb(60, 60, 60); + + --page-bg: #17171a; + --text: #ffffff; + --text-dim: rgb(150, 150, 155); + --text-dimmer: rgb(95, 95, 100); + } + + * { box-sizing: border-box; } + + html, body { + margin: 0; + padding: 0; + background: var(--page-bg); + } + + body.is-loading { + overflow: hidden; + } + + body { + color: var(--text); + /*font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + font-size: 13px; + text-shadow: 0 1px 0 rgba(0,0,0,0.8);*/ + font-family: Tahoma, Geneva, sans-serif; + font-size: 13px; + text-rendering: optimizeSpeed; + -webkit-font-smoothing: none; /* mac webkit only, toggles grayscale vs subpixel */ + font-smoothing: none; + padding-bottom: 96px; + } + + a { color: var(--text); text-decoration: none; } + + .loading-screen { + position: fixed; + inset: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + background: transparent; + } + + .loading-bar-shell { + width: min(760px, calc(100vw - 16px)); + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 2px var(--border-grey); + padding: 8px; + } + + .loading-bar-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; + font-size: 11px; + text-transform: lowercase; + letter-spacing: 0.5px; + } + + .loading-label { + color: var(--accent); + font-weight: 700; + } + + .loading-status { + color: var(--text-dimmer); + white-space: nowrap; + } + + .loading-track { + height: 8px; + background: var(--inner-fill); + border: 1px solid var(--border-grey); + overflow: hidden; + } + + .loading-fill { + width: 100%; + height: 100%; + background: linear-gradient(90deg, rgba(151, 110, 255, 0.25), var(--accent), rgba(151, 110, 255, 0.25)); + transform-origin: left center; + animation: loading-fill 5s linear forwards; + } + + @keyframes loading-fill { + from { transform: scaleX(0); } + to { transform: scaleX(1); } + } + + body:not(.is-loading) .loading-screen { + display: none; + } + + .page-container { + opacity: 1; + transform: translateY(0); + transition: opacity 450ms ease, transform 450ms ease; + will-change: opacity, transform; + } + + body.is-loading .page-container { + opacity: 0; + transform: translateY(8px); + pointer-events: none; + } + + body:not(.is-loading) .page-container { + opacity: 1; + transform: translateY(0); + } + + ::-webkit-scrollbar { width: 6px; height: 6px; } + ::-webkit-scrollbar-thumb { background: var(--accent); border: 1px solid black; } + ::-webkit-scrollbar-track { background: black; } + + /* ============================================================ + ImGui window chrome — the shared unit every section is built from. + outer fill (30,30,30) -> 1px top accent line -> inner fill (15,15,15) + with its own top accent line -> double border (black, then grey) + ============================================================ */ + .imgui-window { + position: relative; + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 2px var(--border-grey); + padding: 2px; + } + + .imgui-window::before { + content: ""; + position: absolute; + top: 2px; left: 2px; right: 2px; + height: 1px; + background: linear-gradient(to right, transparent, var(--accent), transparent); + } + + .imgui-content { + position: relative; + background: var(--inner-fill); + border: 1px solid var(--border-grey); + padding: 8px; + } + + .imgui-content::before { + content: ""; + position: absolute; + top: 0; left: 2px; right: 2px; + height: 1px; + background: var(--accent); + } + + .imgui-heading { + color: var(--accent); + font-size: 12px; + font-weight: 600; + text-transform: lowercase; + letter-spacing: 0.5px; + margin: 2px 0 8px 2px; + } + + /* ============ NAV ============ */ + .navigation-bar { + position: sticky; + top: 0; + z-index: 50; + margin: 8px 8px 0 8px; + } + + .navigation-bar .imgui-content { + display: flex; + align-items: center; + padding: 6px 10px; + } + + .navigation-logo { + width: 16px; height: 16px; + margin-right: 8px; + /* background: var(--accent); */ + /* box-shadow: 0 0 8px 1px rgba(127,72,163,0.5); */ + flex-shrink: 0; + } + + .nav-spacer { + font-weight: 700; + letter-spacing: 0.4px; + margin-right: 10px; + } + + .nav-date { + color: var(--text-dimmer); + font-size: 11.5px; + margin-right: 10px; + } + + .nav-border { + width: 1px; + height: 16px; + background: var(--outline); + margin-right: 10px; + } + + .nav-links { + display: flex; + align-items: center; + gap: 16px; + margin-left: auto; + } + + .nav-link { + font-size: 12.5px; + color: var(--text-dim); + transition: color .1s ease; + } + .nav-link:hover { color: var(--accent); } + + @media (max-width: 720px) { + .nav-links { display: none; } + } + + /* ============ CONTAINER / LAYOUT ============ */ + .page-container { + margin: 8px; + } + + .sections-container { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; + } + + @media (max-width: 900px) { + .sections-container { flex-direction: column; } + } + + .left-section, .right-section { + display: flex; + flex-direction: column; + gap: 10px; + } + + .left-section { flex: 1 1 55%; min-width: 320px; } + .right-section { flex: 1 1 38%; min-width: 260px; } + + /* ============ LIST BOX (news / stats scroll areas) ============ */ + .list-box { + max-height: 250px; + overflow-y: auto; + padding-right: 2px; + } + + .item-card { + position: relative; + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + padding: 8px 9px; + margin-bottom: 7px; + } + .item-card:last-child { margin-bottom: 0; } + + .item-title { + font-weight: 600; + font-size: 12.5px; + margin: 0 0 3px 0; + } + + .item-timestamp { + float: right; + color: var(--text-dimmer); + font-size: 10.5px; + font-weight: 400; + } + + .item-content { + font-size: 12px; + color: var(--text-dim); + margin: 0; + line-height: 1.5; + } + + /* ============ SERVICES STRIP ============ */ + .services-strip { + display: flex; + gap: 8px; + overflow-x: auto; + padding-bottom: 2px; + } + + .service-chip { + font-size: 11px; + color: var(--text-dim); + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + padding: 6px 11px; + white-space: nowrap; + display: flex; + align-items: center; + gap: 6px; + } + + .service-chip .dot { + width: 5px; height: 5px; + background: var(--accent); + box-shadow: 0 0 6px 0 rgba(127,72,163,0.6); + } + + /* ============ STATS GRID ============ */ + .stat-columns { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + } + + @media (max-width: 680px) { + .stat-columns { grid-template-columns: 1fr; } + } + + .stat-col-title { + font-size: 10.5px; + text-transform: lowercase; + letter-spacing: 0.5px; + color: var(--text-dimmer); + margin-bottom: 6px; + } + + .stat-row { + display: flex; + justify-content: space-between; + font-size: 11.5px; + padding: 4px 6px; + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + margin-bottom: 5px; + } + .stat-row:last-child { margin-bottom: 0; } + .stat-row .val { color: var(--text-dim); } + + .stat-card { + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + padding: 7px 9px; + margin-bottom: 6px; + } + .stat-card:last-child { margin-bottom: 0; } + + .stat-card h4 { + margin: 0 0 4px 0; + font-size: 12px; + color: var(--accent); + } + + .stat-card p { + margin: 0; + font-size: 10.5px; + color: var(--text-dim); + line-height: 1.6; + } + + /* ============ MEDIA GRID ============ */ + .media-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; + } + + .media-item { + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + padding: 6px; + } + + .media-title { + font-size: 11px; + font-weight: 600; + margin: 0 0 5px 2px; + } + + .img-embed { + position: relative; + aspect-ratio: 16/9; + background: repeating-linear-gradient(45deg, #101012 0, #101012 8px, #131316 8px, #131316 16px); + border: 1px solid var(--border-grey); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-dimmer); + font-size: 10.5px; + } + + /* ============ FOOTER ============ */ + .footer { + position: fixed; + left: 8px; + right: 8px; + bottom: 8px; + margin: 0; + z-index: 40; + } + + .footer .imgui-content { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px 0; + padding: 10px 14px; + } + + .footer-tagline { + font-size: 11.5px; + color: var(--text-dimmer); + margin-right: 14px; + } + + .footer-links { + display: flex; + gap: 16px; + margin-left: auto; + } + + .footer-link { + font-size: 12px; + color: var(--text-dim); + transition: color .1s ease; + } + .footer-link:hover { color: var(--accent); } + + /* ============ ADMIN PANEL ============ */ + .admin-grid { + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(300px, 0.85fr); + gap: 10px; + align-items: start; + } + + @media (max-width: 980px) { + .admin-grid { + grid-template-columns: 1fr; + } + } + + .admin-stack { + display: flex; + flex-direction: column; + gap: 10px; + } + + .panel-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; + } + + .panel-note { + color: var(--text-dimmer); + font-size: 11px; + line-height: 1.5; + margin: 0; + } + + .status-pill { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + background: var(--outer-fill); + font-size: 11px; + color: var(--text-dim); + white-space: nowrap; + } + + .status-pill .dot { + width: 6px; + height: 6px; + background: var(--accent); + box-shadow: 0 0 6px rgba(151, 110, 255, 0.55); + } + + .admin-form { + display: grid; + gap: 8px; + } + + .admin-form .field-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + } + + @media (max-width: 560px) { + .admin-form .field-grid { + grid-template-columns: 1fr; + } + } + + .field { + display: grid; + gap: 5px; + } + + .field label { + font-size: 10.5px; + color: var(--text-dimmer); + text-transform: lowercase; + letter-spacing: 0.4px; + } + + .field input, + .field select, + .field textarea { + width: 100%; + color: var(--text); + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + padding: 8px 9px; + font: inherit; + resize: vertical; + } + + .field textarea { + min-height: 92px; + } + + .field input:focus, + .field select:focus, + .field textarea:focus, + .action-button:focus { + outline: 1px solid var(--accent); + outline-offset: 1px; + } + + .action-button { + appearance: none; + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + background: linear-gradient(to bottom, #2a2a2a, #1b1b1b); + color: var(--text); + padding: 8px 11px; + font: inherit; + font-size: 12px; + cursor: pointer; + } + + .action-button:hover { + color: var(--accent); + } + + .action-button.is-danger { + color: #ffb6b6; + } + + .action-button.is-danger:hover { + color: #ff8080; + } + + .action-button.is-inline { + padding: 5px 8px; + font-size: 11px; + } + + .action-toolbar { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + .record-list { + display: grid; + gap: 6px; + max-height: 280px; + overflow: auto; + padding-right: 2px; + } + + .record-card { + display: grid; + gap: 6px; + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + padding: 8px 9px; + } + + .record-topline { + display: flex; + gap: 8px; + justify-content: space-between; + align-items: start; + } + + .record-title { + margin: 0; + font-weight: 600; + font-size: 12px; + } + + .record-meta { + margin: 0; + color: var(--text-dimmer); + font-size: 10.5px; + } + + .record-body { + margin: 0; + color: var(--text-dim); + font-size: 11.5px; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; + } + + .admin-toast { + font-size: 11px; + color: var(--text-dimmer); + min-height: 1.3em; + } + + .admin-toast.is-error { + color: #ff8d8d; + } + + .admin-toast.is-success { + color: #8bd18b; + } + + .mini-columns { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + } + + @media (max-width: 640px) { + .mini-columns { + grid-template-columns: 1fr; + } + } + + .mini-card { + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + padding: 8px 9px; + } + + .mini-card h4 { + margin: 0 0 4px 0; + font-size: 12px; + color: var(--accent); + } + + .mini-card p { + margin: 0; + font-size: 10.5px; + color: var(--text-dim); + line-height: 1.55; + } + + .endpoint-list { + display: grid; + gap: 6px; + } + + .endpoint-item { + display: flex; + gap: 8px; + justify-content: space-between; + align-items: center; + background: var(--outer-fill); + border: 1px solid var(--border-black); + box-shadow: 0 0 0 1px var(--border-grey); + padding: 6px 8px; + font-size: 11px; + } + + .endpoint-item .route { + color: var(--accent); + white-space: nowrap; + } + + .endpoint-item .desc { + color: var(--text-dimmer); + text-align: right; + } \ No newline at end of file