Skip to content

Merge home tools, control panel updates, and upstream changes - #1

Merged
DJAscendance merged 17 commits into
masterfrom
local-testing
Jul 17, 2026
Merged

Merge home tools, control panel updates, and upstream changes#1
DJAscendance merged 17 commits into
masterfrom
local-testing

Conversation

@DJAscendance

@DJAscendance DJAscendance commented Jul 17, 2026

Copy link
Copy Markdown
Owner

This merges the local-testing integration branch into master after bringing it up to date with the latest upstream CTR changes.

What changed

  • Added home-management tools for chat access, information, image uploads, and resetting a home.
  • Added the Citizen Directory and “How Do I?” views to the Control Panel.
  • Brought in the latest upstream club-access, drop-object, and chat updates.
  • Added CLAUDE.md and AGENTS.md with repository setup, coding standards, and local development notes.
  • Added the home image column and runtime upload directory.
  • Consolidated duplicate role rows, preserved existing assignments, and enforced unique role names.
  • Made role lookup deterministic and corrected the can_admin authentication response behavior.

Verification

  • API TypeScript compilation passed.
  • SPA production build passed.
  • Database migrations were tested against a populated database.
  • Home tools, Control Panel views, and authorization behavior were checked in the running stack.

DJAscendance and others added 17 commits July 8, 2026 21:13
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
DJAscendance merged commit a1420a2 into master Jul 17, 2026
@DJAscendance DJAscendance changed the title Home customization, How Do I?/Citizen Directory modals + upstream sync Merge home tools, control panel updates, and upstream changes Jul 17, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants