einstellungen: Zeitfenster, Radar-Schalter, Telegram-Test, Software-Stand und Altreste
Einstellungen, die etwas einstellen (services/einstellungen.py, /api/einstellungen): - Update-Zeitfenster: Wochentag und Uhrzeit des Update-Laufs als Drop-in mc2-autoupdate.timer.d/zeitfenster.conf. Gegen die Nachhol-Falle von Persistent=true (24.09.): Timer vor dem daemon-reload stoppen, Stempel auf jetzt, dann starten. Bei Fehler zurück auf das alte Drop-in. Während eines Update-Laufs abgelehnt. - Neustart nur im eingestellten Fenster (deploy/wartungsfenster.sh, drei Stunden ab der vollen Stunde des Beginns; Standard So 04:00-06:59). - Radar an/aus: disable --now bzw. enable + Stempel + start (kein Nachholen). deploy.sh respektiert den Schalter und startet den Radar-Timer nur mit Stempel; das Zeitfenster-Drop-in fasst er nie an. - Wächter: schläft der Timer eines Timer-Dienstes, ist ein alter Fehlschlag kein Befund mehr (der Dienst selbst ist static und schläft nie). - Telegram-Test je Instanz (/api/einstellungen/telegram-test und /api/homelab/einstellungen/telegram-test) über notify.sh -d; Ergebnis und Grund aus dem Melde-Log. notify.sh schreibt jetzt auch den Grund des gescheiterten Zweitwegs in den FALLBACK-Eintrag. - Einstellungsdatei mc2-einstellungen.json im Datenordner. Software-Stand: deploy.sh und homelab/ausrollen.sh schreiben nach dem Umschalten mc2-stand.json (deploy/stand-schreiben.py); /api/instanz liefert den Stand, die Einstellungen zeigen beide Instanzen und warnen bei Abweichung. Ohne Datei aus git, sonst unbekannt. Aufräumen: Altreste neben dem Betrieb aus einer festen, auf der Box geprüften Liste (Größe, Hinweis, nur was da und nicht in Gebrauch ist). Löschen nur per Kennung und Klick mit Rückfrage; Worktrees mit git worktree prune, Alt-Units mit daemon-reload, /opt/llamacpp notfalls mit sudo -n. Tests: pytest mit einem systemd-Abbild samt Nachhol-Regel, echtem notify.sh (Ersatz-Hermes, Tmp-Zugang, lokale Bot-API), Shell-Funktionen des Wartungsfensters und von deploy.sh; vitest für die Rückfragen der Schublade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
40d2840b16
commit
3d8df1c6fd
@@ -0,0 +1,116 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react"
|
||||
import { api, post } from "@/lib/api"
|
||||
import type { EinstellungenStand, InstanzMitStand } from "@/lib/typen"
|
||||
import { Einstellungen } from "./Einstellungen"
|
||||
|
||||
vi.mock("@/lib/api", async (original) => ({
|
||||
...(await original<typeof import("@/lib/api")>()),
|
||||
api: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}))
|
||||
|
||||
const BOX: EinstellungenStand = {
|
||||
schaltbar: true,
|
||||
update_fenster: {
|
||||
tag: 7, uhrzeit: "04:30", tag_name: "Sonntag", neustart_fenster: "Sonntag 04:00–06:59", timer_aktiv: true,
|
||||
naechster_lauf: "2026-09-27T04:36:00+02:00", wirksam: true,
|
||||
},
|
||||
radar: { an: true, zustand: "an", naechster_lauf: "2026-09-25T00:30:00+02:00", wirksam: true },
|
||||
}
|
||||
|
||||
function instanz(rolle: "box" | "homelab", commit: string): InstanzMitStand {
|
||||
return {
|
||||
rolle, instanz: rolle === "box" ? "Box-Wart" : "Homelab",
|
||||
partner: { eingerichtet: true, name: rolle === "box" ? "Homelab" : "Box-Wart" },
|
||||
stand: {
|
||||
quelle: "deploy", commit, betreff: "doku: Phase 4", commit_datum: "2026-09-24T20:46:03+02:00",
|
||||
deploy_zeit: "2026-09-24T21:10:00+02:00",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function antworten(partnerCommit = "40d2840") {
|
||||
const daten: Record<string, unknown> = {
|
||||
"/api/maintenance/geheimnisse": { hf_token_gesetzt: false, hf_token_aus_env: false, schreibbar: true },
|
||||
"/api/einstellungen": BOX,
|
||||
"/api/instanz": instanz("box", "40d2840"),
|
||||
"/api/partner/instanz": instanz("homelab", partnerCommit),
|
||||
}
|
||||
vi.mocked(api).mockImplementation((async (pfad: string) => {
|
||||
if (pfad in daten) return daten[pfad]
|
||||
throw new Error(`unerwartete Abfrage ${pfad}`)
|
||||
}) as typeof api)
|
||||
}
|
||||
|
||||
function zeigen() {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||
render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<Einstellungen offen onSchliessen={() => {}} />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe("Einstellungen", () => {
|
||||
it("schaltet das Radar erst nach der Rückfrage aus", async () => {
|
||||
antworten()
|
||||
vi.mocked(post).mockResolvedValue({ ok: true, text: "Das Radar ist aus und schläft." })
|
||||
zeigen()
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Ausschalten" }))
|
||||
const dialog = await screen.findByRole("dialog", { name: "Modell-Radar ausschalten?" })
|
||||
expect(dialog).toHaveTextContent("Auch ein Deploy schaltet es nicht wieder ein.")
|
||||
expect(post).not.toHaveBeenCalled()
|
||||
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Ausschalten" }))
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith("/api/einstellungen/radar", { an: false }))
|
||||
})
|
||||
|
||||
it("nennt vor dem Speichern des Zeitfensters das neue Neustart-Fenster", async () => {
|
||||
antworten()
|
||||
vi.mocked(post).mockResolvedValue({ ok: true, text: "Gespeichert." })
|
||||
zeigen()
|
||||
const bereich = await screen.findByRole("region", { name: "Update-Zeitfenster" })
|
||||
const speichern = await within(bereich).findByRole("button", { name: "Speichern" })
|
||||
expect(speichern).toBeDisabled() // nichts geändert, der Timer passt
|
||||
fireEvent.change(within(bereich).getByRole("combobox", { name: "Wochentag" }), { target: { value: "6" } })
|
||||
fireEvent.change(within(bereich).getByRole("combobox", { name: "Uhrzeit" }), { target: { value: "23:30" } })
|
||||
fireEvent.click(speichern)
|
||||
|
||||
const dialog = await screen.findByRole("dialog", { name: "Update-Zeitfenster ändern?" })
|
||||
expect(dialog).toHaveTextContent("jeden Samstag um 23:30 Uhr")
|
||||
expect(dialog).toHaveTextContent("Samstag 23:00 – Sonntag 01:59")
|
||||
expect(post).not.toHaveBeenCalled()
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "Speichern" }))
|
||||
await waitFor(() =>
|
||||
expect(post).toHaveBeenCalledWith("/api/einstellungen/update-fenster", { tag: 6, uhrzeit: "23:30" }))
|
||||
})
|
||||
|
||||
it("zeigt beim Telegram-Test des Homelab-Teils den Grund aus dem Melde-Log", async () => {
|
||||
antworten()
|
||||
vi.mocked(post).mockResolvedValue({
|
||||
ok: false, gesendet: false, weg: null, grund: "Zweitweg: Telegram lehnt ab, HTTP-Fehler",
|
||||
detail: "Die Testmeldung ging nicht raus.", instanz: "Homelab", zeit: "2026-09-24T21:14:00+02:00",
|
||||
})
|
||||
zeigen()
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Testmeldung von Homelab senden" }))
|
||||
await waitFor(() => expect(post).toHaveBeenCalledWith("/api/homelab/einstellungen/telegram-test"))
|
||||
expect(await screen.findByText("Nicht gesendet: Zweitweg: Telegram lehnt ab, HTTP-Fehler")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("zeigt beide Software-Stände und warnt, wenn sie abweichen", async () => {
|
||||
antworten("37133fd")
|
||||
zeigen()
|
||||
expect(await screen.findByText(/laufen mit verschiedenen Ständen/)).toBeInTheDocument()
|
||||
expect(screen.getByText("40d2840")).toBeInTheDocument()
|
||||
expect(screen.getByText("37133fd")).toBeInTheDocument()
|
||||
expect(screen.getAllByText(/Live seit 24\.09\.2026, 21:10/)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("schweigt bei gleichem Stand, auch wenn ein Hash länger ist", async () => {
|
||||
antworten("40d2840a")
|
||||
zeigen()
|
||||
expect(await screen.findByText("40d2840a")).toBeInTheDocument()
|
||||
expect(screen.queryByText(/laufen mit verschiedenen Ständen/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user