Files
rin-mobile-userscript/CLAUDE.md
T
Andre 0d2731eaa9 fix: stop "Mark topics read" from becoming a stuck-open list section
Its link sits inside a category cell but in an inner row with no
nested table, so it slipped past the layout-row guard and got read as
a genuine category heading. Unlike a real heading it never got the
group machinery that makes Announcements and Stickies collapsible, so
it just sat there permanently open with no way to close it.
2026-08-02 16:28:57 +02:00

287 lines
16 KiB
Markdown

# CLAUDE.md
Ein Userscript, das die phpBB-Seiten von `cs.rin.ru` fuer Safari auf iOS in eine
mobile Ansicht umbaut. Kein Build, keine Abhaengigkeiten, kein `package.json` —
die `.user.js`-Datei ist das Produkt.
| Datei | Greift auf | Zweck |
|---|---|---|
| `rin-mobile.user.js` | `*://cs.rin.ru/forum/*` | Beide Ansichten, Weiche nach Seitentyp |
| `dev/rin-mobile.dev.user.js` | zusaetzlich `localhost:*` | Loader, bindet per `@require file:///` das echte Script ein |
| `dev/viewtopic.php` | — | Fixture: gespeicherter Thread, 1114 Seiten |
| `dev/viewforum.php` | — | Fixture: Main Forum, 610 Seiten, mit Unterforen, Ankuendigungen und Stickies |
| `dev/index.php` | — | Fixture: Forumwurzel, zwei Kategorien mit Forenzeilen, keine Paginierung |
| `dev/ucp.php` | — | Fixture: Login-Formular (`mode=login`), nur ausgeloggter Zustand |
| `dev/after_login/ucp.php` | — | Fixture: dieselbe URL nach erfolgreichem Login, "Information"-Vorlage statt Formular |
Die Fixtures sind ueber `.gitignore` (`dev/*.php`) ausgeschlossen. Nach einem
frischen Klon sind sie nicht da und muessen nach der Anleitung in der README neu
gespeichert werden — die Seite verlangt einen Login.
## Aufbau
Ein IIFE, streng der Reihe nach:
1. `DEBUG`-Flag und `log`-Array
2. Weiche: `view` ist `'thread'`, `'liste'`, `'login'` oder `''` — abgeleitet
aus `location.pathname + location.search`
3. Nur bei gesetztem `view`: Viewport-Meta (idempotent ueber `[data-rm]`) und
der komplette Style-Block als Template-Literal
4. `replaceBody(root)` blendet alle bisherigen `body`-Kinder aus und haengt die
neue Ansicht an
5. `pageState()`, `pageSheet()` und `pageNav()` — die Paginierung, von Liste
und Thread benutzt, nicht vom Login
6. `buildList()`, `buildThread()` beziehungsweise `buildLogin()` lesen das
Original-DOM und bauen ein `div.rm-root`; alle drei haengen zum Schluss
ueber `addCrumbs()` die fixierte Brotkrumenleiste an, die zugleich ueber
`loginState()` die Login/Logout-Pille traegt
7. `run()` waehlt die Ansicht und haengt bei `DEBUG` das gruene Overlay an
8. Start ueber `DOMContentLoaded`, falls `readyState === 'loading'`
Auf Seiten ausserhalb der Weiche (`posting.php`, Profile, …) passiert nichts,
insbesondere wird kein Stylesheet injiziert.
## Fallen, die nicht aus dem Code hervorgehen
**Beide Ansichten teilen sich einen Klassennamensraum.** Das war der Grund fuer
die Zusammenlegung: als getrennte Scripts matchte `rin-mobile` auch
`viewtopic.php` und injizierte dort sein Stylesheet, obwohl es nichts baute.
Gleich benannte Regeln kollidierten mit identischer Spezifitaet, und die
Ladereihenfolge der Userscripts entschied ueber das Layout.
- Geteilt: `.rm-root`, `.rm-hidden`, `.rm-dbg`, die globale `img`-Regel
- Geteilt sind ausserdem die Leiste und ihr Sheet: `.rm-nav`, `.rm-sheet*`,
`.rm-goto`, `.rm-grid`
- Unterschiede zwischen den Ansichten ueber einen Modifier am Root loesen,
nicht ueber zwei Regeln mit demselben Selektor. `.rm-root.rm-paged` haelt
so die 64px hinter der fixierten Leiste frei — gesetzt von `pageNav()`
selbst, weil es die Leiste nicht auf jeder Liste gibt.
- Namen sind ansichtsspezifisch zu halten: `.rm-jump` und `.rm-group` gehoeren
der Liste, `.rm-post` und `.rm-sig` dem Thread.
- `.rm-av` braucht `!important` auf Breite und Hoehe, sonst gewinnt die globale
`img { height:auto !important }`-Regel gegen die Avatarmasse.
**Was nach `body` soll, muss nach `replaceBody()` angehaengt werden.**
Die Funktion markiert *alle* vorhandenen `body`-Kinder mit `.rm-hidden`. Das
Sheet wird deshalb erst beim ersten Antippen gebaut — das loest das Problem
nebenbei mit.
**In der Liste entscheidet die Dokumentreihenfolge ueber den Zeilentyp.**
`buildList()` sammelt Links auf `viewforum.php` und `viewtopic.php` in einem
Rutsch ein und ordnet jede `tr` nach ihrem ersten verwertbaren Link zu. Eine
Forenzeile verlinkt in der Spalte „letzter Beitrag" ebenfalls `viewtopic.php` —
zaehlte man Themenlinks zuerst, faellt die Zeile als Thema an, der Forenname
verschwindet und nur die Beschreibung bleibt als `.rm-last` uebrig. Genau so
war es vor 3.2. Brotkrumen verlinken ebenfalls Foren, stehen aber in Zeilen
ohne Forenmerkmale und werden verworfen.
**„Mark topics read" braucht eine eigene Bremse, „Mark forums read" nicht.**
Beide sind reine Aktionslinks, keine Inhaltslinks, aber nur „Mark forums read"
zeigt auf `index.php` und faellt schon beim Einsammeln durch, weil das keine
der gesuchten Adressen ist. „Mark topics read" oberhalb der Themenliste zeigt
dagegen mit `?mark=topics` auf dieselbe `viewforum.php`-Seite und passt damit
auf den Sammel-Selektor. Sein Link liegt zwar in einer eigenen inneren `tr`
ohne verschachtelte Tabelle, `a.closest('td.cat')` findet aber trotzdem die
aeussere Kategoriezelle drumherum — bis 3.7 wurde daraus eine dauerhafte,
nicht einklappbare `.rm-sec`-Karte oberhalb von Global Announcements statt gar
keiner. `rowKind()` verwirft seit 3.8 jeden Link mit `?mark=` oder `&mark=`
in der Adresse, bevor die uebrige Klassifizierung ueberhaupt beginnt. Das
Repository hat keine eingeloggte `viewforum.php`-Fixture, die diesen Link mit
echtem Markup zeigt — geprueft ist die Bremse nur gegen den vermuteten
`mark=topics`-Parameter, per Texteinfuegung in `dev/viewforum.php`.
**Der Typ einer Themenzeile steht im Symbol, nicht am Titel.** `rinDark`
unterscheidet `global_read.png`, `announce_read.png`, `sticky_read.png` und
`topic_read.png` in der ersten Spalte, jeweils auch als `_locked`-Variante. Die
Trennzeilen der Seite (`<td class="row3" colspan="6"><b>Stickies</b>`) tragen
keinen Link und fallen beim Einsammeln durch; ihr Text ist trotzdem die beste
Beschriftung und wird ueber die Vorgaengerzeilen nachgeschlagen. Achtung beim
Zaehlen im Fixture: dieselben Symbole stehen unten noch einmal in der Legende,
ein Grep zaehlt also mehr, als es Zeilen gibt.
**Der Menuebalken der Seite verlinkt Themen.** „Forum rules" und die FAQ stehen
in einer Layoutzeile ohne Tabelle oder Script darin, die Bremse in `rowKind()`
greift dort also nicht. Ohne das Merkmal `a.topictitle` beziehungsweise zwei
Zahlenspalten wurde daraus eine Geisterkarte am Kopf jeder Liste — mit
Gruppenkoepfen faellt sie als eigener Abschnitt „Themen 1" auf.
**Der Gruppenkopf entfaellt hinter einer Kategoriezeile.** Sonst stuende auf
der Forenwurzel unter jeder Kategorie noch ein „FOREN n" — die Kategorie leitet
die Foren darunter bereits ein. Die Karten werden deshalb erst vollstaendig
gebaut und dann ausgegeben: die Anzahl im Kopf steht erst fest, wenn feststeht,
welche Zeilen ueberhaupt eine Karte hergeben.
**Das Seitengeruest ist selbst eine Tabelle.** `closest('tr')` liefert fuer
einen Link ausserhalb einer Listenzeile die Layoutzeile, die die ganze Seite
enthaelt. Ohne Bremse wurde daraus eine Karte mit dem kompletten Seitentext —
Kopfzeile, Stylesheet und Inline-Scripts inklusive — als Forenbeschreibung.
Zeilen mit `table`, `script`, `style` oder `form` darin sind deshalb keine
Kandidaten, und die Beschreibung wird nur aus den direkten Textknoten der
Titelzelle gebildet, nie aus deren gesamtem Text.
**Das Theme ist `rinDark`, nicht subsilver2.** Fuer die Forenliste waren die
Klassennamen aus der phpBB-Dokumentation (`forumlink`, `forumdesc`) deshalb
zunaechst nur eine Vermutung; `dev/viewforum.php` bestaetigt sie inzwischen. Der
Rueckfall auf die Form der Zeile bleibt trotzdem stehen — er kostet nichts und
traegt die Erkennung, wenn das Theme wechselt.
**Die Brotkrumenleiste ist fixiert, nicht sticky.** `.rm-root` traegt
`overflow:hidden` und wuerde damit zum Scrollport einer `position:sticky`
-Leiste, in dem nie gescrollt wird — sie klebte nie. Fixierte Nachfahren
klammert das `overflow` dagegen nicht, weil der Root keinen Containing Block
aufspannt. Die Freihaltung oben haengt an `body.rm-crumbed`, damit ohne
Brotkrumen im Markup kein leerer Streifen bleibt.
**Die Login-Erkennung ist inzwischen gegen eine echte Sitzung geprueft.**
`loginState()` unterscheidet `a[href*="mode=login"]` von
`a[href*="mode=logout"]` im Menuebalken und liest den Namen aus dem Muster
`Logout [ Name ]` im Linktext. `dev/index.php` und `dev/after_login/ucp.php`
sind eingeloggt aufgenommen (`Logout [ <Benutzername> ]`) und bestaetigen das Muster.
Der Fallback bleibt trotzdem stehen: passt das Muster einmal nicht — andere
Sprache, anderer Klammertyp — zeigt die Pille „Logout" ohne Namen, statt eine
Ausnahme zu werfen.
**`ucp.php?mode=login` rendert nach dem Absenden dieselbe URL neu, diesmal
ohne Formular.** phpBB fuellt die Zwischenmeldung („You have been successfully
logged in.") in dieselbe generische „Information"-Vorlage, die auch fuer
andere Meldungen auf dieser Seite dient (vermutlich auch Login-Fehler, dafuer
liegt aber keine Fixture vor). `buildLoginInfo()` erkennt die Vorlage deshalb
an ihrer Form — eine Kopfzeile mit genau einer `th`-Zelle, gefolgt von einer
`td.row1`-Nachrichtenzelle — nicht am Wortlaut, und uebernimmt die Zelle
unveraendert wie das Formular selbst. Findet `buildLogin()` weder Formular
noch diese Form, baut es nichts; die Originalseite bleibt sichtbar.
**`index.php` wird ueber `location.pathname` erkannt, nicht ueber
`here`.** Der Login-Redirect haengt "index.php" gelegentlich als Wert eines
Query-Parameters an `ucp.php` (`redirect=index.php`, siehe `dev/ucp.php`).
Ein Test auf `location.pathname + location.search` wie bei den uebrigen
Weichen wuerde eine solche Login-URL faelschlich als Liste einordnen, noch
bevor die Pruefung auf `ucp.php` ueberhaupt greift.
**Ist eine Seite nicht laenger als der Viewport, kann Safari nicht wegscrollen,
um seine eigene Werkzeugleiste unten freizugeben.** Bei Liste und Thread faellt
das nicht auf, weil Karten beziehungsweise Beitraege fast immer laenger sind
als der Bildschirm. Der Login hat kein `.rm-nav`, das sonst Platz reserviert,
und war als kurze Formularseite der erste Fall, bei dem die Seite genau so
hoch wie der Viewport enden konnte — die teilweise eingefahrene Leiste blieb
dann stehen und verdeckte den Submit-Button, ohne dass noch etwas zu scrollen
war, um sie wegzuschieben. `env(safe-area-inset-bottom)` allein loest das
nicht, das federt nur den Home-Indicator ab. Der Bodensatz unter dem Formular
in `.rm-login form` ist deshalb absichtlich groesser als noetig: er erzwingt
Scrollhoehe ueber den Viewport hinaus.
**`buildLogin()` baut das Formular nicht nach, sondern verschiebt es.** Wie der
Beitragstext im Thread (`bodyWrap.appendChild(pb)`) wandert das
Original-`<form>` unveraendert in `.rm-root` und wird nur per CSS auf
Block-Elemente umgelegt. Nachbauen wuerde bedeuten, `sid`, das doppelte
`redirect`-Feld und `autologin`/`viewonline` einzeln nachzuziehen — verliert
man dabei eines, scheitert der Login stumm oder landet nicht dort, wo er soll.
Aus demselben Grund faengt `buildLogin()` keine Fehlermeldung eines
gescheiterten Logins ab: ohne Fixture eines fehlgeschlagenen Versuchs waere die
Erkennung geraten. Fehlt das Formular (`input[name="username"]`), baut die
Funktion nichts und die Originalseite bleibt sichtbar.
**`Page X of Y` steht als `Page <strong>1</strong> of <strong>1114</strong>`
im Markup.** Ein Grep im Roh-HTML nach `Page 1 of` findet nichts, die Regex auf
`txt(document.body)` greift trotzdem. Nicht vorschnell fuer kaputt halten.
**Seitenlinks stehen nicht nur im Paginierungsblock.** Jede Themenzeile mit
mehr als einer Seite bringt ihr eigenes „Go to page: 1 … 42, 43" mit, und diese
Links tragen ebenfalls `start=`. Die Liste baute ihre Seitenleiste bis 3.5 aus
allen `a[href*="start="]` der Seite und kam so auf 103 Eintraege, von denen die
meisten mitten in einen fremden Thread fuehrten. `pageState()` macht es richtig
und liest nur den Block um den `jumpto()`-Link.
**`per_page` kommt aus der `jumpto()`-Funktion der Seite**
(`var per_page = '15';`). Nur damit lassen sich Seiten-URLs als
`start = (Seite - 1) * per_page` selbst bauen. Die gerenderten Seitenlinks
taugen nicht als Quelle — phpBB zeigt bei 1114 Seiten nur `1, 2, 3, 4, 5 … 1114`.
Ohne `per_page` degradiert die Leiste auf erste Seite plus die
Original-Zurueck/Weiter-Links, das Sheet verschwindet.
**`postRow()` steigt bewusst mehrere `tr` hoch.** Der Beitragstext liegt in
einer verschachtelten Tabelle, `closest('tr')` trifft die innere Zeile ohne
Autor- und Profilspalte.
**`DEBUG` steht auf `true`** und muss vor dem Ausliefern auf `false`. Die
Versionsnummer steht doppelt: im `@version`-Header und im Text des
Debug-Overlays.
## Testen
Es gibt keine Testsuite. Bewaehrt hat sich ein Wegwerf-Harness mit jsdom im
Scratchpad (nicht im Repo ablegen — das Projekt soll abhaengigkeitsfrei
bleiben):
```js
const dom = new JSDOM(html, { url: 'https://cs.rin.ru/forum/viewtopic.php?f=10&t=68689&start=45' });
```
Drei Stolpersteine dabei:
- JSDOM parst asynchron fertig. Nach `eval(src)` rund 60 ms warten, sonst hat
das Script noch auf `DOMContentLoaded` gewartet und nichts gebaut.
- `location.href` ist in JSDOM nicht ueberschreibbar. Fuer Navigationstests im
eingelesenen Quelltext `location.href = u;` durch einen Hook ersetzen.
- `requestAnimationFrame` und `scrollIntoView` fehlen und muessen gestubbt
werden.
Sinnvolle Faelle: erste Seite, zweite Seite, Mitte, letzte Seite, dazu die
Degradation ohne `per_page` und ein Thread ohne Paginierung. Fixture-Eckdaten:
`f=10&t=68689`, `per_page = 15`, 1114 Seiten, letzte Seite `start=16695`.
Dieselben Faelle gelten seit 3.5 fuer die Liste, die sich die Leiste mit dem
Thread teilt. Eckdaten von `dev/viewforum.php`: `f=10`, `per_page = 100`, 610
Seiten, letzte Seite `start=60900`, dazu 4 Unterforen, 7 globale
Ankuendigungen, 2 Ankuendigungen, 8 Stickies und 92 Themen. Achtung beim
Nachbauen einer einseitigen Liste: die Seitenlinks zu entfernen genuegt nicht,
`Page 1 of 610` steht auch im Text und traegt die Gesamtzahl allein.
Fuer die Weiche und die Listenansicht genuegt eine handgeschriebene Tabelle mit
`a.topictitle`-Zeilen. Zu pruefen ist dabei auch eine Seite ausserhalb der
Weiche wie `posting.php`: dort darf weder eine Ansicht gebaut noch ein
`<style>` injiziert werden.
Fuer die Forenliste braucht es eine zweite Tabelle im subsilver2-Schnitt:
`td.cat > h4 > a` als Kategoriezeile, darunter Zeilen mit `a.forumlink`, drei
`p.forumdesc` (Beschreibung, Moderatoren, Unterforen), zwei Zahlenspalten und
einer Spalte „letzter Beitrag" mit Link auf `viewtopic.php`. Vier Faelle sind
sinnvoll: reine Forenliste, reine Themenliste, beides auf einer Seite, und
dieselbe Forenliste ohne die Klassen `forumlink`/`forumdesc` — die muss ueber
die Zahlenspalten dasselbe Ergebnis liefern.
Fuer den Login genuegt `dev/ucp.php` unveraendert: geprueft werden das
uebernommene Formular (Username-/Passwortfeld, `sid`, `redirect`, Submit) und
die Pille im ausgeloggten Zustand. Fuer den eingeloggten Zustand der Pille
`dev/index.php` oder `dev/after_login/ucp.php` laden — beide sind eingeloggt
aufgenommen und tragen das echte `Logout [ Name ]`-Markup.
Fuer die Meldung nach dem Login genuegt `dev/after_login/ucp.php`
unveraendert, URL `ucp.php?mode=login`: geprueft werden Titel, uebernommene
Nachricht samt Link und die Pille. Zu pruefen ist ausserdem, dass `dev/ucp.php`
(das Formular, gleiche URL) weiterhin die Formular-Zweig traf und nicht in die
Meldungs-Erkennung faellt.
Fuer die Forumwurzel genuegt `dev/index.php`, URL `index.php?sid=…`: zwei
Kategorien mit zusammen 16 Forenkarten, keine Paginierung. Zu pruefen ist
zusaetzlich eine `ucp.php?mode=login&redirect=index.php`-URL — sie muss trotz
des `index.php`-Substrings im Login-Zweig landen, nicht im Listen-Zweig.
Am Geraet zu pruefen bleibt, was Blink nicht zeigt: `env(safe-area-inset-*)`,
fixierte Leisten gegen Safaris ein- und ausfahrende Toolbar,
`-webkit-backdrop-filter`, Schriftskalierung. Details und die
Tampermonkey-Einrichtung stehen in der README.
## Konventionen
- **ES5-Stil im Script-Code:** `var`, `function`, keine Arrow Functions, kein
`let`/`const`. Template-Literals und `Set` sind in Gebrauch. Keine
Transpilation, kein Linter.
- **Kommentare deutsch, Umlaute als `ae`/`oe`/`ue` transliteriert.** Echte
Umlaute nur in Strings und Regexes, die im UI landen.
- Kommentare begruenden, warum etwas so gebaut ist (welche DOM-Eigenheit von
phpBB dahintersteckt), nicht was der Code tut.
- CSS-Klassen durchgaengig mit `rm-` praefixiert.
- Neue nutzersichtbare Funktionen in der README dokumentieren — zweisprachig,
siehe globale Anweisung.