Two releases closed out in one long session. v8.2 was the planned work (the URL-resolution fix from v8.1's post-mortem, finally deployed). v8.2.1 was unplanned — ISEmega went live and immediately exposed four real defects, all found by Andrew's own QA against the live corpus.
ResultFormatter.py, MiscSearch.py, and IndexBuilder.py each carried their own separate copy of a $boardurl-scraping "self-correcting" guess (does this string already end in /The_ISE_Project or not?). The guess was wrong on Dev, producing bad index.php?topic=… links. PDFsearch.py had a related but different version — BASE_URL hardcoded to Live's domain, so Dev clicks silently redirected to Live.
ise_settings.py v4.0: one hardcoded _SITE_ROOTS table holding the real, hand-verified URL for each of the four (environment, install) combinations, keyed by two whitelisted words. A one-line per-install Settings_ISE.json (sitting next to that install's own Settings.php) selects the row. An unrecognised combination raises immediately, listing valid options, rather than silently building a wrong link.
Deliberately rejected: putting URLs or filesystem paths in the JSON. Typo-prone free-text was the root cause being fixed — the JSON selects a row, it never supplies URL material.
Files: ise_settings.py v4.0 (new), ResultFormatter.py v7.4.2, MiscSearch.py v7.4.2, IndexBuilder.py v7.4.2, PDFsearch.py v7.4.2, qf_search_bridge.php v1.1, qf_MegaSearch_bridge.php v8.1 (both bridges: putenv('ISE_BASE_URL=…') removed, nothing reads it anymore; ISE_ROOT kept, unrelated).
The uploaded zip wasn't the fix. ISE_Code_v8_2.zip turned out to be the pre-fix v8.1 baseline — ise_settings.py still v1.0, all four modules still scraping. Confirmed by grep before touching anything. The entire fix had to be rebuilt from scratch this session.
Doc-block ≠ visible version. IndexBuilder.py, MiscSearch.py and PDFsearch.py had their doc-block @version bumped to 7.4.2 but not their separate, actually-printed VERSION constant — caught only because Dev's own run output still said v7.4.1 after "deploying" the fix. Exactly the distinction the doc-block convention exists to catch. Fixed and redelivered.
Ran ise_settings.py against a simulated Live/smf20 install: resolves to https://forum.drugs-and-users.org/smf20 (base) and …/smf20/The_ISE_Project (project), no double segment. Confirmed an unrecognised (environment, install) pair raises with the full valid list. ResultFormatter.py import-tested end-to-end against that same layout.
On the boxes: Live Test/smf20 deployed via Andrew's own procedure (DB backup → HTML mirror → DB edit → restore/verify). Dev/smf20 ran IndexBuilder.py and MiscIndexer.py clean, no URL errors — 9 posts / 952 words, 5 misc attachments / 1046 words.
Not reached: Dev Test/smf20 and Live, the last two legs of the agreed rollout order.
Four defects, all surfaced by real use against the live corpus rather than by testing.
meth* on Live reported 6 hits — identically on Live and smf20 — while search.py, PDFsearch.py and MiscSearch.py each returned plenty run individually. That split localised it instantly to the merge/display step.
Cause: MEGA_PAGE_SIZE = 6. Not a stray testing value — its own comment showed it was a deliberate page size from 2026-08-20. The actual defect was that pagination was never wired up: nothing ever requests page 2, so a "page size" with no page 2 is just a silent cap. Worse, the count line reported the real merged total while only that many cards rendered, so the two disagreed.
Raising it to 30 just moved the mismatch (39 reported, 30 shown).
Landed on: MEGA_SOFT_CAP = 180, plus a loud red notice at the very top of the results page stating how many were held back, why, and linking to "Show all N results" (--all / &all=1). Precedence: explicit --limit wins, then --all, then the soft cap. Everything found is still searched, ranked and counted — the cap governs only rendering, and the page says so.
Andrew's call, and the right one: capping is smart; capping silently was the bug.
For scale on why this mattered: meth* matches 5272 posts across 1813 topics on Live.
ise_render.interlace_results() was a flat sort() by raw score across all three lists combined. That only ever looked correct because the old per-source fetch cap (20, later 60) accidentally forced a mix. Remove the cap, and any query where one source outscores the others across the board returns a page entirely from that one source.
Fixed as true round-robin — each source's next-best in turn, until all are exhausted. Verified against the real scenario (PDF scoring ~9x higher than posts): first 180 came back 88 posts / 87 PDF / 5 misc instead of 180 PDF. Each source's internal ranking fully preserved, nothing lost, exhausted sources drop out cleanly, handles empty/None/missing-score inputs, doesn't mutate the caller's dicts.
Bonus: round-robin never compares a PDF score against a posts score, so it sidesteps the cross-source normalization question deferred on 2026-08-20 rather than reopening it.
ise_render.py v8.0#4.
Three real bugs, all the same root cause — the card rebuilding what PDFsearch.py already hands it:
search.BASE_URL + "index.php?msg=N" — the posts module's base URL, and a URL shape PDFsearch never uses. It already returns r["msg_url"], correctly built as topic=N.msgN#msgN.target="_blank" on that link, so it didn't open in a new tab the way every link on ISEpdf's own results page does.board_color.All three now read straight from what get_ranked_results() already returns. Verified by rendering a card with realistic data and asserting each URL, colour and target attribute.
The page painted every card at full detail and only collapsed once ise_mega_detail.js ran — worst of both worlds on a large result set. <body data-mega-detail="minimal"> is now baked in server-side.
Caveat, deliberately not hidden: the JS still restores an explicit Full choice from localStorage a moment after load. So this governs first-time visitors and anyone who hasn't chosen — if you've picked Full in your own browser, you'll still see Full. Clear localStorage to test as a fresh visitor.
MegaSearch.py v8.0#11 → #15 across these four.
PDFsearch.py v7.5.0 — explicit AND/OR mode for ISEpdf. Scoping this turned up a genuine surprise: ISEpdf's search() was already soft-OR (any page matching ≥1 term included, match_count only feeding the ranking score), while posts search's ANDMatcher.py has always been strict AND. Nobody had documented that asymmetry.
So the real gap was adding an AND mode to ISEpdf, not adding OR. match_mode="or" (default, unchanged) / "and" (new hard filter), threaded through search()/get_ranked_results()/main()'s new --mode {or,and}. Defaults to "or" everywhere, so MegaSearch, cron jobs and saved bookmarks behave exactly as before.
Verified by extracting tokenise()/search() and running them against synthetic word-index data: OR returned all matching pages, AND correctly narrowed to only the page containing every term; a quoted "phrase" counts as one required unit, not per-word; wildcards (meth*) likewise behave as one unit.
Design decision — no UI toggle. A bare unquoted AND typed in the query string itself (like Google's typed OR) switches to strict mode, documented in ISE_help.html rather than advertised in the search UI: "let them discover." Not yet built — needs the query-string parsing wired wherever the web bridge calls into PDFsearch.py.
LMVBBC.php v3.3.0 — [lmv=help] / [lmv=?]. A comprehensive info box listing every real [lmv] tag form, built from add()'s own doc-block list (and that list gained an entry for the help form itself, so the two can't drift apart). Uses the redbox mod's RedBoxBBCode::render() when installed, falls back to LMV's own error-box styling otherwise — no hard dependency introduced.
The mega PDF and Misc cards are still hand-written. The posts card was fixed on 2026-08-21 to delegate to ResultFormatter.py's real _render_cluster_item(). PDF and Misc have no equivalent shared function to delegate to, so they remain parallel copies — which is exactly how defect #3 above happened, and how the Formatted/Raw buttons went missing before that. It will drift again. The real fix is delegation, and it's a bigger job.
The over-180 notice is not RedBox. RedBoxBBCode is a PHP class in SMF's Sources/; ISEmega's page is standalone HTML written by Python and readfile()d by the bridge. Python can't call PHP — making redbox requisite changes what's installed, not what's callable across a language boundary. The notice matches redbox's look in ISEmega's own CSS. If the real class must render it, the notice has to move into qf_MegaSearch_bridge.php, which then needs the held-back count passed back from Python.
--all / &all=1 pass-through in qf_MegaSearch_bridge.php — the "Show all" link currently reverts to the capped view because the bridge doesn't read the flag or pass --all to Python. Bridge file not yet seen this session. (Lower priority now that Andrew has accepted the 180 cap as the standing behaviour.)[tag=help] mechanism across all BBCode mods, sourced from each module's own bundled readme.txt, baked at install time, with a link back to the Master Post. Designed, unbuilt.mirror.sh dotfile exclusion and the Dev LVM resize — both still open from before this session.