From fb55377e19ff5fc0d9632855beefd4569b973010 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Fri, 18 Sep 2026 16:08:15 +0200 Subject: [PATCH] Videos: Stufe 5 -- fuehrt der Knopf noch irgendwohin? Ein Video kann bei TikTok geloescht oder auf privat gestellt werden. Bei uns blieb der Eintrag stehen -- mit Cover, Text und einem Knopf auf eine Fehlerseite. Das faellt beim Bauen nicht auf und beim Testen nicht; es faellt dem auf, der klickt, und das ist die Community. DREI ANTWORTEN, NICHT ZWEI. Die naheliegende Fassung fragt TikTok und setzt bei einem Fehlschlag "weg". Die waere an einem einzigen schlechten Nachmittag von TikTok in der Lage, die halbe Galerie als geloescht zu markieren -- und wer das hinterher sucht, sucht lange, denn im Protokoll stuende sauber, dass gefragt wurde. Also: Daten kommen an -> da. Zaehler zurueck, eine frueher gesetzte Markierung faellt weg (privat gestellte Videos kommen wieder). "gibt es nicht" (404) -> ein Fehlversuch. Erst der DRITTE an drei verschiedenen Tagen markiert. niemand antwortet -> weiss nicht. Aendert gar nichts, nicht einmal den Zeitstempel: Wer ihn setzte, verschoebe die naechste Nachfrage um sieben Tage, obwohl nichts gemessen wurde. Drei Geschwindigkeiten beim Nachsehen: unauffaellig woechentlich, verdaechtig taeglich (sonst dauerte eine Loeschung drei Wochen bis zur Anzeige), schon markiert wieder woechentlich -- taeglich nachzufragen waere Hammern fuer eine Auskunft, die wir schon haben. Gefunden hat das eine rote Pruefung, nicht das Nachdenken davor. Der Knopf verschwindet nicht, er sagt "Bei TikTok nicht mehr da". Bewusst kein Rot: Es ist kein Fehler, sondern eine Auskunft -- Titel, Text und Cover stimmen weiter, die liegen bei uns. Geprueft: pruef-video 37 -> 51. Gegenprobe (Stoerung als "weg" werten plus Markieren ab dem ersten Versuch) macht 8 rot, darunter woertlich "eine Stoerung erhoeht den Zaehler NICHT (0 -> 1)". Co-Authored-By: Claude Opus 5 --- server/index.js | 6 +- server/pruef-video.mjs | 118 ++++++++++++++++++++ server/workspace-bereiche.js | 2 +- server/workspace-video.js | 183 +++++++++++++++++++++++++++++++ server/workspace.js | 16 ++- workspace/app.webmanifest | 8 +- workspace/assets/css/bereich.css | 19 ++++ workspace/assets/js/bereich.js | 28 ++++- workspace/aufgaben.html | 24 ++-- workspace/automation.html | 26 ++--- workspace/befinden.html | 24 ++-- workspace/bereich.html | 36 +++--- workspace/bewerben.html | 30 ++--- workspace/bewerbungen.html | 28 ++--- workspace/calls.html | 28 ++--- workspace/chat.html | 28 ++--- workspace/content.html | 30 ++--- workspace/crew-index.html | 10 +- workspace/crew.webmanifest | 8 +- workspace/dateien.html | 26 ++--- workspace/entwicklung.html | 24 ++-- workspace/index.html | 8 +- workspace/kalender.html | 28 ++--- workspace/leistung.html | 26 ++--- workspace/personen.html | 26 ++--- workspace/profil.html | 28 ++--- workspace/rechte.html | 24 ++-- workspace/report.html | 28 ++--- workspace/scouting.html | 26 ++--- workspace/start.html | 24 ++-- workspace/startcheck.html | 28 ++--- workspace/steckbrief.html | 28 ++--- workspace/talente.html | 26 ++--- workspace/team.html | 26 ++--- workspace/teamlage.html | 24 ++-- workspace/teilen.html | 22 ++-- workspace/treff-moderation.html | 24 ++-- workspace/treff-regeln.html | 26 ++--- workspace/uebersicht.html | 26 ++--- workspace/wissen.html | 26 ++--- 40 files changed, 766 insertions(+), 410 deletions(-) diff --git a/server/index.js b/server/index.js index 63f9af33..5a719d54 100644 --- a/server/index.js +++ b/server/index.js @@ -54,7 +54,7 @@ import { teamlageRouter } from "./workspace-teamlage.js"; import { pushRouter, pushStarten } from "./workspace-push.js"; import { chatRouter } from "./workspace-chat.js"; import { anrufRouter } from "./workspace-anruf.js"; -import { videoRouter } from "./workspace-video.js"; +import { videoRouter, videoWacheStarten } from "./workspace-video.js"; import { bewerbungRouter } from "./workspace-bewerbung.js"; import { leistungRouter } from "./workspace-leistung.js"; import { fruehwarnungRouter } from "./workspace-fruehwarnung.js"; @@ -309,6 +309,10 @@ app.use(contentRouter); Systemdienst laeuft und warum eine Dateikopie hier falsch waere. */ sicherungStarten(); aufbewahrungStarten(); +/* Sieht taeglich nach, ob die verlinkten Videos bei TikTok noch da + sind. Ein Knopf, der ins Leere fuehrt, ist schlimmer als kein + Knopf -- man klickt ihn ja. */ +videoWacheStarten(); /* Wiederkehrende Termine einmal auffuellen. Kein Takt, kein Dienst -- siehe workspace-serien.js: Der Nachfueller legt nur an, was fehlt, und diff --git a/server/pruef-video.mjs b/server/pruef-video.mjs index f6044b4b..23007ff9 100644 --- a/server/pruef-video.mjs +++ b/server/pruef-video.mjs @@ -63,6 +63,10 @@ const PNG = Buffer.from( "base64"); let coverAbrufe = 0; let oembedAbrufe = 0; +/* Welche Videos gerade „geloescht" sind und welche „stoeren". + Zwei getrennte Mengen, weil es zwei getrennte Faelle sind. */ +const verschwunden = new Set(); +const stoerung = new Set(); const coverAdresse = (was) => `http://127.0.0.1:${TIKTOK}/${was}` + `?x-expires=${Math.floor(Date.now() / 1000) + 172800}&x-signature=abc`; @@ -75,6 +79,11 @@ const tiktok = createServer((q, a) => { Dienst ist es genauso. */ const wer = (ziel.match(/@([^/?]+)/) || [])[1] || ""; if (!wer) { a.writeHead(404); a.end("{}"); return; } + /* UMSCHALTBAR FUER DIE WACHE (Abschnitt 5b): Ein Video kann + verschwinden und wiederkommen, und TikTok kann stoeren. Beides + sind verschiedene Dinge, und genau das muss messbar sein. */ + if (verschwunden.has(ziel)) { a.writeHead(404); a.end("{}"); return; } + if (stoerung.has(ziel)) { a.writeHead(503); a.end("{}"); return; } /* Welches Cover? Am Videonamen. So lassen sich die Sonderfälle auslösen, ohne den Dienst umzubauen. */ let bild = coverAdresse("cover.jpg"); @@ -303,6 +312,115 @@ melde("=== 5. Die Byteprüfung ==="); ok(b.status === 201, `ein Video ohne Coverbild geht auch (${b.status})`); } +/* ======================================================================= + 5b. FUEHRT DER KNOPF NOCH IRGENDWOHIN? (Stufe 5) + + Der wichtigste Satz dieses Abschnitts ist nicht „geloeschte Videos + werden erkannt", sondern: EINE STOERUNG BEI TIKTOK AENDERT NICHTS. + Die naheliegende Fassung setzt bei jedem misslungenen Abruf „weg" -- + und markiert an einem schlechten Nachmittag die halbe Galerie als + geloescht. Wer das hinterher sucht, sucht lange, denn im Protokoll + steht dann sauber, dass TikTok gefragt wurde. + ======================================================================= */ +melde(""); +melde("=== 5b. Wenn ein Video bei TikTok verschwindet ==="); +{ + const video = await import("./workspace-video.js"); + const db2 = new DatabaseSync(process.env.WORKSPACE_DB); + const nimm = (id) => db2.prepare( + "SELECT quelle_geprueft_am, quelle_fehlt_seit," + + " COALESCE(quelle_fehlversuche,0) AS fehl FROM eintraege WHERE id = ?").get(id); + /* Stellt den naechsten Tag dar. Die Wache sieht verdaechtige Videos + TAEGLICH nach -- ohne Zurueckdatieren waeren alle drei Laeufe am + selben Tag, und nur der erste taete etwas. Genau so ist es + gewollt; die Pruefung muss deshalb die Uhr weiterstellen, nicht + die Regel umgehen. */ + const morgen = (id, stunden = 36) => db2.prepare( + "UPDATE eintraege SET quelle_geprueft_am = ? WHERE id = ?") + .run(new Date(Date.now() - stunden * 3600_000).toISOString(), id); + + /* Derselbe Account wie in Abschnitt 1 -- ein ERFUNDENER wird von der + Route zu Recht abgewiesen, und die Pruefung haette dann gemessen, + dass sie sich selbst vertippt hat. */ + const adresse = videoVon("dogfather0804", "5b1"); + const ein = await holen(adresse); + const id5b = ein.daten?.id; + ok(ein.status === 201 && !!id5b, `ein Video zum Beobachten ist da (#${id5b})`); + + /* ---- Die drei Antworten einzeln ---------------------------------- */ + ok((await video.videoNochDa(adresse)).da === true, + "ein vorhandenes Video meldet: da"); + stoerung.add(adresse); + const beiStoerung = await video.videoNochDa(adresse); + ok(beiStoerung.da === null, + `eine Stoerung meldet WEISS NICHT, nicht „weg" (${beiStoerung.grund})`); + stoerung.delete(adresse); + verschwunden.add(adresse); + const beiWeg = await video.videoNochDa(adresse); + ok(beiWeg.da === false, `ein geloeschtes meldet: weg (${beiWeg.grund})`); + + /* ---- Ein einzelner Fehlversuch markiert NICHT --------------------- */ + const l1 = await video.videoWacheLauf(); + const n1 = nimm(id5b); + ok(n1.fehl === 1 && !n1.quelle_fehlt_seit, + `nach dem ersten Fehlversuch ist es noch NICHT „weg" (Zaehler ${n1.fehl})`); + + morgen(id5b); + await video.videoWacheLauf(); + const n2 = nimm(id5b); + ok(n2.fehl === 2 && !n2.quelle_fehlt_seit, + `nach dem zweiten auch nicht (Zaehler ${n2.fehl})`); + + morgen(id5b); + const l3 = await video.videoWacheLauf(); + const n3 = nimm(id5b); + ok(n3.fehl === 3 && !!n3.quelle_fehlt_seit, + `erst der dritte markiert es (Zaehler ${n3.fehl}, seit ${String(n3.quelle_fehlt_seit).slice(0, 10)})`); + ok(l3.weg === 1, `und der Lauf sagt es auch (weg: ${l3.weg})`); + + /* ---- Die Oberflaeche erfaehrt davon ------------------------------- */ + const e5b = (await liste()).find((x) => x.id === id5b); + ok(!!e5b?.quelle_fehlt_seit, + "die Liste liefert den Zustand mit -- sonst zeigte die Kachel weiter den Knopf"); + + /* ---- DER KERN: eine Stoerung aendert NICHTS ----------------------- */ + verschwunden.delete(adresse); + /* ACHT TAGE, nicht 36 Stunden: Ein bereits markiertes Video wird + absichtlich nur noch woechentlich nachgefragt (siehe die drei + Geschwindigkeiten in workspace-video.js). Die Pruefung muss die + echte Regel abbilden, nicht die bequemere. */ + morgen(id5b, 8 * 24); + await video.videoWacheLauf(); /* wieder da -> Zaehler zurueck */ + const zurueck = nimm(id5b); + ok(zurueck.fehl === 0 && !zurueck.quelle_fehlt_seit, + "kommt es zurueck (privat gestellt und wieder oeffentlich), faellt die Markierung weg"); + + /* Ebenfalls acht Tage: Nach der Rueckkehr steht der Zaehler auf + null, das Video gilt also wieder als unauffaellig -- und + unauffaellige werden nur woechentlich gefragt. */ + morgen(id5b, 8 * 24); + stoerung.add(adresse); + const vorher5b = nimm(id5b); + const lS = await video.videoWacheLauf(); + const nachher5b = nimm(id5b); + stoerung.delete(adresse); + ok(nachher5b.fehl === vorher5b.fehl && !nachher5b.quelle_fehlt_seit, + `eine Stoerung erhoeht den Zaehler NICHT (${vorher5b.fehl} -> ${nachher5b.fehl})`); + ok(nachher5b.quelle_geprueft_am === vorher5b.quelle_geprueft_am, + "und verschiebt auch den Zeitpunkt nicht -- sonst waere sieben Tage Ruhe," + + " obwohl nichts gemessen wurde"); + ok(lS.unklar >= 1 && lS.geprueft === 0, + `der Lauf zaehlt sie als „weiss nicht" (unklar ${lS.unklar}, geprueft ${lS.geprueft})`); + + /* ---- Und ein gesundes Video bleibt unberuehrt --------------------- */ + const heil = (await liste()).find((x) => x.id === eintragId); + ok(!heil?.quelle_fehlt_seit, + "ein Video, das es noch gibt, wird nicht angefasst"); + + db2.close(); + void l1; +} + /* ======================================================================= 6. UND ES ÜBERLEBT, WENN TIKTOK WEG IST ← der eigentliche Punkt ======================================================================= */ diff --git a/server/workspace-bereiche.js b/server/workspace-bereiche.js index 201e1d16..472436e4 100644 --- a/server/workspace-bereiche.js +++ b/server/workspace-bereiche.js @@ -1116,7 +1116,7 @@ function sichtbarRoh2(person, praefix = "e") { const SPALTEN = ` e.id, e.bereich, e.art, e.titel, e.text, e.datum, e.uhrzeit, e.aus_eintrag_id, - e.quelle_url, e.quelle_kanal, e.bewertung, + e.quelle_url, e.quelle_kanal, e.quelle_fehlt_seit, e.bewertung, e.dringlichkeit, e.status, e.creator_id, e.erstellt, e.erstellt_von, e.geaendert, e.hook, e.format, e.saeule_id, e.geplant, e.event_ende, e.event_aufgaben, e.event_regeln, diff --git a/server/workspace-video.js b/server/workspace-video.js index 5e44e796..d3ec8179 100644 --- a/server/workspace-video.js +++ b/server/workspace-video.js @@ -230,3 +230,186 @@ videoRouter.post("/workspace/api/bereich/:bereich/video", async (req, res) => { res.status(503).json({ fehler: "nicht_verfuegbar" }); } }); + + +/* ===================================================================== + STUFE 5: FUEHRT DER KNOPF NOCH IRGENDWOHIN? (18.09.2026) + + Ein Video kann bei TikTok geloescht oder auf privat gestellt werden. + Bei uns bleibt der Eintrag stehen -- Cover, Text, und ein Knopf „Auf + TikTok ansehen", der auf eine Fehlerseite fuehrt. Das faellt beim + Bauen nicht auf und beim Testen nicht; es faellt dem auf, der + klickt, und das ist die Community. + + --------------------------------------------------------------------- + DREI ANTWORTEN, NICHT ZWEI + + Die naheliegende Fassung fragt TikTok und setzt bei einem Fehlschlag + „weg". Die waere an einem einzigen schlechten Nachmittag von TikTok + in der Lage, die halbe Galerie als geloescht zu markieren -- und + niemand haette einen Anhaltspunkt, warum ploetzlich alles fort ist. + + Deshalb wird unterschieden: + + TikTok schickt Daten -> DA. Zaehler zurueck auf null, eine + frueher gesetzte Markierung faellt + weg (ein privat gestelltes Video + kann wiederkommen). + TikTok sagt „gibt es nicht" -> ein FEHLVERSUCH. Erst der dritte an + drei verschiedenen Tagen markiert. + Niemand antwortet / 5xx -> WEISS NICHT. Aendert gar nichts, + nicht einmal den Zaehler. + + Der dritte Fall ist der wichtige: Unerreichbar ist kein Befund. + ===================================================================== */ + +/** Ab wie vielen Fehlversuchen ein Video als „nicht mehr da" gilt. */ +const FEHLT_AB = 3; + +/** Wie viele je Lauf. Nicht alle auf einmal: Ein Schwall Anfragen an + * einen fremden Dienst ist das, was man von einem Programm erwartet, + * das man nicht mehr bedienen will. */ +const WACHE_JE_LAUF = 40; + +/** Abstand zwischen zwei Abfragen. */ +const WACHE_PAUSE_MS = 300; + +/** Wie oft ein unauffaelliges Video nachgesehen wird (Tage). + * + * DREI GESCHWINDIGKEITEN, und jede hat ihren Grund: + * + * unauffaellig alle sieben Tage -- es gibt keinen Anlass. + * verdaechtig taeglich. Sonst dauerte es drei Wochen, bis + * (1-2 Fehlversuche) ein geloeschtes Video als geloescht dasteht. + * schon markiert wieder alle sieben Tage. Die Frage ist + * beantwortet; taeglich nachzufragen waere + * Hammern auf einem fremden Dienst fuer eine + * Auskunft, die wir schon haben. Dass eine + * Rueckkehr dann bis zu eine Woche braucht, ist + * der richtige Preis. */ +const WACHE_ABSTAND_TAGE = 7; + +/** Fragt nach, ob es das Video noch gibt. + * + * @returns {Promise<{da: boolean|null, grund: string}>} `da === null` + * heisst ausdruecklich „konnte nicht nachsehen" -- nicht „weg". + */ +export async function videoNochDa(url) { + let a; + try { + a = await holeMitFrist(`${OEMBED_BASIS}?url=${encodeURIComponent(url)}`, + { accept: "application/json" }); + } catch (f) { + /* Zeitueberschreitung, DNS, kein Netz -- alles Aussagen ueber die + Leitung, keine ueber das Video. */ + return { da: null, grund: `nicht erreichbar (${f?.name || "Fehler"})` }; + } + if (a.status >= 500) return { da: null, grund: `TikTok stoert (${a.status})` }; + /* 400 und 404 sind die Antworten, mit denen TikTok auf geloeschte und + auf privat gestellte Videos antwortet. 403 ebenso. */ + if (a.status === 400 || a.status === 403 || a.status === 404) { + return { da: false, grund: `TikTok kennt es nicht (${a.status})` }; + } + if (!a.ok) return { da: null, grund: `unerwartet (${a.status})` }; + const j = await a.json().catch(() => null); + /* Antwort ohne Titel: TikTok liefert das auch fuer entfernte Videos + mit Status 200. Ein leerer Titel ist deshalb eine Aussage. */ + if (!j || !j.title) return { da: false, grund: "Antwort ohne Titel" }; + return { da: true, grund: "" }; +} + +/** Sieht die faelligen Videos durch. + * + * Getrennt vom Zeitgeber, damit die Pruefung sie aufrufen kann, ohne + * einen Tag zu warten -- und damit sie gemessen werden kann. + * + * @returns {Promise<{geprueft: number, weg: number, zurueck: number, + * unklar: number}>} + */ +export async function videoWacheLauf() { + const d = db(); + const grenze = new Date(Date.now() - WACHE_ABSTAND_TAGE * 86400_000).toISOString(); + const heute = new Date().toISOString().slice(0, 10); + + /* WELCHE ZUERST: die mit Fehlversuchen (die sind verdaechtig und + sollen schnell geklaert sein), dann die am laengsten ungesehenen. + `quelle_geprueft_am IS NULL` zuerst -- das sind die neuen. */ + let faellig = []; + try { + faellig = d.prepare(` + SELECT id, quelle_url, COALESCE(quelle_fehlversuche, 0) AS fehl, + quelle_geprueft_am, quelle_fehlt_seit + FROM eintraege + WHERE quelle_url IS NOT NULL AND quelle_url <> '' + AND ( quelle_geprueft_am IS NULL + OR ( COALESCE(quelle_fehlversuche, 0) > 0 + AND quelle_fehlt_seit IS NULL + AND substr(quelle_geprueft_am, 1, 10) < ? ) + OR quelle_geprueft_am < ? ) + ORDER BY COALESCE(quelle_fehlversuche, 0) DESC, + COALESCE(quelle_geprueft_am, '') ASC + LIMIT ?`).all(heute, grenze, WACHE_JE_LAUF); + } catch (f) { + /* Die Spalten gibt es erst nach dem ersten angemeldeten Aufruf + (sie werden nachgeruestet). Das ist kein Fehler, nur zu frueh. */ + return { geprueft: 0, weg: 0, zurueck: 0, unklar: 0, grund: String(f?.message || f) }; + } + + let weg = 0, zurueck = 0, unklar = 0, geprueft = 0; + for (const e of faellig) { + const { da, grund } = await videoNochDa(e.quelle_url); + const jetzt = new Date().toISOString(); + + if (da === null) { + /* WEISS NICHT AENDERT NICHTS -- auch nicht den Zeitstempel. Wer + ihn setzte, verschoebe die naechste Nachfrage um sieben Tage, + obwohl nichts gemessen wurde. */ + unklar++; + continue; + } + geprueft++; + + if (da) { + /* Wieder da -- oder war nie weg. Beides raeumt auf. */ + if (e.quelle_fehlt_seit || e.fehl) zurueck++; + d.prepare(`UPDATE eintraege SET quelle_geprueft_am = ?, + quelle_fehlversuche = 0, quelle_fehlt_seit = NULL + WHERE id = ?`).run(jetzt, e.id); + continue; + } + + const fehl = e.fehl + 1; + const markieren = fehl >= FEHLT_AB && !e.quelle_fehlt_seit; + d.prepare(`UPDATE eintraege SET quelle_geprueft_am = ?, + quelle_fehlversuche = ?, quelle_fehlt_seit = ? + WHERE id = ?`) + .run(jetzt, fehl, markieren ? jetzt : (e.quelle_fehlt_seit || null), e.id); + if (markieren) { + weg++; + protokolliere("video_nicht_mehr_da", { + detail: `#${e.id} nach ${fehl} Versuchen: ${grund}`.slice(0, 120), + }); + } + if (WACHE_PAUSE_MS) await new Promise((f) => setTimeout(f, WACHE_PAUSE_MS)); + } + + return { geprueft, weg, zurueck, unklar }; +} + +/** Einmal am Tag, erstmals fuenf Minuten nach dem Start. + * + * Fuenf Minuten und nicht neunzig Sekunden wie beim Aufraeumen: Dieser + * Lauf geht nach DRAUSSEN. Er soll nicht mit dem Start um die ersten + * Anfragen der Leute konkurrieren, und er hat es nie eilig -- ein + * Video, das gestern geloescht wurde, darf noch fuenf Minuten laenger + * verlinkt sein. */ +export function videoWacheStarten() { + const erster = setTimeout(() => { + videoWacheLauf().catch((f) => console.error("[video-wache]", f?.message)); + }, 5 * 60_000); + const takt = setInterval(() => { + videoWacheLauf().catch((f) => console.error("[video-wache]", f?.message)); + }, 24 * 3600_000); + erster.unref?.(); + takt.unref?.(); +} diff --git a/server/workspace.js b/server/workspace.js index 2e250f95..5ef70edb 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -1800,7 +1800,21 @@ function umstellungen(d) { try { const spalten = d.prepare("PRAGMA table_info(eintraege)").all().map((s) => s.name); for (const [name, typ] of [["quelle_url", "TEXT"], ["quelle_kanal", "TEXT"], - ["quelle_geholt_am", "TEXT"]]) { + ["quelle_geholt_am", "TEXT"], + /* ---- Stufe 5: fuehrt der Knopf noch irgendwohin? (18.09.2026) ---- + + Ein Video kann bei TikTok geloescht oder auf privat gestellt + werden. Bei uns bleibt der Eintrag stehen -- mit Cover, Text + und einem Knopf, der ins Leere fuehrt. Das faellt niemandem + auf, der die Seite baut; es faellt dem auf, der klickt. + + `quelle_fehlversuche` ist der Grund, warum es drei Spalten + sind und nicht eine: Ein einzelner misslungener Abruf heisst + NICHT, dass das Video weg ist -- er heisst vielleicht, dass + TikTok gerade Schluckauf hat. Wer daraus sofort „geloescht" + macht, leert bei der naechsten Stoerung die halbe Galerie. */ + ["quelle_geprueft_am", "TEXT"], ["quelle_fehlt_seit", "TEXT"], + ["quelle_fehlversuche", "INTEGER"]]) { if (spalten.length && !spalten.includes(name)) { d.exec(`ALTER TABLE eintraege ADD COLUMN ${name} ${typ}`); console.log(`[workspace] Spalte '${name}' an den Eintraegen ergaenzt.`); diff --git a/workspace/app.webmanifest b/workspace/app.webmanifest index a4044584..35a1f0a8 100644 --- a/workspace/app.webmanifest +++ b/workspace/app.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/workspace-192.png?v=202609181521", + "src": "/assets/img/app-symbole/workspace-192.png?v=202609181607", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-512.png?v=202609181521", + "src": "/assets/img/app-symbole/workspace-512.png?v=202609181607", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609181521", + "src": "/assets/img/app-symbole/workspace-192-maskable.png?v=202609181607", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609181521", + "src": "/assets/img/app-symbole/workspace-512-maskable.png?v=202609181607", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/assets/css/bereich.css b/workspace/assets/css/bereich.css index caaa1993..6443391a 100644 --- a/workspace/assets/css/bereich.css +++ b/workspace/assets/css/bereich.css @@ -1165,6 +1165,25 @@ .videozeile__weg { flex: 0 0 auto; text-decoration: none; } +/* Ein Video, das es bei TikTok nicht mehr gibt. + + BEWUSST KEIN ROT und keine Warnfarbe: Es ist kein Fehler, es ist + eine Auskunft. Rot an dieser Stelle liesse die ganze Kachel wie + einen Schaden aussehen, obwohl Titel, Text und Cover weiter + stimmen -- die liegen bei uns. + + Gestrichelter Rand statt Flaeche: Der Platz des Knopfes bleibt + sichtbar, aber nichts darin laedt zum Tippen ein. */ +.videozeile__fort { + flex: 0 0 auto; + padding: .34rem .7rem; + border: 1px dashed rgba(150, 170, 195, .45); + border-radius: .6rem; + color: #8fa3bb; + font-size: .84rem; + cursor: default; +} + /* --------------------------------------------------------------------- DIE KANALZEILE (17.09.2026) -- Schritt 3 aus dem Video-Plan diff --git a/workspace/assets/js/bereich.js b/workspace/assets/js/bereich.js index 3abd1a35..310157a5 100644 --- a/workspace/assets/js/bereich.js +++ b/workspace/assets/js/bereich.js @@ -451,11 +451,29 @@ k.dataset.kanal = e.quelle_kanal; z.append(k); } - const a = el('a', 'schritt videozeile__weg', 'Auf TikTok ansehen →'); - a.href = e.quelle_url; - a.target = '_blank'; - a.rel = 'noopener noreferrer'; - z.append(a); + /* IST DAS VIDEO BEI TIKTOK NOCH DA? (Stufe 5, 18.09.2026) + + Wird es dort geloescht oder auf privat gestellt, bleibt der + Eintrag hier stehen. Der Knopf fuehrt dann auf eine + Fehlerseite -- und wer darauf tippt, haelt UNS fuer kaputt. + + Der Knopf verschwindet trotzdem nicht: Ein Eintrag, bei dem + wortlos etwas fehlt, wirft die Frage auf, ob man sich + taeuscht. Es steht jetzt einfach da. Das Cover bleibt + ohnehin -- es liegt bei uns, nicht bei TikTok. */ + if (e.quelle_fehlt_seit) { + const f = el('span', 'videozeile__weg videozeile__fort', + 'Bei TikTok nicht mehr da'); + f.title = 'Seit ' + (e.quelle_fehlt_seit || '').slice(0, 10) + + ' antwortet TikTok zu diesem Video nicht mehr.'; + z.append(f); + } else { + const a = el('a', 'schritt videozeile__weg', 'Auf TikTok ansehen →'); + a.href = e.quelle_url; + a.target = '_blank'; + a.rel = 'noopener noreferrer'; + z.append(a); + } k.append(z); } diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html index 9a9ecacd..a3d860e5 100644 --- a/workspace/aufgaben.html +++ b/workspace/aufgaben.html @@ -12,19 +12,19 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - + @@ -291,10 +291,10 @@ - - - - - + + + + + diff --git a/workspace/automation.html b/workspace/automation.html index 47adb294..1de1580b 100644 --- a/workspace/automation.html +++ b/workspace/automation.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -168,10 +168,10 @@ - - - - - + + + + + diff --git a/workspace/befinden.html b/workspace/befinden.html index 080ac9e3..4f043935 100644 --- a/workspace/befinden.html +++ b/workspace/befinden.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -105,9 +105,9 @@ - - - - + + + + diff --git a/workspace/bereich.html b/workspace/bereich.html index de057359..d0e4cd83 100644 --- a/workspace/bereich.html +++ b/workspace/bereich.html @@ -12,24 +12,24 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - + + + + + + - - + + - + @@ -243,14 +243,14 @@ - - - - - - - - - + + + + + + + + + diff --git a/workspace/bewerben.html b/workspace/bewerben.html index 33929993..85ef4d21 100644 --- a/workspace/bewerben.html +++ b/workspace/bewerben.html @@ -7,17 +7,17 @@ - - - - - - - - - + + + + + + + + + - + @@ -107,10 +107,10 @@ - - - - - + + + + + diff --git a/workspace/bewerbungen.html b/workspace/bewerbungen.html index 0200ec5b..77e48757 100644 --- a/workspace/bewerbungen.html +++ b/workspace/bewerbungen.html @@ -7,16 +7,16 @@ - - - - - - - - + + + + + + + + - + @@ -80,10 +80,10 @@ - - - - - + + + + + diff --git a/workspace/calls.html b/workspace/calls.html index 9e0ea19d..a4660570 100644 --- a/workspace/calls.html +++ b/workspace/calls.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -89,11 +89,11 @@ - - - - - - + + + + + + diff --git a/workspace/chat.html b/workspace/chat.html index 191c35ea..4db3b2c0 100644 --- a/workspace/chat.html +++ b/workspace/chat.html @@ -12,26 +12,26 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -341,15 +341,15 @@ - - - - + + + + - - + + diff --git a/workspace/content.html b/workspace/content.html index 5e9c9049..47a0c7c6 100644 --- a/workspace/content.html +++ b/workspace/content.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -179,12 +179,12 @@ - - - - - - - + + + + + + + diff --git a/workspace/crew-index.html b/workspace/crew-index.html index a742d637..232763cf 100644 --- a/workspace/crew-index.html +++ b/workspace/crew-index.html @@ -36,15 +36,15 @@ - - - + + + - + @@ -339,6 +339,6 @@ - + diff --git a/workspace/crew.webmanifest b/workspace/crew.webmanifest index fd8da0b9..3273f7fd 100644 --- a/workspace/crew.webmanifest +++ b/workspace/crew.webmanifest @@ -22,25 +22,25 @@ }, "icons": [ { - "src": "/assets/img/app-symbole/crew-192.png?v=202609181521", + "src": "/assets/img/app-symbole/crew-192.png?v=202609181607", "sizes": "192x192", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-512.png?v=202609181521", + "src": "/assets/img/app-symbole/crew-512.png?v=202609181607", "sizes": "512x512", "type": "image/png", "purpose": "any" }, { - "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609181521", + "src": "/assets/img/app-symbole/crew-192-maskable.png?v=202609181607", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { - "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609181521", + "src": "/assets/img/app-symbole/crew-512-maskable.png?v=202609181607", "sizes": "512x512", "type": "image/png", "purpose": "maskable" diff --git a/workspace/dateien.html b/workspace/dateien.html index a6ca6909..77ff6476 100644 --- a/workspace/dateien.html +++ b/workspace/dateien.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -108,10 +108,10 @@ - - - - - + + + + + diff --git a/workspace/entwicklung.html b/workspace/entwicklung.html index c7925010..18acd7ef 100644 --- a/workspace/entwicklung.html +++ b/workspace/entwicklung.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -102,9 +102,9 @@ - - - - + + + + diff --git a/workspace/index.html b/workspace/index.html index 3a2ea1c2..7490d393 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -30,9 +30,9 @@ - - - + + + @@ -401,6 +401,6 @@ - + diff --git a/workspace/kalender.html b/workspace/kalender.html index 744eeb55..9062e557 100644 --- a/workspace/kalender.html +++ b/workspace/kalender.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -377,11 +377,11 @@ - - - - - - + + + + + + diff --git a/workspace/leistung.html b/workspace/leistung.html index 50e951de..4011e04f 100644 --- a/workspace/leistung.html +++ b/workspace/leistung.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -444,10 +444,10 @@ - - - - - + + + + + diff --git a/workspace/personen.html b/workspace/personen.html index 904fcc99..d76a275d 100644 --- a/workspace/personen.html +++ b/workspace/personen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -423,10 +423,10 @@ - - - - - + + + + + diff --git a/workspace/profil.html b/workspace/profil.html index 0544fe12..8d82b928 100644 --- a/workspace/profil.html +++ b/workspace/profil.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -214,11 +214,11 @@ - - - - - - + + + + + + diff --git a/workspace/rechte.html b/workspace/rechte.html index b8043ed1..3a5eec43 100644 --- a/workspace/rechte.html +++ b/workspace/rechte.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -134,9 +134,9 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - + + + + diff --git a/workspace/report.html b/workspace/report.html index dc2d0bd6..b9fcd167 100644 --- a/workspace/report.html +++ b/workspace/report.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -106,11 +106,11 @@ - - - - - - + + + + + + diff --git a/workspace/scouting.html b/workspace/scouting.html index e3e02039..e54968a9 100644 --- a/workspace/scouting.html +++ b/workspace/scouting.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -173,10 +173,10 @@ - - - - - + + + + + diff --git a/workspace/start.html b/workspace/start.html index 15fddd7d..b00f9d44 100644 --- a/workspace/start.html +++ b/workspace/start.html @@ -12,21 +12,21 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - + + - + @@ -674,10 +674,10 @@

- - - - - + + + + + diff --git a/workspace/startcheck.html b/workspace/startcheck.html index 2ffa9581..fc8be5bb 100644 --- a/workspace/startcheck.html +++ b/workspace/startcheck.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -85,11 +85,11 @@ - - - - - - + + + + + + diff --git a/workspace/steckbrief.html b/workspace/steckbrief.html index da0a537e..6e0e777b 100644 --- a/workspace/steckbrief.html +++ b/workspace/steckbrief.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -146,11 +146,11 @@ - - - - - - + + + + + + diff --git a/workspace/talente.html b/workspace/talente.html index d30ab113..f365f790 100644 --- a/workspace/talente.html +++ b/workspace/talente.html @@ -7,18 +7,18 @@ - - - - - - + + + + + + - - - + + + @@ -111,9 +111,9 @@ - - - - + + + + diff --git a/workspace/team.html b/workspace/team.html index 322bcfa0..b29657d7 100644 --- a/workspace/team.html +++ b/workspace/team.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + @@ -110,10 +110,10 @@ - - - - - + + + + + diff --git a/workspace/teamlage.html b/workspace/teamlage.html index 044507dc..bd5d22d5 100644 --- a/workspace/teamlage.html +++ b/workspace/teamlage.html @@ -7,20 +7,20 @@ - - - - - - - + + + + + + + - + - - - - + + + + diff --git a/workspace/teilen.html b/workspace/teilen.html index 24196118..59c860b2 100644 --- a/workspace/teilen.html +++ b/workspace/teilen.html @@ -7,13 +7,13 @@ - - - - - - - + + + + + + + @@ -112,9 +112,9 @@ - - - - + + + + diff --git a/workspace/treff-moderation.html b/workspace/treff-moderation.html index 92c0fba3..10dd3a9f 100644 --- a/workspace/treff-moderation.html +++ b/workspace/treff-moderation.html @@ -7,14 +7,14 @@ - - - - - - - - + + + + + + + + @@ -126,9 +126,9 @@ - - - - + + + + diff --git a/workspace/treff-regeln.html b/workspace/treff-regeln.html index a9296837..d56f5362 100644 --- a/workspace/treff-regeln.html +++ b/workspace/treff-regeln.html @@ -7,18 +7,18 @@ - - - - - - - + + + + + + + - + @@ -211,10 +211,10 @@ Beim ersten Anlauf fehlte die Zeile auf genau diesen beiden neuen Seiten. Gefunden hat das nicht das Auge, sondern pruef-css-klassen, die seit dem 07.09. jede Seite danach absucht. --> - - - - - + + + + + diff --git a/workspace/uebersicht.html b/workspace/uebersicht.html index b0186ebb..0dea434b 100644 --- a/workspace/uebersicht.html +++ b/workspace/uebersicht.html @@ -12,22 +12,22 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - + + + + - - - + + + - + @@ -149,11 +149,11 @@ - + - - - - + + + + diff --git a/workspace/wissen.html b/workspace/wissen.html index 2d089dbb..c71aec0c 100644 --- a/workspace/wissen.html +++ b/workspace/wissen.html @@ -12,20 +12,20 @@ Ton wie die Kopfleiste darunter (#06090f), damit Leiste und Seite eine Flaeche sind statt zweier. --> - - - - - - - + + + + + + + - + @@ -190,10 +190,10 @@ - - - - - + + + + +