# Shared Area Guide content — same-codebase, shared-database plan

**Date:** 2026-09-13, **last checked 2026-09-14.** **Status:** registry and map/media
implementation is available locally, with an audit-remediation pass recorded in the
[validation record](/docs/modules_handbook/shared/project-catalogue/area-guide-map/validation.md#remediation-checkpoint--2026-09-14);
integrated acceptance and production migration are pending. See the
[map handbook](/docs/modules_handbook/shared/project-catalogue/area-guide-map/readMe.md),
[registry handbook](/docs/modules_handbook/shared/project-catalogue/area-guide-registry/readMe.md)
and [shared media contract](/docs/modules_handbook/shared/media/readMe.md) for the current
code, settings, ownership, durable cleanup, rollout steps and limits. The sections below
retain the design contract; the requirements checklist records actual verification.
The user clarified that production is `propertylabglobal.com`, participating sites use
this same codebase and will connect to the same live master project database. They prefer
editing inside the current site's backend. This is the deployment contract for the new
feature, not an assertion that every deployed environment has already been inspected.
The complete checklist is [R1–R16](/docs/modules_handbook/main/area-guide/catalogue-map-requirements.md).

## What it does

Each site's Manage portal renders the same Area Guide editor from this codebase. Authorized
edits write to shared Area Guide tables in the master database. Models, panoramas, video,
covers, placements, regions and building annotations have one shared source of truth.
Other sites load that same content. A separate project folder, central-editor redirect,
new content gateway or cross-site administrator login is not required by this design.

Malaysia is the first content rollout. The new map/model/panorama/video functionality
must be country-independent. Full project-detail tabs use the same components in the
standalone page and drawer. Panoramas remain administrator-only; admins manually trace
a building's boundary and bind a published new-launch or subsale project, making the
whole polygon clickable.

## How it works

### Current-site editor, shared records and shared files

```mermaid
flowchart TD
    A[Site A Manage editor] --> D[Shared master database]
    B[Site B Manage editor] --> D
    A --> M[MediaService and shared file storage]
    B --> M
    D --> VA[Site A Area Guide]
    D --> VB[Site B Area Guide]
    M --> VA
    M --> VB
```

All sites render the same editor and enforce the acting user's local permissions. The
shared records contain the result, so a model moved from site A is read at its new
position when site B next reloads/revalidates. There is no per-site placement copy to
synchronize. Shared data does not change which countries/projects a site is allowed to
show or which viewers may access admin-only panoramas.

The editor must clearly label the scope of a save: shared-content changes affect all
participating sites. Show the source site and administrator in the change history.
This is part of the existing edit/save flow, not a separate central approval workflow.

The database stores metadata and associations; the shared storage holds actual model,
image and video bytes. A common database alone does not share files stored on one site's
local disk. The stored disk/path must resolve to the same object on every deployment.
Use a dedicated shared media disk/configuration if site-local uploads use different buckets;
do not move customer uploads to a different bucket as a side effect of this feature.

Shared data revisions must invalidate per-site cached manifests. Revalidate a shared
revision on load rather than assuming that clearing site A's cache clears site B's cache.
Already-open browser push updates are not required for the first release. Include an
optimistic revision check so two administrators cannot silently overwrite each other's edits.

A shared codebase means one implementation to maintain. If sites deploy separately,
component changes still require releasing that code version to each deployment; database
content changes do not require a code release.

### Database ownership and access

The new shared tables belong to the master database and use catalogue-family migrations
in `database/migrations/catalogue/`. Reuse the existing `catalogue` connection where it
resolves to the agreed live master. The application's default database continues to own
each site's users, leads, bookings, visits and other site-local data.

`config/database.php` can collapse `catalogue` onto a local database with
`CATALOGUE_USE_DEFAULT_CONNECTION`. The shared-content feature must validate its intended
connection at setup/deployment; it must not silently create a writable local content copy
on a participating production site. Isolated tests use explicit fixture connections and
must never reach the production master. Local Area Guide settings now point at the new
`petav3_area_guide` development database; the existing master credentials/project connection
and remote records were not changed. Tests use the separate `petav3_area_guide_testing`.

Existing rules require deliberate integration:

- `CatalogueFederationService::masterIsEditable()` defaults to allowing catalogue edits
  from `propertylabglobal.com`; it is not proof that every other host may already edit.
  Add an explicit shared Area Guide permission/allowed-writer policy for participating
  hosts as needed. Do not turn off unrelated catalogue protections globally.
- A SELECT-only database credential cannot save shared content. Sites authorized to edit
  need write grants for the required shared content/media tables. Read-only sites can
  remain readers. The existing `catalogue_master` mirror connection is explicitly read-only
  and is not a write connection to repurpose.
- Repositories use transactions on the connection being written. Do not change the whole
  application's default connection or assume default `DB::transaction()` protects a
  master write. Use stable catalogue UUIDs (Universally Unique Identifiers) for identities,
  local integer relationship keys as appropriate, and no cross-database joins.
- New content must be excluded from scraper/reseed/whole-table replacement operations.
  Existing mirror/import packages do not automatically support newly added tables or media
  references. A later copied-catalogue deployment needs an explicit compatibility design;
  building that alternative is not a prerequisite for the user's live-master topology.
- Administrator identity is site-scoped. `RecordsBlame` uses `Auth::id()`, and user 10 on
  two sites can be two different people. Shared audit data must record the site identity
  and administrator UUID/display snapshot alongside compatible blame fields. Do not
  resolve another site's creator through the current site's users table.

### MediaService remains the common storage mechanism

Reuse [MediaService](/docs/modules_handbook/shared/media/readMe.md) for storage, reads,
replacement and cleanup. Add explicit server-controlled shared ownership/connection
support only where required; the request cannot select an arbitrary database connection.

This was the gap list when the plan was written; every item has since been implemented and is
kept here as the reason the work exists:

- `MediaRepository::create()` wrote default `Media` inside a default transaction.
- `storeFromPath()` did not inherit an owner's connection and resolved its outbox locally.
- `MediaCleanupOutbox` and `CleanupMediaObject` also used default-connection records.
- `MasterMedia` already represented catalogue-owned stored files, but passing it to an
  otherwise local cleanup retry was not complete connection-aware support.

Extend the existing path coherently across MediaRepository, storage ownership, outbox,
queue payload and cleanup lookup. Preserve local consumers as the default behavior.
Scope cleanup identity by its owning connection as well as task ID, so a worker cannot
act on the same numeric task/media ID in another site's database. Only one worker should
claim a given cleanup task, using the existing locking/lease pattern on the correct database.
Replacement remains store-new → verify → attach → cleanup-old; a failed upload retains
previously published content.

**As implemented (2026-09-14).** A cleanup row moves through `pending` → `writing` (a stream
upload owns its intent) or `processing` (a worker or a synchronous delete owns the task), each
with a fresh owner token and a configurable lease — `media.writing_lease_minutes` (default 60,
which must exceed the slowest realistic upload) and `media.cleanup_claim_minutes` (default 15).
The claim, the storage call and the completion are three steps, so the remote delete runs with
no open transaction and no row lock; the completing write verifies the claim is still its own
before deleting anything. The hourly scanner hands an expired lease of either kind back to
`pending`, and a writer whose intent was reclaimed after its upload succeeded removes its own
object. A queued task whose database identity no longer matches fails permanently instead of
retrying, and a stored media row that lives in another schema (the catalogue master's) is
removed by its own repository once the object's cleanup intent is durable — the retry job only
ever searches its own scope's media table. The
[Media handbook](/docs/modules_handbook/shared/media/readMe.md) owns this contract.

MediaService does not validate file formats, convert model formats or transcode videos.
Those responsibilities remain with dedicated Form Requests and feature processing.
Prefer streaming large temporary files through `storeFromPath()` over buffering them
through `storeUpload()`. Proposed formats and configurable limits must be documented and
tested with representative files; a file-size questionnaire does not block the architecture.

### Database-backed Areas and country-independent behavior

Reuse existing country identity by country code. Store structured country presentation,
region and Area records, not one opaque copy of the entire JavaScript registry.

| Shared content data | Code/configuration or versioned assets |
|---|---|
| Country/region/area keys, hierarchy, order and availability | Mapbox credentials and renderer implementation |
| Names, translations, descriptions, stories and facts | Generic scoring and camera interaction algorithms |
| Centre, bounds, initial zoom and country presentation profile | Boundary geometry assets, referenced/versioned by records |
| Per-area walk availability, radius and station limit | General graphics thresholds and implementation constants |
| Media references, models, placements and polygon annotations | Country-specific project-detail/analysis adapters |
| Publication, audience, provenance and revision | Shared Vue components and authorization logic |

The shared registry must serve CoursesController, AreaGuideController, ChatRequest,
RenderAreaNarration, AreaStationFinder, AreaGuidePanel, tutorial area options and Road
links. Existing sources include `config/area_guide.php`, `config/area_guide_game.php`,
and the Malaysia/Hong Kong/UAE JavaScript registries.

Import by stable key, with dry-run and differences. Re-running an import must not duplicate
records or overwrite administrator edits. Preserve existing `area_key` values and aliases
used by tutorials, narration, visits and links. Import does not invoke paid narration,
chat or tutorial generation. Preserve site-local learning history.

The renderer must replace first-region-only selection and peninsula-specific bounds with
data-driven profiles. Existing authored Malaysia content covers the current western areas;
new areas require actual content and geometry. Prove a second region and country can use
the same new functionality with fixtures. Existing Malaysia-specific chat, tutorial prompts
and currency assumptions still need parameterization or explicit capability limits before
claiming those older learning/analysis features support another country.

### Panorama audience and building interaction

> **Revised 2026-09-15.** The traced polygon below was replaced, by user decision, with a
> click-placed MARKER (a ring with the building's name; movable and resizable in edit mode)
> stored as a yaw/pitch point + size in `area_guide_hotspots`. Later the same day a marker
> gained a THIRD thing it can link — a location video (`video_asset_id`), the same asset the map
> places, opened on its own full-screen page — and the project/building/video link rule moved into
> one shared class, `Src\AreaGuide\Services\AreaGuideLinks`. Everything about binding, audience,
> image revisions and cross-site preservation still holds. Current behaviour:
> [area-guide-map/readMe.md](/docs/modules_handbook/shared/project-catalogue/area-guide-map/readMe.md).

Administrators trace a polygon, search published catalogue projects (new launches and
subsale), and bind the selected project. Save spherical vertices, image revision and
project UUID. Provide edit/remove/undo, visible selection, seam handling and deterministic
overlap behavior. Replacing the photo requires annotation revalidation.

Admin-only applies to the entry, page, preview, original bytes and annotations. The existing
`admin` middleware checks `isManageUser()` and admits other Manage roles, so the intended
administrator audience needs an explicit policy. Use authenticated media delivery;
public-approved bake routes and public immutable caching do not implement this policy.
Publication and audience remain separate, allowing a future member rollout without
rebuilding content. Recheck project publication and per-site visibility when serving a
model, polygon or direct detail request.

### Acceptance additions

- Edit through site A's local editor, reload site B, and verify the same revision, model
  placement and media. Repeat after replacement and unpublish, with separate caches.
- Prove shared tables use the master while users, leads, bookings and visits stay local.
- Test site-scoped permissions, two administrators with the same local integer ID, and
  concurrent edits. A read-only site must not claim a save succeeded.
- Test shared and local media tasks with colliding integer IDs; verify correct ownership,
  single-worker claims, safe retries and no damage to existing local MediaService consumers.
- Deny direct panorama access for anonymous users, members and unauthorized Manage roles.
- Verify idempotent import, old area links/history, a second region and a second country.
- Prove complete shared detail tabs work in both hosts with independent navigation state.
- Use real models, panoramas and videos for rendering, polygon hit tests, seeking and mobile
  checks, then run the relevant regression tests and production builds.

The registry/editor portion has automated evidence: schema/import, permission gates, actor
identity, optimistic revisions, lightweight/full consumers, authoritative empty data and
country map profiles. The original 3 countries, 6 regions and 24 areas were imported into
the new local development database, and an idempotent rerun preserved all records. A local
authorized route probe returns the editor and its data. Member histories, Area Tutorials
and narration audio media remain site-local. There is no registry cache in this slice.

Shared upload/outbox ownership, project markers, the complete Malaysian detail component,
model placement, panorama polygons and location video are now implemented locally. Their
focused automated suites pass; current browser acceptance and final regression results are
tracked in the checklist. Representative original assets and actual multi-deployment
propagation remain pending. No migration or write was performed on the real master. See the
[checklist evidence](/docs/modules_handbook/main/area-guide/catalogue-map-requirements.md)
before treating any part of the full plan as verified.

Two plan-level points the 2026-09-14 remediation settled, and one it did not:

- **Per-site visibility is rechecked on every serve**, as this plan required, and the detail
  route now asks the full question — published **and** listed on this site — rather than
  publication alone. That has a deployment consequence worth planning for: a site switched to
  explicit site-listing mode would 404 every project detail page until its listing rows exist,
  and no administrator screen writes those rows yet.
- **Reader gates are authorization, not withheld props.** Each guide reader endpoint applies the
  same server-side check as the tab, so a locked or not-yet-eligible member is refused rather
  than merely shown no control.
- **Not settled:** the whole backend is still uncommitted, and the cross-deployment acceptance
  in the list above has no automated substitute. Nothing in the remediation ran against a real
  second deployment, a real bucket or a browser.

## Related files

- [Implemented registry](/docs/modules_handbook/shared/project-catalogue/area-guide-registry/readMe.md)
- [Implemented map, uploads and rollout](/docs/modules_handbook/shared/project-catalogue/area-guide-map/readMe.md)
- [Requirements checklist](/docs/modules_handbook/main/area-guide/catalogue-map-requirements.md)
- [Catalogue map](/docs/modules_handbook/shared/project-catalogue/start-here.md)
- [Database ownership](/docs/modules_handbook/shared/project-catalogue/databases-and-distribution.md)
- [Media handbook](/docs/modules_handbook/shared/media/readMe.md)
- [Connections](/config/database.php), [site edit configuration](/config/site.php)
- [Catalogue edit gates](/app/Services/Property/CatalogueFederationService.php)
- [MasterMedia](/src/Common/MasterMedia.php), [MediaRepository](/src/Common/Repositories/MediaRepository.php)
- [MediaService](/src/Common/Services/MediaService.php), [cleanup outbox](/src/Common/Services/MediaCleanupOutbox.php)
- [Cleanup job](/app/Jobs/Media/CleanupMediaObject.php)
- [Manage middleware](/app/Http/Middleware/EnsureUserIsAdmin.php)
