Foren waehlbar, Emoji-Tafel repariert, Balken sagt seit wann er misst
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Drei Dinge, die beim Ausprobieren aufgefallen sind.

## Foren standen nicht zur Auswahl

Ein Forum-Kanal ist fuer discord.js nicht "textBased" — er hat keine
Nachrichten, nur Beitraege. Die Kanal-Liste im Panel hat genau danach
gefiltert, also tauchte das Forum nirgends auf, und beim Speichern haette
dieselbe Pruefung es abgelehnt. Beides kennt jetzt Foren und Medien-Kanaele;
in der Auswahl stehen sie mit 📋 statt # und dem Hinweis "Forum".

Das erklaert auch das "Unknown Channel" beim Testen: eingestellt war noch der
alte Kanal, und der neue liess sich gar nicht erst waehlen.

## Emoji-Tafel landete in der Nachbarspalte

Die Config laeuft ab genug Breite zweispaltig — CSS `columns: 2`. In einem
Mehrspalten-Layout zerreisst es absolut positionierte Kaesten: die Tafel wurde
fragmentiert und ihr Inhalt in der anderen Spalte gezeichnet, waehrend beim
Feld nur der leere Rahmen stehen blieb. Genau so sah es auch aus.

Die Tafel haengt jetzt an <body> und wird per JS an den Knopf gerechnet, wie
der Dialog auch. Sie klappt nach oben, wenn unten kein Platz ist, wandert beim
Scrollen mit und bleibt beim Fenstergroesse-Aendern an ihrem Feld.

## Balken sagt jetzt, seit wann er misst

Die Waechter-Balken sahen leer aus. Sind sie nicht: 545 Messpunkte, alle
gruen — aber eben nur von heute, weil die Dienste gestern angelegt wurden.
Sechs von sieben Tagen sind zu Recht grau, das sieht nur aus wie kaputt.

Steht links jetzt "misst seit 01.08." statt stur "vor 7 Tagen", sobald die
erste Messung deutlich spaeter liegt als der Anfang des Fensters. Bei vollem
Verlauf bleibt es bei "vor 7 Tagen".

Geprueft im Browser: Tafel an <body> und `position: fixed`, richtig
positioniert und nach oben geklappt, alle sieben Gruppen und 127 Symbole in
einem Stueck; Forum in der Auswahlliste mit Kennzeichnung; beide
Beschriftungen des Balkens (angebrochener und voller Verlauf).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 23:59:19 +02:00
co-authored by Claude Opus 5
parent cc7a51f02e
commit 1856a1ed83
7 changed files with 81 additions and 14 deletions
+41 -6
View File
@@ -9,7 +9,8 @@
// Suchbegriffe dürfen deutsch sein.
//
// Server-Emojis (<:name:id>) tippt man weiter ins Feld — die kennt nur Discord.
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { IconSearch, IconX } from '../icons.jsx';
const GRUPPEN = [
@@ -75,21 +76,50 @@ const GRUPPEN = [
export default function EmojiPick({ value, onChange, placeholder = '🙂' }) {
const [offen, setOffen] = useState(false);
const [suche, setSuche] = useState('');
const [platz, setPlatz] = useState(null);
const wrap = useRef(null);
const tafel = useRef(null);
// Die Tafel hängt an <body>, nicht am Feld: der Config-Bereich läuft
// stellenweise zweispaltig (CSS columns), und darin zerreißt es absolut
// positionierte Kästen — die Tafel landete dann in der anderen Spalte.
const messen = useCallback(() => {
const r = wrap.current?.getBoundingClientRect();
if (!r) return;
const breite = Math.min(336, window.innerWidth - 24);
const hoehe = 340;
// Nach oben klappen, wenn unten kein Platz ist
const nachOben = r.bottom + hoehe > window.innerHeight && r.top > hoehe;
setPlatz({
left: Math.max(12, Math.min(r.left, window.innerWidth - breite - 12)),
top: nachOben ? Math.max(12, r.top - hoehe - 6) : r.bottom + 6,
breite,
});
}, []);
useLayoutEffect(() => { if (offen) messen(); }, [offen, messen]);
// Klick daneben oder Escape schließt — ohne das bleibt das Feld offen
// stehen, sobald man woanders weiterarbeitet
useEffect(() => {
if (!offen) return undefined;
const aus = (e) => { if (!wrap.current?.contains(e.target)) setOffen(false); };
const aus = (e) => {
if (wrap.current?.contains(e.target) || tafel.current?.contains(e.target)) return;
setOffen(false);
};
const taste = (e) => { if (e.key === 'Escape') setOffen(false); };
document.addEventListener('mousedown', aus);
document.addEventListener('keydown', taste);
window.addEventListener('resize', messen);
// Beim Scrollen mitwandern — capture, damit auch innere Bereiche zählen
window.addEventListener('scroll', messen, true);
return () => {
document.removeEventListener('mousedown', aus);
document.removeEventListener('keydown', taste);
window.removeEventListener('resize', messen);
window.removeEventListener('scroll', messen, true);
};
}, [offen]);
}, [offen, messen]);
const q = suche.trim().toLowerCase();
const gefiltert = GRUPPEN
@@ -127,8 +157,12 @@ export default function EmojiPick({ value, onChange, placeholder = '🙂' }) {
)}
</div>
{offen && (
<div className="ep-tafel">
{offen && platz && createPortal(
<div
className="ep-tafel"
ref={tafel}
style={{ left: platz.left, top: platz.top, width: platz.breite }}
>
<div className="ep-suche">
<IconSearch size={13} />
<input
@@ -160,7 +194,8 @@ export default function EmojiPick({ value, onChange, placeholder = '🙂' }) {
</div>
))}
</div>
</div>
</div>,
document.body
)}
</div>
);
+12 -1
View File
@@ -46,6 +46,17 @@ export default function UptimeTrack({ buckets, modus = 'stunden' }) {
}
const quote = gemessen > 0 ? (erreichbar / gemessen) * 100 : null;
// Wer seit einem Tag misst, hat sechs Siebtel graue Striche — das sieht aus
// wie ein kaputter Balken, ist aber die Wahrheit. Also links nicht stur
// „vor 7 Tagen" behaupten, sondern sagen, ab wann überhaupt gemessen wurde.
const ersteMessung = striche.find((s) => s.b)?.zeit ?? null;
const frueh = striche[0]?.zeit;
const spaeterBegonnen = ersteMessung && frueh
&& ersteMessung.getTime() - frueh.getTime() > m.ms * 2;
const linkeBeschriftung = spaeterBegonnen
? t('server.measuredSince', datum(ersteMessung, { day: '2-digit', month: '2-digit' }))
: t(modus === 'tage' ? 'status.days90' : 'server.days7');
const titel = (s) => {
const wann = modus === 'tage'
? datum(s.zeit, { day: '2-digit', month: '2-digit', year: 'numeric' })
@@ -66,7 +77,7 @@ export default function UptimeTrack({ buckets, modus = 'stunden' }) {
))}
</div>
<div className="up-track-foot">
<span>{modus === 'tage' ? t('status.days90') : t('server.days7')}</span>
<span>{linkeBeschriftung}</span>
{quote != null && (
<span className={quote >= 99 ? 'up-good' : quote >= 90 ? '' : 'up-bad'}>
{quote.toLocaleString(tag, { minimumFractionDigits: 1, maximumFractionDigits: 1 })} %
+1
View File
@@ -158,6 +158,7 @@ export default {
'server.players24h': 'Spielerzahl · letzte 24h',
'server.peak': 'Peak',
'server.uptime': 'Uptime',
'server.measuredSince': 'misst seit %s',
'server.days7': 'vor 7 Tagen',
'server.now': 'jetzt',
'server.noData': 'keine Messung',
+1
View File
@@ -157,6 +157,7 @@ export default {
'server.players24h': 'Player count · last 24h',
'server.peak': 'Peak',
'server.uptime': 'Uptime',
'server.measuredSince': 'measuring since %s',
'server.days7': '7 days ago',
'server.now': 'now',
'server.noData': 'no reading',
+7 -1
View File
@@ -1189,7 +1189,13 @@ export default function Settings({ me }) {
if (error) return <>{header}<section className="content"><p className="notice">Fehler beim Laden ({error}).</p></section></>;
if (!data || !form) return <>{header}<section className="content"><p className="notice">Lade </p></section></>;
const channelItems = data.channels.map((c) => ({ id: c.id, label: `#${c.name}`, hint: c.guild }));
// Foren stehen mit dabei — sichtbar gekennzeichnet, weil dort nur der
// Voting-Kanal sinnvoll ist: posten kann der Bot in ein Forum nicht
const channelItems = data.channels.map((c) => ({
id: c.id,
label: `${c.forum ? '📋' : '#'}${c.name}`,
hint: c.forum ? 'Forum' : c.guild,
}));
const roleItems = (data.roles ?? []).map((r) => ({ id: r.id, label: `@${r.name}`, hint: r.guild }));
// Der Composer bringt seine eigene Auswahl mit und erwartet <option>-Elemente
const channelOptions = channelItems.map((c) => (
+4 -2
View File
@@ -1981,9 +1981,11 @@ label.toggle {
}
.ep-weg:hover { color: #f23f43; }
/* Hängt an <body> und wird per JS positioniert — im zweispaltigen Teil der
Config (CSS columns) zerreißt es absolut positionierte Kästen sonst, und die
Tafel landet in der Nachbarspalte. */
.ep-tafel {
position: absolute; z-index: 60; top: calc(100% + .35rem); left: 0;
width: min(21rem, 78vw);
position: fixed; z-index: 1100;
background: var(--bg2); border: 1px solid var(--border); border-radius: 6px;
box-shadow: 0 12px 30px rgba(0, 0, 0, .55);
animation: riseIn .15s ease both;