DER FEHLER ZUERST, denn er ist meiner von gestern: die Liste der
Meldungsanlaesse stand ZWEIMAL da -- als Standard in meldung.ts und noch einmal
als Weissliste beim Speichern in dash.ts. Beim Einbau des Plattenwaechters habe
ich nur die erste angefasst.
Ergebnis: das Kaestchen "Der Platz auf der Platte wird knapp" liess sich
ankreuzen, der Server warf den Wert beim Speichern weg, und beim naechsten
Laden war es wieder aus. Ohne ein Wort dazu. Nachgemessen am laufenden Dienst:
geschickt ["sicherung","verwaiste","platte"]
gespeichert ["sicherung","verwaiste"]
Die Liste steht jetzt an EINER Stelle (ANLAESSE in meldung.ts) und wird von
beiden benutzt. Gegengeprueft: alle drei kommen an.
SITZUNGEN BEENDEN. Wer sich an einem fremden Rechner angemeldet hat und es
spaeter merkt, hatte keine Moeglichkeit das zurueckzunehmen -- ein geaendertes
Passwort half nicht, die Sitzungen haengen an einer eigenen Tabelle und
ueberleben es. Der Knopf steht im Kontomenue und nur dann, wenn es ueberhaupt
eine zweite Sitzung gibt. Gemessen: 87 offen, 86 beendet, meine lebt.
Dabei in die eigene Falle getreten: der Weg lag zuerst bei den anderen
/auth-Wegen -- und die stehen mit Absicht VOR der Wache, weil man sich anmelden
koennen muss, ohne angemeldet zu sein. Dort ist c.get('user') leer, und der Weg
antwortete mit "Cannot read properties of undefined". Hono setzt Middleware und
Handler in der Reihenfolge ihrer Anmeldung zusammen; der Pfad sagt darueber
nichts.
VERWALTUNGS-VERLAUF. Der Datei-Verlauf beantwortet "was ist mit den Dateien
passiert". Wer den Discord-Webhook geaendert, einen Token angelegt oder ein
Konto entfernt hat, stand nirgends. Bei zwei Konten verschmerzbar -- sobald ein
drittes ueber die Discord-Rolle von selbst entsteht, ist es die erste Frage.
Eigene Tabelle und nicht events: dort traegt jede Zeile path und size, und eine
Einstellungsaenderung muesste path mit etwas fuellen, das kein Pfad ist.
WERTE STEHEN DORT NIE, nur Schluesselnamen. Unter den Einstellungen liegen das
Discord-Geheimnis und das Nextcloud-Passwort; ein Verlauf, der sie mitschreibt,
macht aus einer Tabelle mit einem Passwort eine Tabelle mit allen, die es je
gab.
Beim Einbau derselbe Fehlertyp wie gestern beim SVG: im SQL-Kommentar standen
Backticks, und das Schema liegt in einem Template-Literal -- ein Backtick
beendet es. Der Uebersetzer hat es gefangen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
import { create } from 'zustand'
|
|
import { api, ApiError, type User } from './api'
|
|
|
|
// -- Rückmeldungen -------------------------------------------------------------
|
|
//
|
|
// DIE EINE REGEL: jeder Knopf gibt Rückmeldung, auch der erfolgreiche. Deshalb
|
|
// steht die Meldungsliste hier im Laden und nicht in irgendeiner Seite — sie
|
|
// gehört zu jeder von ihnen.
|
|
|
|
export type NoteKind = 'ok' | 'fehler' | 'hinweis'
|
|
export type Note = { id: number; kind: NoteKind; text: string }
|
|
|
|
let nextNoteId = 1
|
|
|
|
type State = {
|
|
user: User | null
|
|
/** Solange true, weiß noch niemand, ob eine Sitzung besteht — in dieser
|
|
* Zeit darf weder das Anmeldeformular noch das Dashboard aufblitzen. */
|
|
checking: boolean
|
|
notes: Note[]
|
|
|
|
/** Wie viele Sitzungen dieses Konto offen hat. 0 = noch nicht geladen. */
|
|
sitzungen: number
|
|
checkSession: () => Promise<void>
|
|
login: (username: string, password: string) => Promise<void>
|
|
logout: () => Promise<void>
|
|
|
|
note: (kind: NoteKind, text: string) => void
|
|
dismiss: (id: number) => void
|
|
}
|
|
|
|
export const useStore = create<State>((set, get) => ({
|
|
user: null,
|
|
checking: true,
|
|
sitzungen: 0,
|
|
notes: [],
|
|
|
|
async checkSession() {
|
|
try {
|
|
const { user, sitzungen } = await api.me()
|
|
set({ user, sitzungen: sitzungen ?? 0, checking: false })
|
|
} catch (err) {
|
|
// 401 ist hier kein Fehler, sondern die Antwort "noch nicht
|
|
// angemeldet". Alles andere ist einer und wird gesagt.
|
|
if (err instanceof ApiError && err.status === 401) {
|
|
set({ user: null, checking: false })
|
|
return
|
|
}
|
|
set({ user: null, checking: false })
|
|
get().note('fehler', err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
}
|
|
},
|
|
|
|
async login(username, password) {
|
|
const { user } = await api.login(username, password)
|
|
set({ user })
|
|
get().note('ok', `Angemeldet als ${user.username}.`)
|
|
},
|
|
|
|
async logout() {
|
|
await api.logout()
|
|
set({ user: null })
|
|
// Die Erfolgsmeldung kommt hier NICHT: der Aufrufer geht über `run`,
|
|
// und der meldet. Beides hieße zwei Zettel für einen Klick.
|
|
},
|
|
|
|
note(kind, text) {
|
|
const id = nextNoteId++
|
|
set((s) => ({ notes: [...s.notes, { id, kind, text }] }))
|
|
// Fehler bleiben stehen, bis jemand sie wegklickt. Ein Fehler, der von
|
|
// allein verschwindet, ist ein Fehler, den niemand gelesen hat.
|
|
if (kind !== 'fehler') {
|
|
setTimeout(() => get().dismiss(id), 4000)
|
|
}
|
|
},
|
|
|
|
dismiss(id) {
|
|
set((s) => ({ notes: s.notes.filter((n) => n.id !== id) }))
|
|
},
|
|
}))
|
|
|
|
/** Eine Aktion ausführen und IMMER etwas dazu sagen.
|
|
*
|
|
* Der Erfolgstext wird mitgegeben, der Fehlertext kommt vom Dienst. Wer
|
|
* darüber geht, kann keinen stillen Knopf bauen — und genau darum geht es. */
|
|
export async function run<T>(
|
|
action: () => Promise<T>,
|
|
erfolg: string | ((result: T) => string),
|
|
): Promise<T | null> {
|
|
const { note } = useStore.getState()
|
|
try {
|
|
const result = await action()
|
|
note('ok', typeof erfolg === 'function' ? erfolg(result) : erfolg)
|
|
return result
|
|
} catch (err) {
|
|
note('fehler', err instanceof Error ? err.message : 'Unbekannter Fehler')
|
|
// Eine abgelaufene Sitzung ist kein gewöhnlicher Fehler: sie führt
|
|
// zurück aufs Anmeldeformular, statt jeden weiteren Knopf mit
|
|
// "nicht angemeldet" zu quittieren.
|
|
if (err instanceof ApiError && err.status === 401) {
|
|
useStore.setState({ user: null })
|
|
}
|
|
return null
|
|
}
|
|
}
|