diff --git a/docs/cms-upgrade-2026-09.md b/docs/cms-upgrade-2026-09.md
index f91f845e..3cdc53b1 100644
--- a/docs/cms-upgrade-2026-09.md
+++ b/docs/cms-upgrade-2026-09.md
@@ -114,3 +114,25 @@ change is required. Browser checks use real components with simulated data at
New UI copy is supplied in English, Italian and Dutch; other locales receive English fallback strings. No new runtime dependencies. Browser fixtures use real UI components with simulated server responses; the database migration and production behavior have not been exercised on the live hotel.
Validation for this increment: 1,589 tests passed, six skipped; TypeScript, Biome, translation contracts and fixture production build passed. Browser checks covered user/settings/news forms, permission preview, CMS errors, integrity preview and history restore at 1280 and 390 pixels. Double submission, stale preview, blocked navigation, field focus and horizontal overflow were checked with simulated server actions. No live database writes or deployment were performed.
+
+
+## September 11: public pages and staff workflows
+
+- Docker stages now follow the exact `.nvmrc` release, enforced by the toolchain check.
+- `/news` supports search, ordering by effective publication date and real pagination.
+- `/events` supports upcoming/ongoing/completed filters, explicit UTC week windows, local displayed times and personal registrations.
+- `/search` searches users, open rooms, published news and events with independent pagination and partial failure states.
+- `/me` shows support replies, incoming friend requests, the next registered event and available referral rewards. Reply availability does not claim unread status.
+- Profile privacy is managed in `/settings`. Wallet values are private by default; visitors do not receive hidden sections in HTML. Photo galleries initially show six photos and can expand to the loaded limit of 24.
+- Ticket desks support waiting-for-staff and assignment filters, with elapsed time since the latest reply.
+- HK table views save filters, order and visible columns per account and table (maximum 20). Existing session column preferences remain available until a named view is applied.
+- Official and clone synchronization run through the existing durable import queue. Reloading restores history; interrupted uncertain writes still require inspection before repair. Successful items are not repeated.
+- Publication preflight validates URL syntax/protocols and schedules, shows affected page links and keeps existing article previews. It does not claim remote URLs are reachable. Drafts remain savable. Partial event updates preserve omitted fields.
+- Admin APIs return `x-operation-id`; server errors, staff audit records and import jobs share correlation context. Error and audit screens link to each other. Older records without this context remain readable.
+- Public reads distinguish unavailability from empty results and real 404s, preserving independently available sections on home, dashboard, staff, photos, rankings and groups/forums.
+
+### Data and verification
+
+Additive migrations `0029_admin_table_views.sql` and `0030_profile_privacy.sql` run through the existing deployment migration runner. They create CMS-owned tables and do not change emulator user settings. Keep the existing shared storage volume and background jobs worker for durable imports.
+
+Browser verification used real components with controlled data fixtures at 1280 and 390 pixels, including failure and partial-result cases. Production compilation and full lint were checked locally. No production content was created during those checks; real authenticated content and external source availability remain environment-dependent.
diff --git a/src/app/(site)/guilds/[id]/forum/[threadId]/page.tsx b/src/app/(site)/guilds/[id]/forum/[threadId]/page.tsx
index fe0fb2c4..f5dc495a 100644
--- a/src/app/(site)/guilds/[id]/forum/[threadId]/page.tsx
+++ b/src/app/(site)/guilds/[id]/forum/[threadId]/page.tsx
@@ -4,6 +4,7 @@ import { getTranslations } from "next-intl/server";
import type { CSSProperties } from "react";
import { replyToThread } from "@/actions/social";
import Link from "@/components/link";
+import { PublicLoadError } from "@/components/public/load-error";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { auth } from "@/lib/auth";
import {
@@ -14,6 +15,7 @@ import {
User,
} from "@/lib/db";
import { formatDate } from "@/lib/format-date";
+import { publicReadFailure } from "@/lib/services/public-read";
type SearchParams = Promise<{ replied?: string; error?: string }>;
@@ -60,6 +62,8 @@ export default async function GuildForumThreadPage({
pinned: number | null;
openerId: number | null;
} | null = null;
+ let postsFailed = false;
+ let authorsFailed = false;
let posts: {
id: number;
userId: number;
@@ -96,9 +100,15 @@ export default async function GuildForumThreadPage({
]);
guild = guildRow;
thread = threadRow;
- } catch {
- guild = null;
- thread = null;
+ } catch (error) {
+ publicReadFailure("guild.thread")(error);
+ return (
+
+
+
+
+
+ );
}
if (!guild || !thread) notFound();
@@ -122,7 +132,9 @@ export default async function GuildForumThreadPage({
asc(GuildsForumsComments.createdAt),
asc(GuildsForumsComments.id),
);
- } catch {
+ } catch (error) {
+ publicReadFailure("guild.posts")(error);
+ postsFailed = true;
posts = [];
}
@@ -137,7 +149,9 @@ export default async function GuildForumThreadPage({
.from(User)
.where(inArray(User.id, authorIds))
: [];
- } catch {
+ } catch (error) {
+ publicReadFailure("guild.post-authors")(error);
+ authorsFailed = true;
users = [];
}
const usernameById = new Map(users.map((u) => [u.id, u.username]));
@@ -195,10 +209,17 @@ export default async function GuildForumThreadPage({
- {posts.length === 0 ? (
+ {authorsFailed && (
+
+ )}
+ {postsFailed ? (
+
+ ) : posts.length === 0 ? (
{t("postsEmpty")}
) : (