Skip to content

Commit c15715c

Browse files
committed
Build game crew foundation
1 parent 1420bc8 commit c15715c

22 files changed

Lines changed: 1837 additions & 1403 deletions
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { getSessionCurrent } from "../../../../src/api/session-api-client.js";
2+
import {
3+
createServerRepositoryClient,
4+
readServerToolConstants,
5+
requireServerConstant,
6+
} from "../../../../src/api/server-api-client.js";
7+
8+
const constants = readServerToolConstants("game-crew");
9+
10+
export const GAME_CREW_TABLES = Object.freeze(requireServerConstant(constants, "GAME_CREW_TABLES", "game-crew"));
11+
export const GAME_CREW_MEMBER_ROLES = Object.freeze(requireServerConstant(constants, "GAME_CREW_MEMBER_ROLES", "game-crew"));
12+
13+
export function createGameCrewApiRepository(options = {}) {
14+
return createServerRepositoryClient("game-crew", options);
15+
}
16+
17+
const repository = createGameCrewApiRepository();
18+
19+
const elements = {
20+
action: document.querySelector("[data-game-crew-action]"),
21+
add: document.querySelector("[data-game-crew-add]"),
22+
count: document.querySelector("[data-game-crew-count]"),
23+
guidance: document.querySelector("[data-game-crew-guidance]"),
24+
log: document.querySelector("[data-game-crew-log]"),
25+
outputStatus: document.querySelector("[data-game-crew-output-status]"),
26+
owner: document.querySelector("[data-game-crew-owner]"),
27+
projectName: document.querySelector("[data-game-crew-project-name]"),
28+
refresh: document.querySelector("[data-game-crew-refresh]"),
29+
selected: document.querySelector("[data-game-crew-selected]"),
30+
status: document.querySelector("[data-game-crew-status]"),
31+
table: document.querySelector("[data-game-crew-table]"),
32+
tableCounts: document.querySelector("[data-game-crew-table-counts]"),
33+
};
34+
35+
function setText(target, value) {
36+
if (target) {
37+
target.textContent = value;
38+
}
39+
}
40+
41+
function createCell(text) {
42+
const cell = document.createElement("td");
43+
cell.textContent = text;
44+
return cell;
45+
}
46+
47+
function createButton(label, datasetName, value, options = {}) {
48+
const button = document.createElement("button");
49+
button.className = options.secondary ? "btn btn--secondary btn--compact" : "btn btn--compact";
50+
button.type = "button";
51+
button.dataset[datasetName] = value;
52+
button.textContent = label;
53+
if (options.disabled) {
54+
button.disabled = true;
55+
}
56+
return button;
57+
}
58+
59+
function currentSession() {
60+
try {
61+
return getSessionCurrent();
62+
} catch {
63+
return { authenticated: false };
64+
}
65+
}
66+
67+
function redirectGuestWriteAction() {
68+
if (currentSession()?.authenticated === true) {
69+
return false;
70+
}
71+
setText(elements.log, "Sign in before changing project crew membership.");
72+
window.location.href = new URL("/account/sign-in.html", window.location.href).href;
73+
return true;
74+
}
75+
76+
function renderTableCounts(snapshot) {
77+
if (!elements.tableCounts) {
78+
return;
79+
}
80+
elements.tableCounts.replaceChildren();
81+
snapshot.tableCounts.forEach((count) => {
82+
const row = document.createElement("tr");
83+
row.append(createCell(count.table), createCell(String(count.rows)));
84+
elements.tableCounts.append(row);
85+
});
86+
}
87+
88+
function renderMembers(snapshot) {
89+
if (!elements.table) {
90+
return;
91+
}
92+
elements.table.replaceChildren();
93+
94+
if (!snapshot.members.length) {
95+
const row = document.createElement("tr");
96+
const cell = document.createElement("td");
97+
cell.colSpan = 5;
98+
cell.textContent = "No crew members yet.";
99+
row.append(cell);
100+
elements.table.append(row);
101+
return;
102+
}
103+
104+
snapshot.members.forEach((member) => {
105+
const row = document.createElement("tr");
106+
row.dataset.gameCrewMemberRow = member.userKey;
107+
const actions = document.createElement("div");
108+
actions.className = "action-group action-group--tight";
109+
actions.append(createButton(
110+
member.role === "Owner" ? "Owner locked" : "Remove",
111+
"gameCrewRemove",
112+
member.userKey,
113+
{ disabled: member.role === "Owner", secondary: true },
114+
));
115+
const actionCell = document.createElement("td");
116+
actionCell.append(actions);
117+
row.append(
118+
createCell(member.displayName),
119+
createCell(member.role),
120+
createCell(member.status),
121+
createCell(member.joinedAt ? new Date(member.joinedAt).toLocaleDateString() : "Ready"),
122+
actionCell,
123+
);
124+
elements.table.append(row);
125+
});
126+
}
127+
128+
function normalizeSnapshot(value) {
129+
return value && typeof value === "object" && Array.isArray(value.members)
130+
? value
131+
: {
132+
activeProject: null,
133+
guidance: "Project crew is temporarily unavailable. Refresh the page.",
134+
members: [],
135+
owner: null,
136+
status: "Unavailable",
137+
tableCounts: GAME_CREW_TABLES.map((table) => ({ rows: 0, table })),
138+
};
139+
}
140+
141+
function render() {
142+
const snapshot = normalizeSnapshot(repository.getSnapshot());
143+
setText(elements.status, snapshot.status);
144+
setText(elements.outputStatus, snapshot.status);
145+
setText(elements.owner, snapshot.owner?.displayName || snapshot.activeProject?.ownerDisplayName || "Unknown");
146+
setText(elements.count, String(snapshot.members.length));
147+
setText(elements.projectName, snapshot.activeProject?.name || "No project selected");
148+
setText(elements.guidance, snapshot.guidance);
149+
renderMembers(snapshot);
150+
renderTableCounts(snapshot);
151+
}
152+
153+
elements.add?.addEventListener("click", () => {
154+
if (redirectGuestWriteAction()) {
155+
return;
156+
}
157+
const result = repository.readAddMemberPlaceholder();
158+
setText(elements.action, result.message);
159+
setText(elements.log, result.message);
160+
});
161+
162+
elements.refresh?.addEventListener("click", () => {
163+
setText(elements.log, "Project crew refreshed from the API.");
164+
render();
165+
});
166+
167+
elements.table?.addEventListener("click", (event) => {
168+
const remove = event.target.closest("[data-game-crew-remove]");
169+
if (!remove || remove.disabled) {
170+
return;
171+
}
172+
if (redirectGuestWriteAction()) {
173+
return;
174+
}
175+
const result = repository.readRemoveMemberPlaceholder(remove.dataset.gameCrewRemove);
176+
setText(elements.selected, result.member?.displayName || "Selected member");
177+
setText(elements.action, result.message);
178+
setText(elements.log, result.message);
179+
});
180+
181+
render();
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- Game Foundry Studio DEV database DDL
2+
-- Group: Game Crew
3+
-- Ownership: docs_build/database/ddl/game-crew.sql
4+
-- Target DEV database: gamefoundry_dev
5+
-- Scope: executable grouped table DDL for active API/database migration.
6+
-- Authoritative key values are generated by the server/API layer.
7+
-- Owned tables: project_members
8+
9+
CREATE TABLE IF NOT EXISTS project_members (
10+
key text PRIMARY KEY,
11+
"projectKey" text NOT NULL REFERENCES game_workspace_games(key),
12+
"userKey" text NOT NULL REFERENCES users(key),
13+
role text NOT NULL,
14+
status text NOT NULL,
15+
"invitedBy" text REFERENCES users(key),
16+
"invitedAt" timestamptz,
17+
"joinedAt" timestamptz,
18+
"removedAt" timestamptz,
19+
"createdAt" timestamptz NOT NULL DEFAULT now(),
20+
"updatedAt" timestamptz NOT NULL DEFAULT now(),
21+
"createdBy" text NOT NULL REFERENCES users(key),
22+
"updatedBy" text NOT NULL REFERENCES users(key)
23+
);
24+
25+
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_members_project_user ON project_members ("projectKey", "userKey");
26+
CREATE INDEX IF NOT EXISTS idx_project_members_projectkey ON project_members ("projectKey");
27+
CREATE INDEX IF NOT EXISTS idx_project_members_userkey ON project_members ("userKey");
28+
CREATE INDEX IF NOT EXISTS idx_project_members_createdby ON project_members ("createdBy");
29+
CREATE INDEX IF NOT EXISTS idx_project_members_updatedby ON project_members ("updatedBy");
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- Game Foundry Studio DEV database DML / seed review
2+
-- Group: Game Crew
3+
-- Ownership: docs_build/database/dml/game-crew.sql
4+
-- Runtime setup/seed operations for this group must run through server-side APIs.
5+
-- Browser pages must not directly seed authoritative DB records.
6+
-- Owned tables: project_members
7+
8+
-- DML status: Server-seed-owned.
9+
-- The server/API layer generates all non-user keys and audit fields.
10+
-- Direct SQL setup for this group remains deferred until a later migration-runner PR explicitly owns it.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Grouped Game Crew DDL entrypoint.
2+
-- Apply docs_build/database/ddl/game-crew.sql for the executable table contract.
3+
\i ../ddl/game-crew.sql
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Grouped Game Crew DML entrypoint.
2+
-- Review docs_build/database/dml/game-crew.sql for the server-owned seed contract.
3+
\i ../dml/game-crew.sql
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"group": "Game Crew",
3+
"delegates": {
4+
"seed": "../seed/game-crew.json",
5+
"guestSeed": "../seed/guest/game-crew.json"
6+
}
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"group": "Game Crew",
3+
"groupKey": "game-crew",
4+
"owner": "docs_build/database/seed",
5+
"serverSideSeedRequired": true,
6+
"browserAuthoritativeKeyGenerationAllowed": false,
7+
"tables": {
8+
"project_members": []
9+
},
10+
"note": "Project crew records are owned by the API/database contract. Browser pages must not seed or generate authoritative membership records."
11+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"group": "Game Crew",
3+
"groupKey": "game-crew",
4+
"owner": "docs_build/database/seed/guest",
5+
"readOnly": true,
6+
"writableByGuest": false,
7+
"signInRedirect": "account/sign-in.html",
8+
"tables": {
9+
"project_members": []
10+
},
11+
"samplePackages": [
12+
{
13+
"key": "guest-game-crew-starter",
14+
"audience": "guest",
15+
"createdBy": "01K2GFSJ0Y0000000000000054",
16+
"groupKey": "game-crew",
17+
"label": "Guest Game Crew starter",
18+
"loadablePath": "toolbox/game-crew/index.html",
19+
"readOnly": true,
20+
"sampleKind": "toolSeed",
21+
"signInRedirect": "account/sign-in.html",
22+
"source": "docs_build/database/seed/guest/game-crew.json",
23+
"toolKey": "game-crew",
24+
"toolName": "Game Crew",
25+
"writableByGuest": false
26+
}
27+
]
28+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# PR_26177_ALFA_059 Branch Validation
2+
3+
| Check | Result | Evidence |
4+
| --- | --- | --- |
5+
| Branch | PASS | `PR_26177_ALFA_059-game-crew-foundation` |
6+
| Base gate | PASS | Branch created from clean synced `main` after `main...origin/main` reported `0 0`. |
7+
| Scope | PASS | Changes are limited to Game Crew foundation, DB docs, targeted tests, and required reports. |
8+
| Runtime architecture | PASS | Browser reads Game Crew through the API repository; server owns member rows. |
9+
| No full invitations/permissions | PASS | Add/remove controls return Creator-safe planned-state guidance only. |
10+
| SQLite/tmp dependency | PASS | Scoped Game Crew surface search found no SQLite or `tmp/local-api` runtime references. |
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# PR_26177_ALFA_059 Manual Validation Notes
2+
3+
## Reviewed
4+
- Game Crew page loads Theme V2 chrome and the shared Tool Display Mode component.
5+
- Active project displays as `Demo Game`.
6+
- Owner summary displays `User 1`.
7+
- Member count displays `3`.
8+
- Member table includes `User 1`, `DavidQ`, and `User 3`.
9+
- Repository table count shows `project_members`.
10+
- Add Member shows planned invitation guidance for signed-in users.
11+
- Remove on a non-owner shows planned removal guidance.
12+
- Guest Add Member redirects to `account/sign-in.html`.
13+
14+
## Not In Scope
15+
- Full member invitations.
16+
- Permissions, ownership transfer, or role enforcement.
17+
- Applying database migrations to the developer workstation database.

0 commit comments

Comments
 (0)