Der Waechter prueft alles ausser sich selbst — ist der Bot weg, prueft niemand mehr, und hinterher sieht der Verlauf aus, als waere nie etwas gewesen. Im Dashboard stand deshalb nur "seit 12d 4h", also wie lange es diesmal gutgegangen ist. Was davor war, wusste niemand. Jetzt schreibt der Bot alle zwei Minuten eine Zeile: laeuft der Prozess, und steht die Verbindung zu Discord? Ein Bot, der laeuft aber nicht verbunden ist, ist fuer alle draussen genauso weg — deshalb zaehlt beides. Der Takt ist absichtlich nicht einstellbar. Ein spaeter geaenderter Abstand wuerde den alten Verlauf falsch aussehen lassen: wie viele Proben eine Stunde haette haben muessen, liesse sich rueckwirkend nicht mehr sagen. Ausgewertet wird vor allem die Abwesenheit. Fuer einen fremden Dienst heisst eine fehlende Stunde "niemand hat gemessen" und wird grau; beim Bot selbst ist genau das die Aussage — fehlende Stunden werden rot. Grau bleibt nur, was vor der allerersten Aufzeichnung liegt. Die angebrochene erste und die laufende letzte Stunde rechnen anteilig, sonst waeren sie dauerhaft gelb. Nebenbei mitgenommen: - Statusseite und die frei angelegten Seiten stehen jetzt in der Fusszeile beider Seiten — dort sucht man Rechtliches. Die Navigationsleiste zeigt weiterhin nur, was "im Menü" gesetzt hat; wer die Nutzungsbedingungen dort raus nimmt, hat sie trotzdem noch in der Fusszeile. - Die Config-Spalte hatte keine Obergrenze. min-width:0 allein reicht nicht, wenn ein Kind eine grosse Mindestbreite mitbringt — mit dem Balken wuchs sie auf 724 statt 335 px, und weil der Body waagerecht abschneidet, war der Ueberstand nicht scrollbar, sondern weg. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
124 lines
5.8 KiB
React
124 lines
5.8 KiB
React
// Rahmen der Bot-Produktseite — eigene Navigation, orange Leitfarbe.
|
|
// Das Dashboard ist dieselbe Config-Seite wie bisher, nur unter /dashboard.
|
|
import { useEffect, useState } from 'react';
|
|
import { Routes, Route, NavLink, Navigate, useLocation } from 'react-router-dom';
|
|
import { apiGet } from './api.js';
|
|
import BotLanding from './pages/BotLanding.jsx';
|
|
import BotFeatures from './pages/BotFeatures.jsx';
|
|
import BotCommands from './pages/BotCommands.jsx';
|
|
import Settings from './pages/Settings.jsx';
|
|
import Impressum from './pages/Impressum.jsx';
|
|
import Datenschutz from './pages/Datenschutz.jsx';
|
|
import { IconMenu, IconX } from './icons.jsx';
|
|
import { useT, LangSwitch } from './i18n.jsx';
|
|
|
|
export default function BotApp() {
|
|
const [me, setMe] = useState({ user: null, admin: false, scopes: [], loading: true });
|
|
const [scrolled, setScrolled] = useState(false);
|
|
const [drawer, setDrawer] = useState(false);
|
|
const [hub, setHub] = useState(null);
|
|
// Seiten wie die Nutzungsbedingungen liegen auf dem Hub — verlinkt
|
|
// wird deshalb absolut, und nur wenn es einen Hub gibt.
|
|
const [pages, setPages] = useState([]);
|
|
const location = useLocation();
|
|
const { t } = useT();
|
|
|
|
useEffect(() => { setDrawer(false); }, [location.pathname]);
|
|
|
|
useEffect(() => {
|
|
apiGet('/api/me')
|
|
.then((data) => setMe({ scopes: [], ...data, loading: false }))
|
|
.catch(() => setMe({ user: null, admin: false, scopes: [], loading: false }));
|
|
apiGet('/api/legal').then((d) => setHub(d.split ? d.hubUrl : null)).catch(() => {});
|
|
apiGet('/api/pages').then((d) => setPages(d.pages)).catch(() => {});
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const onScroll = () => setScrolled(window.scrollY > 24);
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
return () => window.removeEventListener('scroll', onScroll);
|
|
}, []);
|
|
|
|
const canDashboard = me.admin || me.scopes?.length > 0;
|
|
|
|
return (
|
|
<div className="app site-bot">
|
|
<header className={`nav ${scrolled ? 'scrolled' : ''}`}>
|
|
<NavLink to="/" className="nav-logo">
|
|
D4RKBOT<span>.</span>
|
|
</NavLink>
|
|
|
|
<nav className="nav-links">
|
|
<NavLink to="/features">{t('nav.features')}</NavLink>
|
|
<NavLink to="/commands">{t('nav.commands')}</NavLink>
|
|
{canDashboard && <NavLink to="/dashboard">{t('nav.dashboard')}</NavLink>}
|
|
{hub && <a href={hub}>{t('nav.community')}</a>}
|
|
</nav>
|
|
|
|
<div className="auth">
|
|
<LangSwitch />
|
|
{me.loading ? null : me.user ? (
|
|
<span className="user-chip">
|
|
{me.user.avatar && <img className="avatar" src={me.user.avatar} alt="" />}
|
|
<span className="username">{me.user.username}</span>
|
|
<a className="logout" href="/auth/logout" title={t('nav.signOut')}>
|
|
<IconX size={13} />
|
|
</a>
|
|
</span>
|
|
) : (
|
|
<a className="btn-login" href="/auth/login">{t('nav.signIn')}</a>
|
|
)}
|
|
</div>
|
|
|
|
<button className="burger" onClick={() => setDrawer((d) => !d)} aria-label={t('nav.menu')}>
|
|
{drawer ? <IconX size={20} /> : <IconMenu size={20} />}
|
|
</button>
|
|
</header>
|
|
|
|
{drawer && (
|
|
<nav className="drawer" onClick={() => setDrawer(false)}>
|
|
<NavLink to="/features">{t('nav.features')}</NavLink>
|
|
<NavLink to="/commands">{t('nav.commands')}</NavLink>
|
|
{canDashboard && <NavLink to="/dashboard">{t('nav.dashboard')}</NavLink>}
|
|
{hub && <a href={hub}>{t('nav.community')}</a>}
|
|
{me.user
|
|
? <a href="/auth/logout">{t('nav.signOut')}</a>
|
|
: <a href="/auth/login">{t('nav.signIn')}</a>}
|
|
{/* Auf schmalen Schirmen ist .auth ausgeblendet — der
|
|
Sprachwechsel muss trotzdem erreichbar bleiben. */}
|
|
<div className="drawer-lang" onClick={(e) => e.stopPropagation()}>
|
|
<LangSwitch />
|
|
</div>
|
|
</nav>
|
|
)}
|
|
|
|
<main className="main">
|
|
<Routes>
|
|
<Route path="/" element={<BotLanding />} />
|
|
<Route path="/features" element={<BotFeatures />} />
|
|
<Route path="/commands" element={<BotCommands />} />
|
|
<Route path="/dashboard" element={<Settings me={me} />} />
|
|
<Route path="/impressum" element={<Impressum />} />
|
|
<Route path="/datenschutz" element={<Datenschutz />} />
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</main>
|
|
|
|
<footer className="footer">
|
|
<span className="footer-logo">D4RKBOT<span>.</span></span>
|
|
<span className="footer-sep">//</span>
|
|
<a href="https://git.d4rkst3r.de/D4rkst3r/d4rkbot" target="_blank" rel="noreferrer">
|
|
{t('footer.source')}
|
|
</a>
|
|
<span className="footer-sep">//</span>
|
|
{hub && <a href={`${hub}/status`}>{t('footer.status')}</a>}
|
|
<NavLink to="/impressum">{t('footer.imprint')}</NavLink>
|
|
<NavLink to="/datenschutz">{t('footer.privacy')}</NavLink>
|
|
{hub && pages.map((p) => (
|
|
<a href={`${hub}/${p.slug}`} key={p.slug}>{p.title}</a>
|
|
))}
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|