Skip to content

Commit 2b60371

Browse files
committed
Finalize DB idempotency, add Owner DB status, and start R2 asset storage - PR_26167_208-210-db-to-storage-stack
1 parent 6021f21 commit 2b60371

19 files changed

Lines changed: 8386 additions & 888 deletions

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,11 @@ GAMEFOUNDRY_SUPABASE_ANON_KEY=
1818
# Do not expose these values to browser JavaScript, HTML, reports, or screenshots.
1919
GAMEFOUNDRY_SUPABASE_SERVICE_ROLE_KEY=
2020
GAMEFOUNDRY_DATABASE_URL=
21+
22+
# Server-only project asset storage configuration.
23+
# Browser uploads must go through the server API and must not receive these secrets.
24+
GAMEFOUNDRY_STORAGE_ENDPOINT=
25+
GAMEFOUNDRY_STORAGE_ACCESS_KEY_ID=
26+
GAMEFOUNDRY_STORAGE_SECRET_ACCESS_KEY=
27+
GAMEFOUNDRY_STORAGE_BUCKET=
28+
GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX=

.vscode/settings.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,29 @@
8585
"javascript.suggest.autoImports": true,
8686
"typescript.suggest.autoImports": true,
8787
"html.suggest.angular1": true,
88-
"html.suggest.ionic": true
88+
"html.suggest.ionic": true,
89+
"sqltools.connections": [
90+
{
91+
"ssh": "Disabled",
92+
"previewLimit": 50,
93+
"server": "192.168.2.5",
94+
"port": 55433,
95+
"driver": "PostgreSQL",
96+
"name": "GFS - IST",
97+
"database": "gamefoundry_ist",
98+
"username": "postgres",
99+
"password": "postgres"
100+
},
101+
{
102+
"ssh": "Disabled",
103+
"previewLimit": 50,
104+
"server": "192.168.2.5",
105+
"port": 55431,
106+
"driver": "PostgreSQL",
107+
"name": "GFS - DEV",
108+
"database": "gamefoundry_dev",
109+
"username": "postgres",
110+
"password": "postgres"
111+
}
112+
]
89113
}

assets/theme-v2/js/owner-operations.js

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,28 +49,22 @@ class OwnerOperationsController {
4949
});
5050
}
5151

52-
renderDatabaseOperations(operations = []) {
53-
const rows = Array.isArray(operations) ? operations : [];
52+
renderDatabaseStatus(databaseStatus = {}) {
53+
const migrationCounts = databaseStatus.migrationCounts || {};
54+
const lastMigration = databaseStatus.lastMigration || {};
55+
const rows = [
56+
["Connection Configured", databaseStatus.configured === true ? "PASS" : "WARN", databaseStatus.configured === true ? "yes" : "no"],
57+
["Database Host", databaseStatus.hostStatus || "WARN", databaseStatus.host || "not configured"],
58+
["Database Port", databaseStatus.portStatus || "WARN", databaseStatus.port ? String(databaseStatus.port) : "not configured"],
59+
["Database Name", databaseStatus.databaseNameStatus || "WARN", databaseStatus.databaseName || "not configured"],
60+
["SSL Mode", databaseStatus.sslModeStatus || "WARN", databaseStatus.sslMode || "not configured"],
61+
["Migration Counts", databaseStatus.migrationStatus || "WARN", `DDL=${migrationCounts.DDL || 0}; DML=${migrationCounts.DML || 0}`],
62+
["Last Migration", databaseStatus.lastMigrationStatus || "WARN", lastMigration.name && lastMigration.appliedAt ? `${lastMigration.type || "unknown"} ${lastMigration.name} at ${lastMigration.appliedAt}` : "not recorded"],
63+
];
5464
this.databaseStatusRows.replaceChildren();
55-
if (!rows.length) {
56-
const emptyRow = document.createElement("tr");
57-
["Database Operations", "WARN", "read-only", "unavailable", "No database operation status was returned."].forEach((value) => {
58-
const cell = document.createElement("td");
59-
cell.textContent = value;
60-
emptyRow.append(cell);
61-
});
62-
this.databaseStatusRows.append(emptyRow);
63-
return;
64-
}
65-
rows.forEach((operation) => {
65+
rows.forEach((row) => {
6666
const tableRow = document.createElement("tr");
67-
[
68-
operation.label || operation.id || "Database Operation",
69-
operation.status || "WARN",
70-
operation.mode || "read-only",
71-
operation.command || "not exposed",
72-
operation.message || "No database operation message returned.",
73-
].forEach((value) => {
67+
row.forEach((value) => {
7468
const cell = document.createElement("td");
7569
cell.textContent = value;
7670
tableRow.append(cell);
@@ -98,7 +92,7 @@ class OwnerOperationsController {
9892
try {
9993
const payload = readOwnerOperationsStatus();
10094
this.renderConnectionSummary(payload.connectionSummary || {});
101-
this.renderDatabaseOperations(payload.databaseOperations || []);
95+
this.renderDatabaseStatus(payload.databaseStatus || {});
10296
this.setStatus(payload.status || "PASS", payload.message || "Owner Operations loaded.");
10397
} catch (error) {
10498
this.setStatus("FAIL", error instanceof Error ? error.message : "Owner Operations are unavailable.");
@@ -109,7 +103,7 @@ class OwnerOperationsController {
109103
try {
110104
const result = validateOwnerOperationsConnection();
111105
this.renderConnectionSummary(result.connectionSummary || {});
112-
this.renderDatabaseOperations(result.databaseOperations || []);
106+
this.renderDatabaseStatus(result.databaseStatus || {});
113107
this.appendResult(result);
114108
this.setStatus(result.status || "PASS", result.message || "Connection validation finished.");
115109
} catch (error) {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# PR_26167_208-database-idempotency-final-check
2+
3+
## Branch Validation
4+
- Current branch: `main`
5+
- Expected branch: `main`
6+
- Local branches found: `main`
7+
- Result: PASS
8+
9+
## Scope
10+
- Impacted lane: runtime database migration apply lane.
11+
- Full samples smoke: SKIP. This PR validates database migration idempotency only and does not touch sample runtime behavior.
12+
- R2/storage work: SKIP. Storage is explicitly out of scope for this PR.
13+
14+
## Validation Evidence
15+
- PASS - Read `docs_build/dev/PROJECT_INSTRUCTIONS.md` before execution.
16+
- PASS - `node .\scripts\validate-runtime-connections.mjs`
17+
- Overall Result: PASS
18+
- `.env` loaded: 5 key(s)
19+
- Database connection: `host=192.168.2.5; port=55431; database=gamefoundry_dev`
20+
- Database SSL mode: `disable`
21+
- PASS - First `node .\scripts\apply-database-ddl.mjs`
22+
- processed=15
23+
- applied=0
24+
- skipped=15
25+
- PASS - First `node .\scripts\apply-database-dml.mjs`
26+
- processed=15
27+
- applied=0
28+
- skipped=15
29+
- PASS - Second `node .\scripts\apply-database-ddl.mjs`
30+
- processed=15
31+
- applied=0
32+
- skipped=15
33+
- PASS - Second `node .\scripts\apply-database-dml.mjs`
34+
- processed=15
35+
- applied=0
36+
- skipped=15
37+
- PASS - Read-only `schema_migrations` count query:
38+
- DDL count: 15
39+
- DML count: 15
40+
- Last migration: `DML support-tickets.sql` at `2026-06-17 01:07:30.540517+00`
41+
- PASS - Read-only `platform_settings` migration-key check:
42+
- migration-like `platform_settings` rows: 0
43+
44+
## Idempotency Evidence
45+
- DDL apply lane processed all 15 DDL migrations and applied 0 on both requested runs.
46+
- DML apply lane processed all 15 DML migrations and applied 0 on both requested runs.
47+
- `schema_migrations` remained at 15 DDL records and 15 DML records after the repeated apply.
48+
49+
## Migration Authority Evidence
50+
- PASS - `scripts/apply-database-ddl.mjs` delegates DDL apply to `scripts/database-migration-runner.mjs`.
51+
- PASS - `scripts/apply-database-dml.mjs` delegates DML apply to `scripts/database-migration-runner.mjs`.
52+
- PASS - `scripts/database-migration-runner.mjs` reads existing migration state from `schema_migrations`.
53+
- PASS - `scripts/database-migration-runner.mjs` records applied migrations only in `schema_migrations`.
54+
- PASS - No migration-tracking state was found in `platform_settings`.
55+
56+
## Requirement Checklist
57+
- PASS - Read `docs_build/dev/PROJECT_INSTRUCTIONS.md` first.
58+
- PASS - HARD STOP guard passed because current branch is `main`.
59+
- PASS - Verified DB migration apply lane is idempotent.
60+
- PASS - Used `.env` only for validation/apply commands.
61+
- PASS - Confirmed `schema_migrations` remains the only DDL/DML apply authority.
62+
- PASS - Confirmed `platform_settings` is not used for migration tracking.
63+
- PASS - No R2/storage work was added in this PR.
64+
- PASS - Ran `node .\scripts\validate-runtime-connections.mjs`.
65+
- PASS - Ran `node .\scripts\apply-database-ddl.mjs`.
66+
- PASS - Ran `node .\scripts\apply-database-dml.mjs`.
67+
- PASS - Ran apply again and confirmed 0 new DDL/DML migrations.
68+
- PASS - Confirmed `schema_migrations` remains 15 DDL and 15 DML.
69+
- PASS - Did not run full samples smoke.
70+
71+
## Required Reports
72+
- PASS - `docs_build/dev/reports/codex_review.diff`
73+
- PASS - `docs_build/dev/reports/codex_changed_files.txt`
74+
- PASS - `docs_build/dev/reports/PR_26167_208-database-idempotency-final-check.md`
75+
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# PR_26167_209-owner-database-status-surface
2+
3+
## Branch Validation
4+
- Current branch: `main`
5+
- Expected branch: `main`
6+
- Local branches found: `main`
7+
- Result: PASS
8+
9+
## Scope
10+
- Impacted lane: runtime Owner Operations page/API.
11+
- Full samples smoke: SKIP. This PR only changes Owner Operations status rendering and targeted server API payload behavior.
12+
- Destructive operations: SKIP. The browser does not execute destructive database operations.
13+
14+
## Implementation Summary
15+
- Added a safe Owner-only database status payload from the local server API.
16+
- Updated Owner Operations to render only:
17+
- connection configured
18+
- database host
19+
- database port
20+
- database name
21+
- SSL mode
22+
- migration counts by type
23+
- last migration name/date
24+
- Replaced the previous database operations command/status table with a status-only database table.
25+
- Kept `.env` editing and destructive operation execution out of the browser surface.
26+
27+
## Validation Evidence
28+
- PASS - Read `docs_build/dev/PROJECT_INSTRUCTIONS.md` before execution.
29+
- PASS - `node --check assets/theme-v2/js/owner-operations.js`
30+
- PASS - `node --check src/dev-runtime/server/local-api-router.mjs`
31+
- PASS - `node --check tests/playwright/tools/AdminPlatformToolsWireframes.spec.mjs`
32+
- PASS - `npx playwright test tests/playwright/tools/AdminPlatformToolsWireframes.spec.mjs -g "Owner Operations"`
33+
- 2 tests passed.
34+
- DavidQ owner fixture saw the Owner Operations DB status table.
35+
- Signed-in non-owner fixture was blocked by `main[data-session-access-blocked='owner']`.
36+
- PASS - Playwright safe status assertions confirmed the DB status table shows:
37+
- `Connection Configured`
38+
- `Database Host`
39+
- `192.168.2.5`
40+
- `Database Port`
41+
- `55431`
42+
- `Database Name`
43+
- `gamefoundry_dev`
44+
- `SSL Mode`
45+
- `disable`
46+
- `Migration Counts`
47+
- `DDL=15; DML=15`
48+
- `Last Migration`
49+
- `DML support-tickets.sql at 2026-06-17 01:07:30.540517+00`
50+
- PASS - Playwright safe status assertions confirmed the DB status table does not show:
51+
- `postgres://`
52+
- `password`
53+
- `SERVICE_ROLE`
54+
- WARN - Advisory V8 coverage:
55+
- `(100%) assets/theme-v2/js/owner-operations.js - executed lines 121/121; executed functions 19/19`
56+
- `(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only`
57+
58+
## Owner Access Evidence
59+
- PASS - DavidQ owner session fixture used `displayName: "DavidQ"` and `roleSlugs: ["owner", "admin", "creator"]`.
60+
- PASS - DavidQ owner session saw the DB status table and safe database values.
61+
- PASS - Non-owner session fixture used `displayName: "User 1"` and `roleSlugs: ["creator"]`.
62+
- PASS - Non-owner session rendered `Owner role required` and removed `[data-owner-operations]`.
63+
64+
## Safe Status Evidence
65+
- PASS - The server API returns `databaseStatus`, not a full connection string.
66+
- PASS - The browser renderer displays only safe fields from `databaseStatus`.
67+
- PASS - Passwords, keys, and full connection strings are not rendered.
68+
- PASS - `.env` editing is not exposed by the Owner Operations page.
69+
- PASS - Destructive operations are not executed from the browser.
70+
71+
## Requirement Checklist
72+
- PASS - Read `docs_build/dev/PROJECT_INSTRUCTIONS.md` first.
73+
- PASS - HARD STOP guard passed because current branch is `main`.
74+
- PASS - Added Owner-only database status surface.
75+
- PASS - Showed safe DB status only.
76+
- PASS - Did not show passwords, keys, or full connection strings.
77+
- PASS - Did not allow `.env` editing.
78+
- PASS - Did not execute destructive operations from UI.
79+
- PASS - Owner Operations remains status-first.
80+
- PASS - Ran `node --check` for changed JS/MJS files.
81+
- PASS - Ran targeted Owner Operations Playwright.
82+
- PASS - Confirmed DavidQ can see DB status.
83+
- PASS - Confirmed non-owner users cannot see Owner DB status.
84+
- PASS - Did not run full samples smoke.
85+
86+
## Required Reports
87+
- PASS - `docs_build/dev/reports/codex_review.diff`
88+
- PASS - `docs_build/dev/reports/codex_changed_files.txt`
89+
- PASS - `docs_build/dev/reports/PR_26167_209-owner-database-status-surface.md`
90+
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# PR_26167_210-r2-project-assets-dev-storage
2+
3+
## Branch Validation
4+
- PASS - Current branch is `main`.
5+
- PASS - PROJECT_INSTRUCTIONS.md was read before implementation.
6+
- PASS - R2/storage lane started after DB lane validation passed:
7+
- PR_26167_208 confirmed idempotent DDL/DML apply with 0 new migrations on repeat apply and `schema_migrations` counts of 15 DDL / 15 DML.
8+
- PR_26167_209 owner database status surface validation passed for owner and non-owner access.
9+
10+
## Implementation Summary
11+
- Added the server-only storage env contract to `.env.example`.
12+
- Added `.env`-driven storage config loading and validation in `src/dev-runtime/storage/storage-config.mjs`.
13+
- Added R2/S3-compatible project asset storage in `src/dev-runtime/storage/r2-project-asset-storage.mjs`.
14+
- Wired the local API repository layer to use configured project asset storage for asset uploads, list, and read.
15+
- Added a server read route at `/api/storage/project-assets/read?key=...` that only serves keys under the configured projects prefix.
16+
- Added `scripts/validate-storage-config.mjs` for `.env`-only storage config validation.
17+
- Added targeted assets.html Playwright coverage for upload/list/read through the API/service contract.
18+
19+
## Storage Env Contract Evidence
20+
- PASS - Contract keys added:
21+
- `GAMEFOUNDRY_STORAGE_ENDPOINT`
22+
- `GAMEFOUNDRY_STORAGE_ACCESS_KEY_ID`
23+
- `GAMEFOUNDRY_STORAGE_SECRET_ACCESS_KEY`
24+
- `GAMEFOUNDRY_STORAGE_BUCKET`
25+
- `GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX`
26+
- PASS - Runtime storage config reads from environment values only.
27+
- PASS/SKIP - `.env` storage validation ran against current DEV `.env`; storage values are not present on this machine:
28+
- `PASS - .env loaded for storage config validation (5 key(s) applied).`
29+
- `PASS - Storage env keys present=0/5.`
30+
- `SKIP - Storage DEV values are not fully configured in .env (...)`.
31+
32+
## Upload/List/Read Evidence
33+
- PASS - Targeted Playwright covered assets.html upload/list/read:
34+
- Command: `npx playwright test tests/playwright/tools/AssetToolMockRepository.spec.mjs -g "Assets DEV storage upload list and read"`.
35+
- Result: PASS, 1 test passed.
36+
- PASS - Upload evidence:
37+
- UI reported `Batch upload complete: 1 written, 0 failed, 0 skipped, 0 warnings.`
38+
- Fake R2-compatible service captured the uploaded PNG bytes.
39+
- PASS - List evidence:
40+
- Browser called the repository API method `listStoredProjectObjects`.
41+
- Returned storage key matched the uploaded object key.
42+
- PASS - Read evidence:
43+
- Browser called the repository API method `readStoredProjectObject`.
44+
- Returned base64 bytes matched the uploaded PNG.
45+
46+
## Prefix/Path Evidence
47+
- PASS - Test configuration used `GAMEFOUNDRY_STORAGE_PROJECTS_PREFIX=/dev/projects/`.
48+
- PASS - Uploaded object path landed under the configured prefix:
49+
- `/dev/projects/<projectId>/image/storage-dev-upload.png`.
50+
- PASS - Server read route rejects empty keys, missing prefix config, and keys outside the configured prefix.
51+
- PASS - Browser-visible asset path is an API read path, not a raw storage URL:
52+
- `api/storage/project-assets/read?key=...`.
53+
54+
## Safety Evidence
55+
- PASS - Browser did not receive storage secrets:
56+
- Targeted Playwright asserted the page body did not contain `asset-test-secret-key`.
57+
- Targeted Playwright asserted the page body did not contain `asset-test-access-key`.
58+
- PASS - Browser uploads go through repository/API methods; R2 client and signing stay server-side.
59+
- PASS - No `.env` editing UI was added.
60+
- PASS - No destructive storage operations were added.
61+
- PASS - No DEV/IST/UAT/PRD branching was added.
62+
- PASS - No silent fallback was added; missing storage config returns explicit unavailable/failure diagnostics.
63+
- PASS - No `imageDataUrl` persistence was introduced in changed storage files.
64+
65+
## Validation
66+
- PASS - `node --check assets/theme-v2/js/owner-operations.js`.
67+
- PASS - `node --check scripts/validate-storage-config.mjs`.
68+
- PASS - `node --check src/dev-runtime/storage/storage-config.mjs`.
69+
- PASS - `node --check src/dev-runtime/storage/r2-project-asset-storage.mjs`.
70+
- PASS - `node --check src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js`.
71+
- PASS - `node --check src/dev-runtime/server/local-api-router.mjs`.
72+
- PASS - `node --check tests/playwright/tools/AdminPlatformToolsWireframes.spec.mjs`.
73+
- PASS - `node --check tests/playwright/tools/AssetToolMockRepository.spec.mjs`.
74+
- PASS/SKIP - `node .\scripts\validate-storage-config.mjs`; `.env` loaded, storage DEV values unavailable, live storage readiness skipped.
75+
- PASS - `npx playwright test tests/playwright/tools/AssetToolMockRepository.spec.mjs -g "Assets DEV storage upload list and read"`.
76+
- PASS - `npm run validate:browser-env-agnostic`.
77+
- PASS - Targeted grep found no `imageDataUrl` in changed storage path.
78+
- SKIP - Full samples smoke was intentionally not run per BUILD instruction.
79+
80+
## Requirement Checklist
81+
- PASS - Read `docs_build/dev/PROJECT_INSTRUCTIONS.md` first.
82+
- PASS - HARD STOP unless current branch is `main`.
83+
- PASS - Started R2/storage lane only after DB PRs passed.
84+
- PASS - Added required storage env contract.
85+
- PASS - Runtime loads storage config from `.env`/environment only.
86+
- PASS - Browser does not receive storage secrets.
87+
- PASS - Browser uploads through API/service contract only.
88+
- PASS - Implemented DEV upload/list/read path needed by assets.html.
89+
- PASS - Object keys use configured prefix.
90+
- PASS - Did not branch by DEV/IST/UAT/PRD.
91+
- PASS - No silent fallback.
92+
- PASS - Ran required targeted validation.
93+
- PASS - Did not run full samples smoke.
94+
- PASS - Required reports prepared: `codex_review.diff`, `codex_changed_files.txt`, and this report.
95+
- PASS - Required repo-structured artifact output produced at `tmp/PR_26167_210-r2-project-assets-dev-storage_delta.zip`.

0 commit comments

Comments
 (0)