first commit

This commit is contained in:
st committed 2026-07-08 15:34:08 -05:00
commit 74229b4471
4 files changed
+967

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
admin.html
admin.js
API_REFERENCE.md
tachyon_merged_dynamic.html
+121
View File
@@ -0,0 +1,121 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tachyon</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body class="is-loading">
<div class="loading-screen" aria-live="polite" aria-busy="true">
<div class="loading-bar-shell">
<div class="loading-bar-row">
<span class="loading-label">initializing tachyon</span>
<span class="loading-status">please wait</span>
</div>
<div class="loading-track" aria-hidden="true">
<div class="loading-fill"></div>
</div>
</div>
</div>
<div class="navigation-bar imgui-window">
<div class="imgui-content">
<div class="navigation-logo">
<img class="navigation-logo" src="./assets/logo.png" />
</div>
<span class="nav-spacer">tachyon</span>
<span class="nav-date">placeholder date</span>
<div class="nav-border"></div>
<div class="nav-links">
<a href="#" class="nav-link">documentation</a>
<div class="nav-border"></div>
<a href="#" class="nav-link">panel</a>
<a href="#" class="nav-link">register</a>
</div>
</div>
</div>
<div class="page-container">
<div class="sections-container">
<div class="left-section">
<div class="imgui-window">
<div class="imgui-content">
<div class="imgui-heading">news</div>
<div class="list-box" id="news-list">
<!-- populated by fetchNews() — this placeholder shows only if the fetch fails -->
<div class="item-card"><p class="item-content">Loading news…</p></div>
</div>
</div>
</div>
<div class="imgui-window">
<div class="imgui-content">
<div class="imgui-heading">services</div>
<div class="services-strip">
<div class="service-chip"><div class="dot"></div>service one</div>
<div class="service-chip"><div class="dot"></div>service two</div>
<div class="service-chip"><div class="dot"></div>service three</div>
<div class="service-chip"><div class="dot"></div>service four</div>
<div class="service-chip"><div class="dot"></div>service five</div>
</div>
</div>
</div>
<div class="imgui-window">
<div class="imgui-content">
<div class="imgui-heading">overview</div>
<div class="stat-columns">
<div>
<div class="stat-col-title">current users</div>
<div id="current-users"><div class="stat-row"><span>Loading…</span><span class="val"></span></div></div>
</div>
<div>
<div class="stat-col-title">recent activity</div>
<div id="recent-activity"><div class="stat-row"><span>Loading…</span><span class="val"></span></div></div>
</div>
<div>
<div class="stat-col-title">general statistics</div>
<div id="general-stats">
<div class="stat-card"><h4>loading…</h4></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- RIGHT COLUMN -->
<div class="right-section">
<div class="imgui-window">
<div class="imgui-content">
<div class="imgui-heading">media</div>
<div class="media-grid" id="media-grid">
<div class="media-item"><p class="media-title">loading media…</p></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="footer imgui-window">
<div class="imgui-content">
<span class="footer-tagline">server powered by a somali guy on a bike</span>
<div class="footer-links">
<a href="#" class="footer-link">powered by radium technologies</a>
</div>
</div>
</div>
<script src="./main.js"></script>
</body>
</html>
+134
View File
@@ -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 = `<div class="item-card"><p class="item-content">No news yet.</p></div>`;
return;
}
el.innerHTML = posts.map(post => `
<div class="item-card">
<p class="item-title">${escapeHtml(post.title)}<span class="item-timestamp">${timeAgo(post.created_at)}</span></p>
<p class="item-content">${escapeHtml(post.content)}</p>
</div>
`).join("");
} catch (err) {
console.error("Failed to load news:", err);
el.innerHTML = `<div class="item-card"><p class="item-content">Couldn't load news right now.</p></div>`;
}
}
// ---------- Media ----------
async function loadMedia() {
const el = document.getElementById("media-grid");
try {
const items = await getJSON("/media?limit=8");
if (items.length === 0) {
el.innerHTML = `<div class="media-item"><p class="media-title">No media yet.</p></div>`;
return;
}
el.innerHTML = items.map(item => `
<div class="media-item">
<p class="media-title">${escapeHtml(item.title)}</p>
${item.media_type === "video"
? `<video class="img-embed" src="${escapeHtml(item.url)}" controls style="width:100%;height:auto;background:#000;"></video>`
: `<img class="img-embed" src="${escapeHtml(item.url)}" alt="${escapeHtml(item.title)}" style="width:100%;height:auto;object-fit:cover;">`
}
</div>
`).join("");
} catch (err) {
console.error("Failed to load media:", err);
el.innerHTML = `<div class="media-item"><p class="media-title">Couldn't load media right now.</p></div>`;
}
}
// ---------- 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 => `
<div class="stat-row"><span>${escapeHtml(u.username)}</span><span class="val">now</span></div>
`).join("")
: `<div class="stat-row"><span>No one online</span><span class="val"></span></div>`;
activityEl.innerHTML = data.recentActivity.length
? data.recentActivity.map(a => `
<div class="stat-row"><span>${escapeHtml(a.description)}</span><span class="val">${timeAgo(a.created_at)}</span></div>
`).join("")
: `<div class="stat-row"><span>No recent activity</span><span class="val"></span></div>`;
const { users, reports, build } = data.general;
generalEl.innerHTML = `
<div class="stat-card">
<h4>Users</h4>
<p>Total: ${users.total}<br>Recent: ${users.mostRecent ? escapeHtml(users.mostRecent.username) : "—"}</p>
</div>
<div class="stat-card">
<h4>Reports</h4>
<p>Named: ${reports.named}<br>Associated: ${reports.associated}<br>Logged: ${reports.logged} time(s)</p>
</div>
<div class="stat-card">
<h4>Build</h4>
<p>Pipeline: ${build.pipeline}<br>Build: ${build.buildNumber}<br>Version: ${build.version}</p>
</div>
`;
} catch (err) {
console.error("Failed to load overview stats:", err);
usersEl.innerHTML = `<div class="stat-row"><span>no active users!</span><span class="val"></span></div>`;
activityEl.innerHTML = `<div class="stat-row"><span>no activity!</span><span class="val"></span></div>`;
generalEl.innerHTML = `<div class="stat-card"><h4>no statistics!</h4></div>`;
}
}
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);
+708
View File
@@ -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;
}