Merge home tools, control panel updates, and upstream changes - #1
Merged
Conversation
Wires up the long-dead "Directory" link in the Control Panel sidebar with a searchable, paginated citizen listing (username, role, citizen since date, online status). New public GET /member/directory endpoint exposes only safe fields, modeled on the existing admin-only searchUsers query and the getMemberInfoPublic field allowlist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires up two more dead Control Panel rows. "How Do I?" is a new consolidated help page porting the original archived content and button icons (converted to WebP for size), covering membership, 3D, chat, homes, clubs, message boards, jobs, security, and privacy. "Feedback" reuses the existing per-place MessageBoard component, mirroring how the original site's feedback board worked. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The page content now scrolls in its own inner container (matching the admin.vue/CreatorPage.vue pattern) instead of growing the outer page scrollbar and pushing the Control Panel sidebar off-frame. Topic icons moved from tiny inline badges next to each heading to standalone 2x-scaled banners above each section, rendered with pixelated scaling to stay crisp. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The archived legacy nav-button GIFs (converted to WebP) were only 116x16px pixel art and unreadable even scaled up. Swapped them for real text spans using the app's existing .btn-ui-inline style (dark navy background, clean Arial, already used for POST/MANAGE/LOGOUT elsewhere), which is crisp at any size and matches the rest of the app. Also added the same button treatment to Placing Objects and Privacy Policy, which never had an icon in the original archive. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- How Do I? now opens as an in-app modal (like My Info/Citizens Online) instead of navigating away, so it doesn't interrupt the in-world chat/3D session behind it. - Feedback now opens in a popup window via the app's existing openWindow() pattern (same mechanism used for Messages/Inbox elsewhere), for the same non-disruptive reason. MessageBoard.vue itself is untouched since it's reused across ~10 other call sites. - Corrected several sections that described non-existent or since-removed functionality: dropped Voice Chat/Mute a Voice (the revival is text-only), replaced with the real "Ignore" block feature; rewrote Alert Security to describe the actual live flow (Citizens Online -> Jail inbox message) since the real-time alert button is currently commented out in the app; clarified backpack access via the chat sidebar's Next button. - Added a hero header to How Do I? featuring Mina (the site's own mascot), background-removed from the existing Plaza splash art and composited onto a dark-navy gradient banner matching the app's palette. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Matches the same non-disruptive pattern as My Info/Citizens Online: Directory has no route params and no shared-component dependencies, so it wraps trivially in the existing Modal component, keeping the in-world chat/3D session visible and running behind it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts: # spa/src/App.vue # spa/src/routes.ts
…, Reset Home owners can now restrict chat participation to up to 8 guests via a new Home Chat Guest role (enforced server-side in the socket layer, muted users' chat silently dropped with a private system notice), edit a free-text home description shown to visitors, upload a profile image (server-side resize to 200x200 + WebP via sharp), and reset a home to a new free lot within its block (atomically freeing the old lot, clearing name/description/image/chat-access/3D-design, with wallet refund). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Enforce home chat access restrictions server-side in the REST message endpoint, not just the Socket.IO layer, closing a bypass where a muted user could post directly via the API. - Make home lot claims atomic (conditional UPDATE guarding on an unclaimed lot) in createHome/moveHome/resetHome, closing a race where two concurrent requests could both pass the availability check and one claim would silently clobber the other, orphaning a home's location. - Fix a null-pointer crash + infinite loading spinner on HomeUpdatePage for visitors who don't own a home yet. - Enforce the 1000-character home description limit server-side, not just client-side. - Batch the directory's per-citizen home-ownership check into a single query instead of one query per citizen. - Fix directory error responses serializing to an empty object. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- createHome rolls back its just-created place row when the atomic lot claim loses the race, instead of leaving an orphaned home-typed place with no map location that permanently blocks that member from ever creating a home again. - Wrap the new chat-access check in addMessage in a try/catch matching every sibling branch in the handler, so a transient DB error there returns the standard 400 instead of hanging the request. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gave all of security the ability to enter all clubs. Added account removal function to admin panel.
Updated access to allow admin to drop objects in public places.
Fix access bug for flea market and blackmarket staff.
- CLAUDE.md documents project layout, coding standards (per-project quote style, indent, line length, trailing commas, EOF newline), the Docker dev environment, build/lint commands, and known gotchas (duplicate-role admin grants, JWT auth, place hierarchy). AGENTS.md points to it. - Chat.vue: wrap the 106-char chat-access v-if to <=100 (max-len). - HomeUpdatePage.vue: convert the getLinks array to double quotes so the file is consistent with the SPA's eslint (quotes: double). Only our added/adjacent lines are touched; pre-existing legacy eslint debt in these files is intentionally left alone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
spa/assets/homes-uploads holds user-uploaded home images (runtime data); ignore it like the existing spa/assets/object upload dir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Finding A — Fragile admin role resolution:
The DB carried duplicate `role` rows (every name existed twice, ids 1-113 and
114-192) because 05-roles/06-donor seeds insert unconditionally, so re-running the
seed suite duplicated all 78 seeded roles. RoleRepository resolved each name to
whichever id was iterated last, so `roleMap.Admin` silently depended on the higher
duplicate id (114). Adding another duplicate would move who is treated as Admin.
- New migration 20260717120000_dedupe_role_rows: keeps the lowest id per name as
canonical, repoints role_assignment onto it (dropping rows that would become exact
duplicates), deletes the redundant role rows, and adds a UNIQUE(name) index so a
duplicate can never be inserted again. Transactional DML; safe/no-op on databases
without duplicates; down() drops only the guard index (consolidation is not undone).
- RoleRepository.populateRoleMap now resolves each name to the lowest id
deterministically (defense in depth), so resolution can never flip on row order.
Finding B — can_admin returned 400 for auth failures:
place.controller.canAdmin decoded the JWT inside a try/catch that returned 400 with
the raw error object for missing/invalid/expired tokens, and the place lookup ran
before auth. Now: missing token -> 401, invalid/expired token -> 401, nonexistent
place -> controlled 404, unexpected failure -> generic 500 (no error object, no token
echoed). Authenticated non-admins get the intended {result:false}; admins {result:true}.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DJAscendance
added a commit
that referenced
this pull request
Jul 27, 2026
The beta database records 20260717120000_dedupe_role_rows.ts as applied (migrations id 39, batch 5) - it came across in the mysqldump of the local dev database, which had local-testing applied. The file itself never made it onto beta, because beta was built from upstream/master plus PRs CybertownRevival#409-CybertownRevival#412 and fork PR #5, none of which carry it. Knex validates that every completed migration still exists on disk, so beta could not run ANY migration command: The migration directory is corrupt, the following files are missing: 20260717120000_dedupe_role_rows.ts Both migrate:list and migrate:latest fail. Nothing had broken yet only because compose.beta.yml's ct-api command does not migrate at start-up. Restored byte-for-byte from its single authoritative blob (0eb926a - identical at PR #1 head 4616466, at the fork merge a1420a2, and on fork master). Not renamed, not edited, and NOT re-run: beta already records it as applied, and a repaired migrate:latest against a database in beta's migration state reports "Already up to date" with the migrations row count unchanged. Verified against a disposable MySQL 5.7 seeded to beta's migration state: without the file -> migrate:list and migrate:latest both error as above with the file -> migrate:list succeeds, "No Pending Migration files Found"
DJAscendance
added a commit
that referenced
this pull request
Jul 27, 2026
Reimplements the Home Information behaviour that fork PR #1 specified, on the CURRENT beta architecture. The historical files were read as a behavioural spec, not ported: local-testing's Information.vue and home.service.ts also carry its pre-PR-CybertownRevival#410 image handling and pre-PR-CybertownRevival#412 socket code, none of which may come back. Server: - HomeService.getHomeInformation(placeId) returns '' for a missing place or a place that is not a home, so the route cannot be used to read a club's or block's description. - HomeService.updateHomeInformation(memberId, text) resolves the target via updateHomeByMemberId, whose WHERE is scoped to type='home' AND member_id=?. Ownership is therefore structural: the request body carries only the text, so there is no member id, home id or username for a caller to substitute. - Validation is server-side and independent of the textarea's maxlength: type (non-strings rejected rather than reaching .match()/the database), length against HomeService.INFORMATION_MAX_LENGTH, and badwords. An empty description remains a valid, intentional value. - GET /home/information/:placeId is registered ABOVE the '/:username' catch-all, which would otherwise match 'information' as a username. Client: - New HomeUpdateInformationPage.vue + /home/update/information route, built on the same conventions as the existing HomeUpdateHomePage. - The Update menu's Information tile and HomeTools' Information control were both inert (link: '' and a non-interactive <span>); both now resolve. - Information.vue gains a home branch rendering the description through Vue text interpolation - never v-html - with white-space: pre-wrap for the owner's line breaks and a fallback for an empty description. Verified: api tsc --noEmit clean; api eslint 0 errors (warnings are the pre-existing no-explicit-any style shared with the existing specs); SPA eslint error count unchanged at 29 vs the beta baseline, with both new/edited home files clean; SPA production build DONE; api suite 18 passed / 4 failed vs a beta baseline of 16 passed / 4 failed - the SAME four suites (role.repository, wallet.service, club.service, member.service), which fail on unmodified beta for want of a reachable MySQL. No new failures.
DJAscendance
added a commit
that referenced
this pull request
Jul 27, 2026
Reimplements the Reset behaviour fork PR #1 specified, on the current beta architecture. The historical implementation is reference only: it was not transactional, it refunded on every request, and its image handling predates PR CybertownRevival#410 entirely. One transaction covers ownership, both map locations, the home metadata, the design removal, the refund and the image-state transition, so a failure anywhere leaves the home, lot, wallet, design and image state exactly as they were. Lock order is fixed and request-independent - home, then the affected map_location rows in PRIMARY KEY order, then wallet - so two resets swapping lots in opposite directions wait for each other instead of deadlocking. Conditional claim (MapLocationRepository.claimLocationWithin): freeness is proved by the UPDATE's own WHERE and reported through the affected-row count. A prior read plus an unconditional update would leave a window for a concurrent claim of the same lot, which is how two homes end up on one lot. Exactly-once refund with NO schema change. The decision is made from home_design_id read under the home's row lock, and the same transaction clears that column and writes the ledger row - so the home row's own state is the idempotency token. A duplicate submit, a client retry, a transaction retry or a second concurrent reset all observe an already-null design and refund nothing; a crash mid-flight rolls back, leaving the design intact so a retry is correct. This needed createHomeRefundTransactionWithin on TransactionRepository, since every existing helper opens its own transaction and so cannot be atomic with the design clear. That new path also takes FOR UPDATE on the wallet before reading its balance; the pre-existing helpers' read-then-write is left untouched. Refund amount and the Champion/championhome zero rule are taken verbatim from updateHome - the two refund paths must agree - and read from HomeDesignRepository, never from the request. Image state moves to ('none', null) under the same row lock every other image mutation takes, which is exactly the state home.service.ts already documents for "remove / reset". Cleanup afterwards reuses the existing helpers unchanged: the private file is addressed by the exact revision captured under the lock, and the public file only through the state-guarded re-lock, so an upload or approval that committed in the gap keeps its image. No wildcard deletion, and a failed unlink cannot invalidate a committed reset. Destructive-action handling: POST-only route, and the SPA arms then requires a separately-worded confirmation naming the home and target lot before it posts. No GET, page load or navigation can mutate state. Occupied lots render as icons rather than buttons and cannot be selected; a failed reset clears the selection and reloads the map, since the usual cause is someone claiming the lot first. The block map background honours PR CybertownRevival#411's map_background_index. Chat-access clearing is intentionally NOT here - beta has no guest-list representation yet (zero Home Chat Guest assignments exist), so it lands with the Chat Access work in Phase 5 rather than as a silent no-op stub. Verified: 26 new reset tests pass; api suite 19 passed / 4 failed vs a beta baseline of 16 / 4 - the SAME four suites, which fail on unmodified beta for want of a reachable MySQL. tsc --noEmit clean. SPA production build DONE, SPA suites 29/29, 13/13, 11/11, 5/5. Lint: both new pages clean; the touched-file error count is unchanged at 29 against the beta baseline.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This merges the
local-testingintegration branch intomasterafter bringing it up to date with the latest upstream CTR changes.What changed
CLAUDE.mdandAGENTS.mdwith repository setup, coding standards, and local development notes.can_adminauthentication response behavior.Verification