From e6ad7b4c4e40e3af988d66570186b535a474c507 Mon Sep 17 00:00:00 2001 From: JB Date: Sat, 25 Jul 2026 14:06:07 -0400 Subject: [PATCH] feat(isac): add oversight proposal corrections --- database/schema.sql | 6 ++- docs/DESIGN_SYSTEM.md | 25 ++++++++-- docs/architecture.md | 6 +-- src/app.css | 1 + src/lib/server/data-moderation.ts | 13 +++++ src/routes/admin/+layout.svelte | 6 +-- .../admin/contributor-guidelines/+page.svelte | 2 +- .../admin/data-moderation/+page.server.ts | 50 ++++++++++++++++++- src/routes/admin/data-moderation/+page.svelte | 41 +++++++++++++-- src/routes/terms/+page.svelte | 2 +- 10 files changed, 131 insertions(+), 21 deletions(-) diff --git a/database/schema.sql b/database/schema.sql index c3ec2b0..fbbb754 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -113,7 +113,7 @@ INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-editor','Data INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-user-operator','User Operator','View and moderate SHD.build users'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-access-manager','Access Manager','Manage user roles and direct permissions'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-admin','Admin','Full administrative access to all current and future ISAC capabilities'); -INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-moderator','Moderator','Community moderation role reserved for build, guide, and comment tools'); +INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-moderator','Community Oversight','Community oversight role reserved for build, guide, and comment tools'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-system-admin','system.admin','Unrestricted access to all administrative capabilities'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-isac-access','isac.access','Open the ISAC interface'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-data-manage','data.manage','Create and edit builder source data'); @@ -133,7 +133,9 @@ INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data- INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-editor','permission-data-view'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-viewer','Data Viewer','Read-only access to ISAC game-data registries'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-contributor','Data Contributor','View game data and submit moderated change proposals'); -INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-approver','Data Approver','Review proposals and apply approved changes to live game data'); +INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-approver','Data Oversight','Review, correct, and decide proposals before applying approved changes to live game data'); +UPDATE roles SET name='Community Oversight',description='Community oversight role reserved for build, guide, and comment tools' WHERE id='role-moderator'; +UPDATE roles SET name='Data Oversight',description='Review, correct, and decide proposals before applying approved changes to live game data' WHERE id='role-data-approver'; INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-viewer','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-viewer','permission-data-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-contributor','permission-isac-access'); diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 2919d0e..6859049 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -224,9 +224,9 @@ Use the spacing tokens for new components. Prefer `8px`, `12px`, and `20px` gaps be textual and use the danger color; inactivity remains a separate activity classification rather than a punishment state. - Role names describe durable organizational responsibility. `Admin` is the - unrestricted administrative role; `Moderator` is reserved for community - content moderation and does not imply ISAC access. Operational ISAC access is - delegated separately through scoped roles and permissions. + unrestricted administrative role; `Community Oversight` is reserved for + community content moderation and does not imply ISAC access. Operational + ISAC access is delegated separately through scoped roles and permissions. - Roles & Permissions is a separate User Operations registry. Role definitions compose discrete capabilities through a dense permission catalogue with an explicit selected count and textual `GRANTED` / `NOT GRANTED` states. The @@ -547,13 +547,28 @@ and other non-mutating inspection controls remain visible and interactive. The ISAC command-status cluster includes an explicit `DATA MODE` cell alongside system, access, and environment. It reports the highest effective data authority -in this order: `ADMIN`, `DATA MANAGER`, `APPROVER`, `CONTRIBUTOR`, then +in this order: `ADMIN`, `OVERSIGHT`, `DATA MANAGER`, `CONTRIBUTOR`, then `READ-ONLY`. Use a neutral treatment for read-only, orange for contributor, -blue for approval authority, and green for direct-management authority. The +blue for Oversight authority, and green for direct-management authority. The label and value are mandatory because color is supplemental. At medium widths the four cells become a two-column grid; on narrow screens the cluster spans the command header width. +### Oversight proposal corrections + +User-facing references to moderators and data approvers use the SHD term +`Oversight`. Distinguish `Community Oversight` from `Data Oversight` when a role +name must identify its operational domain. + +Pending proposal review uses three equal decision controls: approve, correct, +and reject. `CORRECT` is limited to small text or numeric fixes. Present the +stored form fields in a compact two-column correction grid and identify their +original field names; long descriptions and structured JSON values use +textareas. Uploaded file evidence is visible but immutable. Every correction +must be separately saved and audit logged before approval, and material changes +should be rejected for contributor revision. Collapse the decision controls and +correction fields to one column on narrow screens. + Data-navigation links with pending proposals use a dedicated four-column layout: module code, flexible label, square orange count cell, then navigation arrow. Never insert a count into the ordinary three-column link without changing its diff --git a/docs/architecture.md b/docs/architecture.md index 6cca7fb..431d992 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -113,7 +113,7 @@ mutation rather than deletion or modification of the original event: ISAC data access is split into `data.view`, `data.propose`, `data.manage`, and `data.approve`. Data Viewers can inspect registries. Data Contributors can submit the same editor forms, but the request is serialized into `data_change_proposals` -instead of mutating canonical tables. Data Approvers review the queue and must +instead of mutating canonical tables. Data Oversight reviews the queue and must explicitly confirm approval or rejection. Approval replays the stored form through its original SvelteKit server action. @@ -124,8 +124,8 @@ proposal as failed without changing live data. Pending counts appear alongside each data registry in ISAC so collaborators can see concurrent suggestions. The protected seeded roles are Data Viewer, Data Contributor, Data Editor, and -Data Approver. Data Editor retains direct-write access for trusted operators; -Data Approver combines direct-write access with queue review. New ISAC data +Data Oversight. Data Editor retains direct-write access for trusted operators; +Data Oversight combines direct-write access with queue review. New ISAC data modules must use the shared moderation interception path and audit both the final mutation and proposal lifecycle. diff --git a/src/app.css b/src/app.css index d566d6e..2b24230 100644 --- a/src/app.css +++ b/src/app.css @@ -134,4 +134,5 @@ .contributor-guidelines-toolbar{align-items:flex-end}.guideline-version{min-width:120px;display:grid;gap:4px;padding:8px 10px;border:1px solid var(--shd-border);background:#111415}.guideline-version span{color:var(--shd-text-muted);font-size:6px;letter-spacing:1px}.guideline-version strong{color:var(--shd-orange);font-size:9px}.contributor-guidelines{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin:10px 0}.contributor-guidelines section{min-height:122px;display:grid;grid-template-columns:34px 1fr;border:1px solid var(--shd-border);background:#111415}.contributor-guidelines section>span{display:grid;place-items:start center;padding-top:11px;border-right:1px solid var(--shd-border);color:var(--shd-orange);font-size:8px;font-weight:900}.contributor-guidelines section>div{padding:11px}.contributor-guidelines h3{margin:0 0 6px;font-size:9px;letter-spacing:.7px}.contributor-guidelines p{margin:0;color:#858d8f;font-size:8px;line-height:1.55}.contributor-acceptance{display:flex;align-items:flex-start;gap:9px;padding:11px;border:1px solid #6f511c;background:#19150e;cursor:pointer}.contributor-acceptance input{width:auto;margin:2px 0 0;accent-color:var(--shd-orange)}.contributor-acceptance span{display:grid;gap:4px}.contributor-acceptance b{color:#e3c487;font-size:8px;letter-spacing:.7px}.contributor-acceptance small{color:#917d59;font-size:7px}.form-actions a.button{display:inline-flex;align-items:center;justify-content:center;text-decoration:none}@media(max-width:760px){.contributor-guidelines{grid-template-columns:1fr}.data-access-banner{align-items:flex-start;flex-direction:column}.mode-toggle{width:100%;justify-content:center}} .command-status .data-mode-status{border-left:2px solid #667174}.command-status .data-mode-status strong{color:#b8c0c1;white-space:nowrap}.command-status .data-mode-status.contributor{border-left-color:var(--shd-orange)}.command-status .data-mode-status.contributor strong{color:#f2b64d}.command-status .data-mode-status.approver{border-left-color:#68a9c4}.command-status .data-mode-status.approver strong{color:#8ecce5}.command-status .data-mode-status.data-manager,.command-status .data-mode-status.admin{border-left-color:#75c08c}.command-status .data-mode-status.data-manager strong,.command-status .data-mode-status.admin strong{color:#8bd49f}@media(max-width:900px){.admin-command{align-items:flex-start;gap:12px}.command-status{display:grid;grid-template-columns:repeat(2,minmax(105px,1fr))}}@media(max-width:560px){.admin-command{display:grid}.command-status{width:100%}.command-status>div{min-width:0}} .data-readonly .module-toolbar button,.data-readonly .dashboard-row:not(.table-heading)>button,.data-readonly .dashboard-editor,.data-readonly form[method=post]{display:none!important} +.command-status .data-mode-status.oversight{border-left-color:#68a9c4}.command-status .data-mode-status.oversight strong{color:#8ecce5}.oversight-decision-tabs{grid-template-columns:repeat(3,1fr)}.proposal-correction-head{display:flex;align-items:center;justify-content:space-between;margin:10px 0 6px;padding:8px 9px;border:1px solid var(--shd-border);background:#0d1011}.proposal-correction-head>div{display:grid;gap:3px}.proposal-correction-head span{font-size:7px;font-weight:900;letter-spacing:.8px}.proposal-correction-head small{color:var(--shd-text-muted);font-size:6px}.proposal-correction-head>b{color:var(--shd-orange);font-size:7px}.proposal-correction-grid{max-height:340px;display:grid;grid-template-columns:1fr 1fr;gap:6px;overflow:auto;padding:7px;border:1px solid var(--shd-border);background:#0b0e0f}.proposal-correction-grid .field{margin:0}.proposal-correction-grid .field>span{display:flex;justify-content:space-between}.proposal-correction-grid .field>span small{color:#61696b;font-size:5px}.proposal-correction-grid textarea{min-height:74px;resize:vertical}.immutable-proposal-file{display:grid;align-content:center;gap:4px;min-height:54px;padding:8px;border:1px dashed #394041;color:var(--shd-text-muted)}.immutable-proposal-file span,.immutable-proposal-file small{font-size:6px}.immutable-proposal-file b{color:var(--shd-text);font-size:8px}@media(max-width:700px){.oversight-decision-tabs,.proposal-correction-grid{grid-template-columns:1fr}} diff --git a/src/lib/server/data-moderation.ts b/src/lib/server/data-moderation.ts index 6ff9bd2..71e4b20 100644 --- a/src/lib/server/data-moderation.ts +++ b/src/lib/server/data-moderation.ts @@ -43,6 +43,19 @@ export function deserializeProposal(payload: string): FormData { return form; } +export async function refreshProposalDependencies(proposalId: string, payload: string) { + const database = await getDatabase(); + const text = serializedTextValues(payload).map((entry) => entry.value).join('\n'); + const candidates = await (await database.prepare(`SELECT id,draft_entity_id FROM data_change_proposals + WHERE id<>? AND draft_entity_id IS NOT NULL`)).all(proposalId); + const dependencies = candidates.filter((candidate) => text.includes(String(candidate.draft_entity_id))); + await (await database.prepare('DELETE FROM data_change_proposal_dependencies WHERE proposal_id=?')).run(proposalId); + for (const dependency of dependencies) { + await (await database.prepare('INSERT INTO data_change_proposal_dependencies(proposal_id,depends_on_proposal_id) VALUES(?,?)')).run(proposalId, dependency.id); + } + return dependencies.map((dependency) => String(dependency.id)); +} + const serializedTextValues = (payload: string) => (JSON.parse(payload) as SerializedEntry[]).filter((entry): entry is Extract => entry.kind === 'text'); diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte index 50ab982..d245652 100644 --- a/src/routes/admin/+layout.svelte +++ b/src/routes/admin/+layout.svelte @@ -6,7 +6,7 @@ const contributorEnabled = $derived(Boolean(data.contributorMode?.enabled)); const guidelinesPage = $derived(page.url.pathname === '/admin/contributor-guidelines'); const dataReadOnly = $derived(can('data.view') && !can('data.manage') && (!can('data.propose') || !contributorEnabled) && !guidelinesPage); - const dataMode = $derived(can('*') ? 'ADMIN' : can('data.manage') ? 'DATA MANAGER' : can('data.approve') ? 'APPROVER' : contributorEnabled ? 'CONTRIBUTOR' : 'READ-ONLY'); + const dataMode = $derived(can('*') ? 'ADMIN' : can('data.approve') ? 'OVERSIGHT' : can('data.manage') ? 'DATA MANAGER' : contributorEnabled ? 'CONTRIBUTOR' : 'READ-ONLY'); const dataModeClass = $derived(dataMode.toLowerCase().replace(' ', '-')); let readOnlyBlocked = $state(false); const dataSections = [ @@ -23,7 +23,7 @@ ]; const visibleUserSections = $derived(userSections.filter((section) => can(section.permission))); const oversightSections = [ - { href: '/admin/data-moderation', label: 'Data change queue', code: 'CHNG', permission: 'data.approve' }, + { href: '/admin/data-moderation', label: 'Data Oversight', code: 'OVST', permission: 'data.approve' }, { href: '/admin/audit', label: 'Audit history', code: 'AUDT', permission: 'audit.view' }, { href: '/admin/metrics', label: 'System metrics', code: 'MTRX', permission: 'metrics.view' } ]; @@ -33,7 +33,7 @@

INTELLIGENT SYSTEM ANALYTIC COMPUTER

ISAC

SHD.build operations and intelligence control surface

-
SYSTEMONLINE
ACCESSAUTHORIZED
ENVIRONMENTLOCAL
DATA MODE{dataMode}
+
SYSTEMONLINE
ACCESSAUTHORIZED
ENVIRONMENTLOCAL
DATA MODE{dataMode}
{date(proposal.created_at)} {proposal.status.toUpperCase()} - {#if proposal.status === 'pending'}{:else}{/if} + {#if proposal.status === 'pending'}{:else}{/if} {:else}
NO {data.status.toUpperCase()} CHANGE REQUESTSAdjust the module or search filters to inspect another part of the queue.
{/each} @@ -59,7 +69,27 @@
{JSON.stringify(evidence(reviewing.payload_json), null, 2)}
{#if reviewing.review_note}
OVERSIGHT NOTE

{reviewing.review_note}

{/if} {#if reviewing.status === 'pending'} -
+
+ {#if decision === 'correct'} + + {#snippet children(pending)} + +
OVERSIGHT CORRECTIONSFILES REMAIN IMMUTABLE · CHANGES ARE AUDIT LOGGED
{correctionEntries.filter((entry) => entry.kind === 'text').length} EDITABLE FIELDS
+
+ {#each correctionEntries as entry} + {#if entry.kind === 'text'} + + {:else} +
{operationLabel(entry.name)}{entry.fileName}ORIGINAL FILE EVIDENCE · NOT EDITABLE
+ {/if} + {/each} +
+ {#if form?.moderationError}

{form.moderationError}

{/if} +
!SAVE ONLY MINOR CORRECTIONS. MATERIAL CHANGES SHOULD BE REJECTED FOR CONTRIBUTOR REVISION.
+
+ {/snippet} +
+ {:else} {#snippet children(pending)} @@ -69,6 +99,7 @@
{/snippet}
+ {/if} {:else}
{/if} diff --git a/src/routes/terms/+page.svelte b/src/routes/terms/+page.svelte index d2eb9db..7761f0b 100644 --- a/src/routes/terms/+page.svelte +++ b/src/routes/terms/+page.svelte @@ -22,7 +22,7 @@
01

THE SERVICE

SHD.build is an independent community loadout, build-guide, and discussion service. By accessing or using it, you agree to these terms. If you do not agree, do not use the service. Features may change, be suspended, or be discontinued as development continues.

02

ACCOUNTS & SECURITY

You are responsible for activity performed through your account and for maintaining control of your authentication methods. You must provide accurate information, may not impersonate another person, and may not attempt to access another account. We may restrict accounts to protect users, enforce these terms, or comply with law.

Available authentication methods may include passkeys, verified email magic links, local username and password credentials, Discord, and other providers added later. Third-party authentication remains subject to that provider’s terms and privacy practices.

03

USER CONTENT

You retain ownership of original content you submit. You grant SHD.build a non-exclusive, worldwide, royalty-free license to host, reproduce, format, display, and distribute that content as necessary to operate the service. You represent that you have the rights required to post it.

Builds, guides, and comments may remain available after account deletion under a “Deleted Agent” attribution so community references are not broken. You may not upload unlawful, infringing, malicious, or privacy-invasive content.

-
04

COMMUNITY CONDUCT

Do not harass others; publish illegal or infringing material; distribute malware; evade access restrictions; scrape or overload the service; manipulate ratings; spam; or exploit security vulnerabilities. Moderators may remove content, restrict access, or preserve relevant records when reasonably necessary for safety, support, abuse prevention, or legal obligations.

+
04

COMMUNITY CONDUCT

Do not harass others; publish illegal or infringing material; distribute malware; evade access restrictions; scrape or overload the service; manipulate ratings; spam; or exploit security vulnerabilities. Community Oversight may remove content, restrict access, or preserve relevant records when reasonably necessary for safety, support, abuse prevention, or legal obligations.

05

INFORMATION WE PROCESS

Account data may include internal account identifiers, private login username, public display name, password hash, verified email, linked-provider identifiers and profile details, passkey public credentials, avatar, roles, permissions, builds, guides, favorites, ratings, and comments. Passwords are stored only as salted Argon2id hashes. Private passkey material remains on your authenticator.

For login security and support, events may include the time, success status, authentication provider, IP address, user-agent string, normalized browser, operating system and device type, approximate country/region/city when supplied by the hosting proxy, failure reason, automated risk flags, provider or passkey credential reference, associated session identifier, and session end time or reason. We do not sell this information. It is used for authentication, abuse prevention, troubleshooting, moderation, and account support.

OAuth and email providers process information under their own policies.

06

ESSENTIAL COOKIES

SHD.build currently uses only first-party cookies that are strictly necessary to provide authentication and protect sign-in flows. We do not currently use analytics, advertising, behavioral-tracking, or personalization cookies. These essential cookies cannot be disabled through a preference control. You may prevent cookies through your browser, but account sign-in will not function correctly.

The informational notice remembers its dismissal in browser local storage under shd-essential-cookie-notice. It is not sent with web requests and can be removed through your browser’s site-data controls. OAuth state remains in a short-lived HTTP-only cookie because it must be securely validated by the server and unavailable to browser scripts.

If non-essential technologies are introduced later, this policy and the site’s consent controls will be updated before those technologies are enabled where consent is required.

07

RETENTION & DELETION

IP addresses, user-agent strings, normalized device information, and credential references in login history are retained for no more than 30 days, then automatically removed from the event. Non-sensitive facts such as event time, provider, outcome, failure category, and whether a session was created may remain for security and support history.

Deleting your account removes active sessions, credentials, verified email, provider profiles, passkeys, permissions, and uploaded avatar. You may also select immediate purging of retained login-event history during self-deletion. Public contributions may remain attributed to “Deleted Agent.” Backups and records required for fraud prevention, dispute resolution, or law may persist for a limited period where applicable.