Skip to content

Commit fd9e67f

Browse files
committed
Add DB viewer and polish Project Journey note editing usability - PR_26157_004-project-journey-db-audit-and-usability
1 parent d85f01f commit fd9e67f

16 files changed

Lines changed: 895 additions & 118 deletions

‎admin/db-viewer.html‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<!doctype html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<base href="/">
7+
<meta name="viewport" content="width=device-width, initial-scale=1">
8+
<title>DB Viewer - GameFoundryStudio</title>
9+
<meta name="description" content="Admin-only read-only mock DB viewer for GameFoundryStudio project data diagnostics.">
10+
<link rel="icon" href="/favicon.svg">
11+
<link rel="stylesheet" href="assets/theme-v2/css/theme.css">
12+
</head>
13+
14+
<body>
15+
<div data-partial="header-nav"></div>
16+
<main data-admin-only="true">
17+
<section class="page-title">
18+
<div class="container">
19+
<div class="kicker">Admin Only</div>
20+
<h1>DB Viewer</h1>
21+
<p class="lede">Read-only mock DB dump for project tables, relationships, and data diagnostics.</p>
22+
</div>
23+
</section>
24+
<section class="section">
25+
<div class="container account-panel">
26+
<aside class="side-menu" aria-label="Admin pages">
27+
<a href="admin/analytics.html">Analytics</a>
28+
<a href="admin/branding.html">Branding</a>
29+
<a href="admin/controls.html">Controls</a>
30+
<a class="active" aria-current="page" href="admin/db-viewer.html">DB Viewer</a>
31+
<a href="admin/design-system.html">Design System</a>
32+
<a href="toolbox/environments/index.html">Environments</a>
33+
<a href="toolbox/game-migration/index.html">Game Migration</a>
34+
<a href="admin/grouping-colors.html">Grouping Colors</a>
35+
<a href="admin/moderation.html">Moderation</a>
36+
<a href="admin/notes.html">Notes</a>
37+
<a href="toolbox/platform-settings/index.html">Platform Settings</a>
38+
<a href="admin/ratings.html">Ratings</a>
39+
<a href="admin/roles.html">Roles</a>
40+
<a href="admin/site-settings.html">Site Settings</a>
41+
<a href="admin/themes.html">Themes</a>
42+
<a href="admin/tools-progress.html">Tools Progress</a>
43+
<a href="admin/users.html">Users</a>
44+
</aside>
45+
<article class="card" aria-labelledby="admin-db-viewer-title" data-admin-db-viewer>
46+
<div class="card-body content-stack">
47+
<div class="content-cluster">
48+
<div>
49+
<div class="kicker">Mock DB</div>
50+
<h2 id="admin-db-viewer-title">Project Journey Tables</h2>
51+
</div>
52+
<span class="status" role="status" data-admin-db-status>Loading mock DB records.</span>
53+
</div>
54+
<section class="callout content-stack content-stack--compact" aria-labelledby="admin-db-diagnostics-title">
55+
<h3 id="admin-db-diagnostics-title">Diagnostics</h3>
56+
<div class="content-stack content-stack--compact" data-admin-db-diagnostics></div>
57+
</section>
58+
<section class="callout content-stack content-stack--compact" aria-labelledby="admin-db-relationships-title">
59+
<h3 id="admin-db-relationships-title">Relationships</h3>
60+
<div class="content-stack content-stack--compact" data-admin-db-relationships></div>
61+
</section>
62+
<section class="content-stack" aria-label="Mock DB tables" data-admin-db-tables></section>
63+
</div>
64+
</article>
65+
</div>
66+
</section>
67+
</main>
68+
<div data-partial="footer"></div>
69+
<script src="assets/theme-v2/js/gamefoundry-partials.js" defer></script>
70+
<script type="module" src="admin/db-viewer.js"></script>
71+
</body>
72+
73+
</html>

‎admin/db-viewer.js‎

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { createProjectJourneyMockRepository } from "../toolbox/project-journey/project-journey-mock-repository.js";
2+
3+
const AUDIT_FIELDS = ["createdAt", "updatedAt", "createdByType", "updatedByType"];
4+
5+
class AdminDbViewer {
6+
constructor(documentRef = document) {
7+
this.document = documentRef;
8+
this.repository = createProjectJourneyMockRepository();
9+
this.status = documentRef.querySelector("[data-admin-db-status]");
10+
this.diagnostics = documentRef.querySelector("[data-admin-db-diagnostics]");
11+
this.relationships = documentRef.querySelector("[data-admin-db-relationships]");
12+
this.tablesRoot = documentRef.querySelector("[data-admin-db-tables]");
13+
}
14+
15+
start() {
16+
this.repository.openProject("demo-project");
17+
const tables = this.repository.getTables();
18+
this.renderDiagnostics(tables);
19+
this.renderRelationships(tables);
20+
this.renderTables(tables);
21+
if (this.status) {
22+
this.status.textContent = "Read-only mock DB dump loaded for Demo Project.";
23+
}
24+
}
25+
26+
createElement(tagName, options = {}) {
27+
const element = this.document.createElement(tagName);
28+
if (options.className) {
29+
element.className = options.className;
30+
}
31+
if (options.text !== undefined) {
32+
element.textContent = options.text;
33+
}
34+
return element;
35+
}
36+
37+
recordId(record) {
38+
return record.itemId || record.templateId || record.id || record.name || "record";
39+
}
40+
41+
formatValue(value) {
42+
if (Array.isArray(value)) {
43+
return value.join(", ");
44+
}
45+
if (value && typeof value === "object") {
46+
return JSON.stringify(value);
47+
}
48+
if (value === undefined || value === null || value === "") {
49+
return "(empty)";
50+
}
51+
return String(value);
52+
}
53+
54+
tableFields(records) {
55+
const fields = [];
56+
records.forEach((record) => {
57+
Object.keys(record).forEach((field) => {
58+
if (!fields.includes(field)) {
59+
fields.push(field);
60+
}
61+
});
62+
});
63+
return fields;
64+
}
65+
66+
renderTable(tableName, records) {
67+
const details = this.createElement("details", {
68+
className: "vertical-accordion",
69+
});
70+
details.open = true;
71+
details.dataset.adminDbTable = tableName;
72+
73+
const summary = this.createElement("summary", {
74+
text: `${tableName} (${records.length} records)`,
75+
});
76+
const body = this.createElement("div", {
77+
className: "accordion-body",
78+
});
79+
const wrapper = this.createElement("div", {
80+
className: "table-wrapper",
81+
});
82+
const table = this.createElement("table", {
83+
className: "data-table",
84+
});
85+
table.setAttribute("aria-label", `${tableName} records`);
86+
87+
const fields = this.tableFields(records);
88+
const head = this.createElement("thead");
89+
const headerRow = this.createElement("tr");
90+
fields.forEach((field) => {
91+
headerRow.append(this.createElement("th", { text: field }));
92+
});
93+
head.append(headerRow);
94+
95+
const tableBody = this.createElement("tbody");
96+
records.forEach((record) => {
97+
const row = this.createElement("tr");
98+
row.dataset.adminDbRecord = this.recordId(record);
99+
fields.forEach((field) => {
100+
row.append(this.createElement("td", { text: this.formatValue(record[field]) }));
101+
});
102+
tableBody.append(row);
103+
});
104+
105+
table.append(head, tableBody);
106+
wrapper.append(table);
107+
body.append(wrapper);
108+
details.append(summary, body);
109+
return details;
110+
}
111+
112+
renderTables(tables) {
113+
this.tablesRoot.replaceChildren();
114+
Object.keys(tables)
115+
.sort()
116+
.forEach((tableName) => {
117+
this.tablesRoot.append(this.renderTable(tableName, tables[tableName]));
118+
});
119+
}
120+
121+
auditFindings(tables) {
122+
const findings = [];
123+
Object.entries(tables).forEach(([tableName, records]) => {
124+
records.forEach((record) => {
125+
AUDIT_FIELDS.forEach((field) => {
126+
if (!Object.hasOwn(record, field)) {
127+
findings.push(`${tableName}.${this.recordId(record)} is missing ${field}.`);
128+
}
129+
});
130+
});
131+
});
132+
return findings;
133+
}
134+
135+
relationshipsForTables(tables) {
136+
const noteTypeIds = new Set(tables.project_journey_note_types.map((type) => type.id));
137+
const noteIds = new Set(tables.project_journey_notes.map((note) => note.id));
138+
const activeTemplateIds = new Set(
139+
tables.project_journey_templates
140+
.filter((template) => template.isActive)
141+
.map((template) => template.templateId),
142+
);
143+
return [
144+
{
145+
name: "project_journey_notes.typeId -> project_journey_note_types.id",
146+
checked: tables.project_journey_notes.length,
147+
missing: tables.project_journey_notes.filter((note) => !noteTypeIds.has(note.typeId)),
148+
},
149+
{
150+
name: "project_journey_items.noteId -> project_journey_notes.id",
151+
checked: tables.project_journey_items.length,
152+
missing: tables.project_journey_items.filter((item) => !noteIds.has(item.noteId)),
153+
},
154+
{
155+
name: "system project_journey_items.templateId -> active project_journey_templates.templateId",
156+
checked: tables.project_journey_items.filter((item) => item.createdByType === "system").length,
157+
missing: tables.project_journey_items.filter(
158+
(item) => item.createdByType === "system" && !activeTemplateIds.has(item.templateId),
159+
),
160+
},
161+
{
162+
name: "project_journey_activity.noteId -> project_journey_notes.id",
163+
checked: tables.project_journey_activity.length,
164+
missing: tables.project_journey_activity.filter((activity) => !noteIds.has(activity.noteId)),
165+
},
166+
];
167+
}
168+
169+
tableBleedFindings(tables) {
170+
const notesById = new Map(tables.project_journey_notes.map((note) => [note.id, note]));
171+
const findings = [];
172+
tables.project_journey_items.forEach((item) => {
173+
const note = notesById.get(item.noteId);
174+
if (note && note.projectId !== item.projectId) {
175+
findings.push(`${item.itemId} projectId ${item.projectId} does not match note ${note.id} projectId ${note.projectId}.`);
176+
}
177+
});
178+
tables.project_journey_activity.forEach((activity) => {
179+
const note = notesById.get(activity.noteId);
180+
if (note && note.projectId !== activity.projectId) {
181+
findings.push(`${activity.id} projectId ${activity.projectId} does not match note ${note.id} projectId ${note.projectId}.`);
182+
}
183+
});
184+
return findings;
185+
}
186+
187+
renderList(messages, dataName) {
188+
const list = this.createElement("ul");
189+
list.dataset[dataName] = "";
190+
messages.forEach((message) => {
191+
list.append(this.createElement("li", { text: message }));
192+
});
193+
return list;
194+
}
195+
196+
renderDiagnostics(tables) {
197+
this.diagnostics.replaceChildren();
198+
const auditFindings = this.auditFindings(tables);
199+
const bleedFindings = this.tableBleedFindings(tables);
200+
const auditSummary = auditFindings.length
201+
? auditFindings
202+
: ["All Project Journey mock DB tables include createdAt, updatedAt, createdByType, and updatedByType."];
203+
const bleedSummary = bleedFindings.length ? bleedFindings : ["No table bleed detected."];
204+
this.diagnostics.append(
205+
this.renderList(auditSummary, "adminDbAuditFindings"),
206+
this.renderList(bleedSummary, "adminDbBleedFindings"),
207+
);
208+
}
209+
210+
renderRelationships(tables) {
211+
this.relationships.replaceChildren();
212+
const relationshipRows = this.relationshipsForTables(tables);
213+
const missingLinks = relationshipRows.flatMap((relationship) =>
214+
relationship.missing.map((record) => `${relationship.name} missing for ${this.recordId(record)}.`),
215+
);
216+
const relationshipList = this.createElement("ul");
217+
relationshipList.dataset.adminDbRelationshipSummary = "";
218+
relationshipRows.forEach((relationship) => {
219+
const linkedCount = relationship.checked - relationship.missing.length;
220+
relationshipList.append(
221+
this.createElement("li", {
222+
text: `${relationship.name}: ${linkedCount}/${relationship.checked} records linked.`,
223+
}),
224+
);
225+
});
226+
this.relationships.append(relationshipList);
227+
this.relationships.append(
228+
this.renderList(missingLinks.length ? missingLinks : ["No missing links detected."], "adminDbMissingLinks"),
229+
);
230+
}
231+
}
232+
233+
new AdminDbViewer().start();

‎admin/notes.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ <h1>Admin Notes</h1>
2727
<a href="admin/analytics.html">Analytics</a>
2828
<a href="admin/branding.html">Branding</a>
2929
<a href="admin/controls.html">Controls</a>
30+
<a href="admin/db-viewer.html">DB Viewer</a>
3031
<a href="admin/design-system.html">Design System</a>
3132
<a href="toolbox/environments/index.html">Environments</a>
3233
<a href="toolbox/game-migration/index.html">Game Migration</a>

‎admin/tools-progress.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ <h1>Tools Progress</h1>
2727
<a href="admin/analytics.html">Analytics</a>
2828
<a href="admin/branding.html">Branding</a>
2929
<a href="admin/controls.html">Controls</a>
30+
<a href="admin/db-viewer.html">DB Viewer</a>
3031
<a href="admin/design-system.html">Design System</a>
3132
<a href="toolbox/environments/index.html">Environments</a>
3233
<a href="toolbox/game-migration/index.html">Game Migration</a>

‎assets/theme-v2/css/panels.css‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,10 @@ body.tool-focus-mode .tool-display-mode__badge {
463463
.tool-tree-row__content {
464464
flex: 1 1 auto;
465465
justify-content: flex-start;
466-
min-width: var(--space-0)
466+
min-width: var(--space-0);
467+
overflow-wrap: anywhere;
468+
text-align: left;
469+
white-space: normal
467470
}
468471

469472
.tool-tree-row__action {
@@ -490,6 +493,11 @@ body.tool-focus-mode .tool-display-mode__badge {
490493
width: 32px
491494
}
492495

496+
.tool-guidance-block {
497+
width: 100%;
498+
overflow-wrap: anywhere
499+
}
500+
493501
.mascot-role {
494502
color: var(--text);
495503
font-size: var(--font-size-xl);

‎assets/theme-v2/css/tables.css‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ td {
2525
border-collapse: collapse
2626
}
2727

28+
.data-table--fixed {
29+
table-layout: fixed
30+
}
31+
32+
.data-table--fixed th,
33+
.data-table--fixed td {
34+
overflow-wrap: anywhere
35+
}
36+
2837
.data-table caption {
2938
color: var(--text)
3039
}

‎assets/theme-v2/js/gamefoundry-partials.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"admin-themes": "admin/themes.html",
7373
"admin-design-system": "admin/design-system.html",
7474
"admin-controls": "admin/controls.html",
75+
"admin-db-viewer": "admin/db-viewer.html",
7576
"admin-grouping-colors": "admin/grouping-colors.html",
7677
"admin-ratings": "admin/ratings.html",
7778
"admin-users": "admin/users.html",

‎assets/theme-v2/partials/header-nav.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
<a data-nav-link data-route="admin-analytics" href="admin/analytics.html">Analytics</a>
113113
<a data-nav-link data-route="admin-branding" href="admin/branding.html">Branding</a>
114114
<a data-nav-link data-route="admin-controls" href="admin/controls.html">Controls</a>
115+
<a data-nav-link data-route="admin-db-viewer" href="admin/db-viewer.html">DB Viewer</a>
115116
<a data-nav-link data-route="admin-design-system" href="admin/design-system.html">Design System</a>
116117
<a data-nav-link data-route="environments" href="toolbox/environments/index.html">Environments</a>
117118
<a data-nav-link data-route="game-migration" href="toolbox/game-migration/index.html">Game Migration</a>

0 commit comments

Comments
 (0)