Forte Profiles — Step 4: Implementation Summary

  • Changes:

    • TemplateRenderer::renderAuthorHtml(): gained an optional bool $forteTarget = false param, switching the base path from /profiles///user/ to /forte/profiles///forte/user/ when true; default unchanged.

    • renderFile(): new $forteAuthor closure exposed alongside the existing $author closure, calling renderAuthorHtml($record, $e, true).

  • Verification:

    • php -l clean.

    • curl classic's /threads/root-001 and Forte's /forte?selected=root-001: author links on both still point at /profiles///user/ — no template calls $forteAuthor yet, so this stage is purely additive as planned.

  • Notes: none identified.

_(Stage numbers below reflect the Step 3 amendment recorded after this stage — see that doc's note.)_

Stage 2 - Full Forte-styled single-profile page

  • Changes:

    • Application.php: new route /forte/profiles/{slug} and renderForteProfile(), reusing fetchProfileBySlug() unchanged.

    • templates/pages/forte_profile.php: new Forte-styled window (reuses the forte_compose_thread dialog's titlebar chrome as a page-level header, not a <dialog>) showing username, approval status/by-whom, thread/post counts, an advanced-details disclosure (identity ID, profile slug, a link back into the board for the bootstrap thread, public key) — no "Approve user" form. Includes a link back to /forte.

    • forte.css: new .paned-standalone-window/.paned-standalone-body/.paned-standalone-advanced/.paned-standalone-back rules — a centered, auto-height variant of .paned-window for single-card pages instead of the full-bleed board layout.

  • Verification:

    • php -l clean.

    • curl both an approved and an unapproved profile slug: correct fields render, zero "Approve user" occurrences either way.

    • Diffed classic's /profiles/{slug} response across two requests (cache-bust only difference): byte-for-byte identical; git diff confirms profile.php untouched.

    • Screenshot confirms the page reads as a clean, centered "properties window" consistent with the paned chrome; the "Approved by" link correctly points at /forte/profiles/..., not classic's.

    • Re-ran the broader board regression suite — all still pass, zero console errors.

  • Notes: none identified.

Stage 3 - Forte-styled username aggregate page

  • Changes:

    • Application.php: new route /forte/user/{username} and renderForteUsername(), reusing fetchProfilesByUsernameToken()/countVisibleAuthoredRows()/fetchVisibleAuthoredThreads()/fetchVisibleAuthoredPosts() unchanged — identical logic to classic's renderUsername().

    • templates/pages/forte_username.php: combined thread/post counts, authored thread and post lists (threads link via /forte?selected={id}, posts via the same /forte?selected={threadId}&created_post_id={postId}#post-{postId} permalink shape forte_post_permalink established), approved/unapproved profile lists linking to /forte/profiles/{slug}.

    • TemplateRenderer: small follow-on fix found during verification — renderContentMeta() (used for the "by {author} on {date}" line) hardcoded the classic-target renderAuthorHtml() call with no way to opt into the Forte-target variant. Added the same $forteTarget param plus a $forteContentMeta closure, mirroring Stage 1's $author/$forteAuthor split exactly. forte_username.php uses it so its own inline "by guest" mentions don't leak back to classic.

  • Verification:

    • php -l clean.

    • curl both /user/guest and /forte/user/guest (a username with 36 threads/47 posts across 15 approved profiles): combined counts match exactly.

    • Headless-browser test: followed a generated post-permalink-shaped link for an actual reply (not just a thread root), confirmed the reply gets paned-highlight-new — the full round trip works, not just the URL shape.

    • Diffed classic's /user/guest across two requests: byte-for-byte identical, confirming zero impact from the renderContentMeta() change.

    • Screenshot confirms visual consistency with the single-profile page; author mentions within thread/post rows now correctly point at /forte/user/guest, not classic's /user/guest.

    • Re-ran the broader board regression suite — all still pass, zero console errors.

  • Notes: none identified.

Stage 4 - Point Forte's own author names at the new profile pages

  • Changes:

    • paned_board_content_pane.php (root post) and paned_thread_reply_tree.php (replies, including its use capture list) switch from $author(...) to $forteAuthor(...).

  • Verification:

    • php -l clean.

    • curl the board: every author link across the whole page now points at /forte/profiles/...//forte/user/... — zero remaining /profiles///user/ occurrences (confirmed by grepping and counting all four link shapes).

    • Headless-browser test: clicked both an approved (username-linked) and an unapproved (slug-linked) author, confirmed each lands on a working Forte page (title contains "Forte"), zero console errors.

    • Diffed classic's /threads/root-001 across two requests: byte-for-byte identical.

    • Re-ran the broader board regression suite — all still pass.

  • Notes: none identified.

Stage 5 - Quick-glance summary dialog on author-name click

  • Changes:

    • TemplateRenderer::renderAuthorHtml(): Forte-target links gain data-forte-author-link/data-profile-slug attributes for the click interception to key off.

    • paned_profile_summary_dialog.php: new dialog (same titlebar chrome as the New Thread dialog) with a static skeleton of data-role-tagged elements the JS fills via textContent — no innerHTML string-building from fetched data, matching this project's existing untrusted-text convention (thread_reactions.js's setFeedback).

    • paned_board_reader.js: a document-level delegated click listener intercepts [data-forte-author-link] clicks, fetches /api/get_profile?profile_slug=... (reused unchanged), parses its Key: Value text format with a small new parseColonValue() (distinct from thread_reactions.js's parseResponseValue(), which parses key=value — confirmed not reusable as-is, per the Step 3 plan's flagged risk, just close enough in spirit to model after), and populates the dialog.

  • Verification:

    • node --check / php -l clean.

    • Headless-browser test: clicked an author name, confirmed the dialog opens with correct fetched data and the URL/board state never changes (no navigation); confirmed the "View full profile" link's href matches the original anchor's href exactly, and following it lands on the right page.

    • Found and fixed during verification: the click handler initially called preventDefault() unconditionally, which would have hijacked Ctrl/Cmd+Click ("open in new tab") — a real regression against the plan's own stated requirement. Added a guard (event.button/ctrlKey/metaKey/shiftKey/altKey) so only a plain primary click is intercepted. Verified via a real Ctrl+Click in headless Chromium: a new tab opens to the correct URL and the dialog never appears.

    • Diffed classic's /threads/root-001 across two requests: byte-for-byte identical.

    • Screenshot confirms the dialog reads as a native part of the paned chrome, consistent with the New Thread dialog's own look.

    • Re-ran the broader board regression suite — all still pass, zero console errors.

  • Notes: none identified.

Stage 6 - Forte-styled user directory and board nav entry point

  • Changes:

    • Application.php: new route /forte/users/ and renderForteUserDirectory(), reusing fetchApprovedUserDirectoryUsers() unchanged.

    • templates/pages/forte_users.php: paned-styled list, each entry linking straight to /forte/user/{token} (no summary-dialog detour — matches Step 2's stated scope, since a directory row already names exactly one destination).

    • forte_board.php: a small "Users" link added to the statusbar as the board's entry point into the directory.

  • Verification:

    • php -l clean.

    • curl /forte/users/: entries and counts render correctly, sorted the same way as classic's own directory; every link points at /forte/user/....

    • Headless-browser test: clicked the board's new "Users" link, confirmed it lands on the directory page.

    • Diffed classic's /users/ across two requests: byte-for-byte identical.

    • Screenshot confirms visual consistency with the other new Forte pages.

    • Re-ran the broader board regression suite — all still pass, zero console errors.

  • Notes: none identified.

Stage 7 - Full regression check

  • Changes: none (verification only, as planned).

  • Verification:

    • Diffed all three classic destinations (/profiles/{slug}, /user/guest, /users/) plus /threads/root-001 and / across two requests each: byte-for-byte identical across the board.

    • Grepped all three new Forte pages for "approve": every occurrence is read-only display text ("Approved: yes", "Approved by:", "Approved Profiles"/"Unapproved Profiles" section headers) — zero forms, buttons, or action links anywhere.

    • Re-ran every existing board regression suite from prior cycles: forte_reactions (Like/Flag), forte_post_permalink (including the tag-filter-escape case), forte_thread_selection_url_sync (all three history modes, measured via history.length deltas), and the general New Thread/Reply/multi-thread-Like suite — all pass, zero console errors.

    • Data parity already confirmed per-stage (Stage 2: profile fields; Stage 3: username aggregate counts; Stage 6: directory counts) — all matched classic's own numbers exactly, re-confirmed here as still holding after Stages 4-6's changes.

  • Notes:

    • This completes all 7 planned stages for forte_profiles. Forte now has its own profile page, username aggregate page, and user directory — all reusing classic's exact data-fetching logic unchanged, with author names across the board pointing at them instead of classic, and a quick-glance summary dialog for the common case. The "Approve user" action is deliberately absent everywhere, reserved for the separate forte_user_approval cycle.