Convert in-game wording into a reviewable draft. Manual authoring remains available.
",
+ "css": ".ds-assist{border:1px solid var(--shd-model-border,#3b4648);border-left:2px solid var(--shd-info,#70b9d6);border-radius:0;background:var(--shd-model-surface,#0d1213);color:var(--shd-text,#ecebe5);font-family:Barlow,Arial,sans-serif}.ds-assist header{min-height:46px;display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:8px;padding:7px 9px;border-bottom:1px solid var(--shd-border,#303536);background:var(--shd-surface-raised,#141718)}.ds-assist header div{display:grid;gap:2px}.ds-assist span,.ds-assist b{color:var(--shd-info,#70b9d6);font-size:12px;font-weight:900;letter-spacing:.9px}.ds-assist b{padding:5px 7px;border:1px solid var(--shd-model-border,#3b4648)}.ds-assist button{min-height:40px;padding:0 11px;border:1px solid var(--shd-info,#70b9d6);border-radius:0;background:transparent;color:var(--shd-info,#70b9d6);font-size:12px;font-weight:900;letter-spacing:.7px;cursor:pointer}.ds-assist button:hover{background:#152225}.ds-assist button:focus-visible{outline:1px solid var(--shd-orange,#f6a821);outline-offset:2px}.ds-assist button:active{transform:translateY(1px);filter:brightness(.84)}.ds-assist p{margin:0;padding:10px 11px;color:var(--shd-text-muted,#858b8d);font-size:14px;line-height:1.45}"
}
],
"narrative": {
"northStar": "The Strategic Operations Grid",
- "overview": "SHD.build behaves like a dependable intelligence network shared by active agents: precise, restrained, tactical, and structured around trustworthy data. The interface communicates through alignment, density, operational labels, and visible state rather than spectacle. Public surfaces can breathe and use larger editorial hierarchy, while ISAC compresses the same identity into an efficient control surface.\n\nThe world is technical without becoming sterile. Fine structural lines, tonally layered black and graphite surfaces, cropped game iconography, clipped silhouettes, and tactile state changes create its character. Theme language supports comprehension; it never becomes military cosplay.",
+ "overview": "SHD.build serves active agents through a dependable intelligence network: precise, restrained, tactical, and built around trustworthy data. Alignment, density, operational labels, and visible state carry the interface. Public surfaces use space and larger editorial hierarchy. ISAC compresses the same identity into an efficient control surface.\n\nFine structural lines, layered black and graphite surfaces, cropped game iconography, clipped silhouettes, and tactile state changes give the interface its technical character. Theme language supports comprehension and avoids military cosplay.",
"keyCharacteristics": [
"Dark, high-contrast operational canvas with restrained tonal layering.",
"Square engineered geometry, except circular agent avatars and the SHD mark.",
@@ -258,17 +284,22 @@
"rules": [
{
"name": "The Orange Discipline Rule",
- "body": "Signal Orange identifies action and emphasis; it does not cover large decorative areas or compete with semantic state colors.",
+ "body": "Use Signal Orange for action and emphasis. Keep it out of large decorative areas and away from semantic state colors.",
"section": "colors"
},
{
"name": "The Redundant Meaning Rule",
- "body": "Color always travels with text, position, shape, or an icon. No status or quality can depend on hue alone.",
+ "body": "Pair color with text, position, shape, or an icon. A status or quality cannot depend on hue alone.",
"section": "colors"
},
{
"name": "The Two-Voice Rule",
- "body": "Sans-serif operates the system; serif explains it. Never use monospace merely to make an interface look technical.",
+ "body": "Sans-serif operates the system; serif explains it. Do not use monospace to make an interface look technical.",
+ "section": "typography"
+ },
+ {
+ "name": "The Telemetry Restraint Rule",
+ "body": "Uppercase tracked labels are short navigational or state-bearing signals, not paragraph typography.",
"section": "typography"
},
{
@@ -276,15 +307,30 @@
"body": "Use borders, shared edges, and aligned tracks to show relationships. Do not wrap every subsection in another card.",
"section": "layout"
},
+ {
+ "name": "The Operational Density Rule",
+ "body": "Public discovery may breathe; ISAC and data workbenches prioritize scanability and efficient repeated actions.",
+ "section": "layout"
+ },
{
"name": "The Structural Layering Rule",
"body": "Borders and tones establish permanent hierarchy; shadows communicate temporary detachment.",
"section": "elevation"
},
+ {
+ "name": "The No Ambient Card Rule",
+ "body": "Ordinary panels and cards remain shadowless at rest.",
+ "section": "elevation"
+ },
{
"name": "The Engineered Edge Rule",
"body": "Square is the default and rounding must have semantic justification.",
"section": "shapes"
+ },
+ {
+ "name": "The Identity Circle Rule",
+ "body": "Circular crops belong to people, the SHD emblem, and radial information, not arbitrary controls.",
+ "section": "shapes"
}
],
"dos": [
@@ -293,6 +339,7 @@
"Do pair every state color and icon with explicit text.",
"Do use reusable segmented choices for bounded sets and searchable assignments for large relational sets.",
"Do preserve tactile pressed, focus-visible, pending, disabled, error, success, and reduced-motion behavior.",
+ "Do keep AI-generated data inside an explicit draft-and-review boundary while preserving manual authoring controls.",
"Do let public editorial surfaces breathe while keeping ISAC dense and task-oriented."
],
"donts": [
diff --git a/DESIGN.md b/DESIGN.md
index 3205474..c66db03 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -7,8 +7,11 @@ colors:
terminal-surface: "#111415"
terminal-graphite: "#141718"
active-graphite: "#15191a"
+ modeled-data-surface: "#0d1213"
structural-line: "#303536"
structural-line-soft: "#272c2d"
+ structural-line-strong: "#424849"
+ modeled-data-line: "#3b4648"
field-manual-white: "#ecebe5"
telemetry-muted: "#858b8d"
telemetry-dim: "#5d6567"
@@ -100,6 +103,11 @@ components:
textColor: "{colors.field-manual-white}"
rounded: "{rounded.engineered}"
padding: "{spacing.panel}"
+ assistant-panel:
+ backgroundColor: "{colors.modeled-data-surface}"
+ textColor: "{colors.field-manual-white}"
+ rounded: "{rounded.engineered}"
+ padding: "{spacing.md}"
status-chip:
backgroundColor: "{colors.active-graphite}"
textColor: "{colors.telemetry-muted}"
@@ -114,11 +122,11 @@ components:
**Creative North Star: "The Strategic Operations Grid"**
-SHD.build serves active agents as a dependable intelligence network: precise,
-restrained, tactical, and built around trustworthy data. Alignment, density,
-operational labels, and visible state carry the interface. Public surfaces use
-space and larger editorial hierarchy; ISAC uses the same identity in an
-efficient control surface.
+SHD.build serves active agents through a dependable intelligence network:
+precise, restrained, tactical, and built around trustworthy data. Alignment,
+density, operational labels, and visible state carry the interface. Public
+surfaces use space and larger editorial hierarchy. ISAC compresses the same
+identity into an efficient control surface.
Fine structural lines, layered black and graphite surfaces, cropped game
iconography, clipped silhouettes, and tactile state changes give the interface
@@ -178,35 +186,40 @@ Signal Orange, and semantic system and item-quality signals.
### Primary
-- **Signal Orange:** Primary actions, current navigation, focus, selected
+- **Signal Orange** (`#f6a821`): Primary actions, current navigation, focus, selected
controls, important values, and SHD identity details.
-- **Signal Orange Hover:** Confirms that an orange action accepts input; never
+- **Signal Orange Hover** (`#ffc45d`): Confirms that an orange action accepts input; never
use it as a decorative accent.
### Secondary
-- **System Success:** Completed, approved, online, and healthy states.
-- **System Danger:** Failed, rejected, blocked, destructive, and fault states.
-- **System Info:** Informational system state that is neither action nor
+- **System Success** (`#75c08c`): Completed, approved, online, and healthy states.
+- **System Danger** (`#df8069`): Failed, rejected, blocked, destructive, and fault states.
+- **System Info** (`#70b9d6`): Informational system state that is neither action nor
success.
### Tertiary
-- **Quality Standard, Specialized, Superior, and High-end:** Stable
- game-semantic quality colors. They identify item quality and never replace a
- textual quality label.
+- **Quality Standard** (`#69b66f`), **Specialized** (`#4f9ed8`),
+ **Superior** (`#9b68d4`), and **High-end** (`#e4bd3f`): Stable
+ game-semantic quality colors. They identify item quality and never replace
+ a textual quality label.
### Neutral
-- **Operations Black:** The persistent application canvas.
-- **Terminal Black:** Recessed fields and data wells.
-- **Terminal Surface:** Navigation, registry, and low-level panel surfaces.
-- **Terminal Graphite:** Raised working panels and cards.
-- **Active Graphite:** Hovered, selected, and locally emphasized surfaces.
-- **Structural Line / Structural Line Soft:** Primary and subordinate
+- **Operations Black** (`#0a0c0d`): The persistent application canvas.
+- **Terminal Black** (`#0d0f10`): Recessed fields and data wells.
+- **Terminal Surface** (`#111415`): Navigation, registry, and low-level panel surfaces.
+- **Terminal Graphite** (`#141718`): Raised working panels and cards.
+- **Active Graphite** (`#15191a`): Hovered, selected, and locally emphasized surfaces.
+- **Modeled Data Surface** (`#0d1213`): Recessed mechanic programs, calculation revisions,
+ generated script, and AI-assisted drafting workspaces.
+- **Structural Line** (`#303536`) / **Structural Line Soft** (`#272c2d`): Primary and subordinate
separators that create hierarchy without elevation.
-- **Field Manual White:** Primary readable content.
-- **Telemetry Muted / Telemetry Dim:** Secondary labels and low-priority
+- **Structural Line Strong** (`#424849`) / **Modeled Data Line** (`#3b4648`): Interactive boundaries and
+ blue-railed modeled-data panels that need more separation from the registry.
+- **Field Manual White** (`#ecebe5`): Primary readable content.
+- **Telemetry Muted** (`#858b8d`) / **Telemetry Dim** (`#5d6567`): Secondary labels and low-priority
operational metadata.
**The Orange Discipline Rule.** Use Signal Orange for action and emphasis. Keep
@@ -240,7 +253,7 @@ operational data distinct from editorial knowledge.
long passages stay near 65–75 characters per line.
- **Editorial** (400, 1rem, 1.65 line-height): Explanations, introductions, and
long-form descriptions.
-- **Label** (900, 0.625rem, 0.1em tracking): Uppercase telemetry, field labels,
+- **Label** (900, 0.75rem, 0.08em tracking): Uppercase telemetry, field labels,
statuses, metadata, and compact actions.
**The Two-Voice Rule.** Sans-serif operates the system; serif explains it. Do
@@ -392,13 +405,25 @@ Large or relational option sets use a searchable assignment instrument with a
selected summary, removable assignments, live match count, and an explicit
result action. Pending records remain usable and visibly attributed.
+### Modeled Authoring Assist
+
+Mechanic and calculation drafting helpers use a compact Modeled Data Surface
+with a blue information rail. Their headers pair a plain-language task name
+with an explicit `AUTHORIZED` or `PERMISSION REQUIRED` state. Authors without
+permission retain the complete manual editor.
+
+The expanded workspace keeps source wording or calculation requirements
+visible while it presents one result state: pending analysis, clarification,
+capability gap, failure, or draft preview. A generated draft never changes the
+form until the author confirms Apply. Save, proposal, publication, and
+activation remain separate actions in the owning editor.
+
### Registry and Database Cards
Registry rows prioritize names, durable statuses, relationships, and edit
actions within consistent columns. Public database cards use an identity
header, icon, metadata, compact fact matrices, and optional relationship icon
-rails. Facts disappear when they do not apply rather than displaying empty
-placeholders.
+rails. Facts with no applicable value stay hidden.
### Toasts and Dialogs
@@ -421,6 +446,8 @@ entrance animation.
assignments for large relational sets.
- **Do** preserve tactile pressed, focus-visible, pending, disabled, error,
success, and reduced-motion behavior.
+- **Do** keep AI-generated data inside an explicit draft-and-review boundary
+ while preserving manual authoring controls.
- **Do** let public editorial surfaces breathe while keeping ISAC dense and
task-oriented.
diff --git a/database/schema.sql b/database/schema.sql
index b6f3033..669d364 100644
--- a/database/schema.sql
+++ b/database/schema.sql
@@ -3,9 +3,19 @@ CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, username TEXT NOT NULL UN
CREATE TABLE IF NOT EXISTS user_profiles (
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
display_name TEXT NOT NULL,
+ bio_markdown TEXT NOT NULL DEFAULT '',
+ is_public INTEGER NOT NULL DEFAULT 1 CHECK(is_public IN (0, 1)),
+ bio_updated_at TEXT,
updated_at TEXT NOT NULL
);
INSERT OR IGNORE INTO user_profiles(user_id,display_name,updated_at) SELECT id,username,created_at FROM users;
+CREATE TABLE IF NOT EXISTS profile_moderation (
+ user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
+ bio_hidden INTEGER NOT NULL DEFAULT 0 CHECK(bio_hidden IN (0, 1)),
+ reason TEXT,
+ moderated_at TEXT,
+ moderated_by TEXT REFERENCES users(id)
+);
CREATE TABLE IF NOT EXISTS user_avatars (
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
source TEXT NOT NULL CHECK(source IN ('preset', 'upload')),
@@ -127,7 +137,28 @@ CREATE INDEX IF NOT EXISTS avatar_reviews_user_submitted_idx ON avatar_reviews(u
CREATE INDEX IF NOT EXISTS builds_visibility_updated_idx ON builds(visibility, updated_at DESC);
CREATE TABLE IF NOT EXISTS build_items (id TEXT PRIMARY KEY, build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, slot_id TEXT NOT NULL, item_definition_id TEXT NOT NULL, configuration_json TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS ratings (user_id TEXT NOT NULL REFERENCES users(id), build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, value INTEGER NOT NULL CHECK(value BETWEEN 1 AND 5), PRIMARY KEY(user_id, build_id));
-CREATE TABLE IF NOT EXISTS comments (id TEXT PRIMARY KEY, build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id), body TEXT NOT NULL, created_at TEXT NOT NULL);
+CREATE TABLE IF NOT EXISTS comment_threads (
+ id TEXT PRIMARY KEY,
+ target_type TEXT NOT NULL CHECK(target_type IN ('build', 'guide', 'profile')),
+ target_id TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ UNIQUE(target_type, target_id)
+);
+CREATE TABLE IF NOT EXISTS comments (
+ id TEXT PRIMARY KEY,
+ thread_id TEXT NOT NULL REFERENCES comment_threads(id) ON DELETE CASCADE,
+ author_id TEXT NOT NULL REFERENCES users(id),
+ parent_id TEXT REFERENCES comments(id),
+ body_markdown TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'visible' CHECK(status IN ('visible', 'removed')),
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ removed_at TEXT,
+ removed_by TEXT REFERENCES users(id),
+ removal_reason TEXT
+);
+CREATE INDEX IF NOT EXISTS comments_thread_status_created_idx ON comments(thread_id, status, created_at DESC);
+CREATE INDEX IF NOT EXISTS comments_author_status_created_idx ON comments(author_id, status, created_at DESC);
CREATE TABLE IF NOT EXISTS favorites (user_id TEXT NOT NULL REFERENCES users(id), build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, created_at TEXT NOT NULL, PRIMARY KEY(user_id, build_id));
CREATE TABLE IF NOT EXISTS equipment_types (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, category TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS stat_definitions (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, value_kind TEXT NOT NULL, min_value REAL, max_value REAL, unit TEXT, active INTEGER NOT NULL DEFAULT 1);
@@ -242,6 +273,8 @@ INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-metric
INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-system-notifications-manage','system.notifications.manage','View and edit automated ISAC system notification messages');
INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-images-manage','images.manage','View, upload, rename, tag, and categorize managed image assets');
INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-avatars-review','avatars.review','Review and remove custom user avatars');
+INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-community-comments-moderate','community.comments.moderate','Review and moderate public build, guide, and agent-profile comments');
+INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-community-profiles-moderate','community.profiles.moderate','Review and hide public agent-profile biographies');
INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-calculation-publish','calculation.publish','Activate combat calculation revisions used by every loadout');
INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-ai-mechanic-generate','ai.mechanic.generate','Use the OpenAI-assisted mechanic drafting workflow');
INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-ai-calculation-generate','ai.calculation.generate','Use the OpenAI-assisted combat calculation revision drafting workflow');
@@ -277,6 +310,8 @@ INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-audit
INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-auditor','permission-metrics-view');
INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moderator','permission-isac-access');
INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moderator','permission-avatars-review');
+INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moderator','permission-community-comments-moderate');
+INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-moderator','permission-community-profiles-moderate');
CREATE TABLE IF NOT EXISTS oauth_states (state_hash TEXT PRIMARY KEY, provider TEXT NOT NULL, mode TEXT NOT NULL, user_id TEXT REFERENCES users(id) ON DELETE CASCADE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS data_change_proposals (
id TEXT PRIMARY KEY,
diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md
index 7d214c7..975105e 100644
--- a/docs/DESIGN_SYSTEM.md
+++ b/docs/DESIGN_SYSTEM.md
@@ -445,6 +445,62 @@ direct URL. Do not present authentication as a way around a disabled section.
remain unique, ASCII-only credentials and must never appear on public-facing
community surfaces. Authorized ISAC user operations may show both.
+### Public Agent profiles
+
+Public Agent profiles are community dossiers, not account-control screens. Lead
+with the shared `AgentIdentityLink` in its large state: circular portrait,
+display name, explicit `SHD.BUILD AGENT PROFILE` context, and a directional
+link cue. The profile owner may receive an `EDIT MY PROFILE` action in the
+header. Keep public content totals in a compact telemetry strip below the
+identity header, then place the field bio, public builds, published guides, and
+recent discussion in the primary column. Profile messages occupy the narrower
+communications column and may remain sticky where the viewport has room.
+
+Use `AgentIdentityLink` for author attribution on builds, guides, comments, and
+other community records. Its compact and default states combine the circular
+portrait, public display name, optional contextual metadata, and a clear link
+cue. Hover and focus-visible states may turn the name and cue orange. The
+portrait remains circular at every size; selectors may use square tactical
+thumbnails only where the avatar rules explicitly allow them.
+
+Public biographies and community comments use the shared `MarkdownEditor` and
+`MarkdownContent` pair. The editor supplies a small formatting toolbar,
+remaining-character count, concise help, a readable editorial typeface, and a
+minimum 44px touch target for toolbar actions on narrow screens. It supports
+headings, lists, quotes, links, emphasis, and inline code. Raw HTML is escaped.
+Only HTTP and HTTPS links are active; rendered links open separately with UGC,
+nofollow, and opener-isolation attributes. `MarkdownContent` must receive HTML
+from the server renderer, never raw user input or client-rendered Markdown.
+
+Rendered biography copy uses the editorial typeface and a restrained reading
+measure. Compact comment rendering uses the same hierarchy at a denser scale.
+Comment cards identify the author and date before the message. Activity cards
+identify the content type and title, render a useful excerpt, and provide a
+specific action such as `OPEN DISCUSSION`. Empty states state which public
+record is absent without implying an error.
+
+Profile visibility is an explicit public or hidden account setting. A hidden
+profile is excluded from public discovery and cannot receive new profile
+messages. Hiding it does not remove the Agent's display name or portrait from
+public builds, guides, or comments they authored. State this consequence beside
+the visibility control so the setting does not imply retroactive anonymity.
+
+Community forms use `PendingForm` to block duplicate submissions. Pending
+actions retain their label context, validation errors preserve entered text,
+and success or error feedback appears beside the affected form. All editor,
+link, and button controls require the shared orange focus-visible treatment.
+Message removal is a two-step, in-card confirmation: the first action reveals a
+red danger panel, then separate Cancel and Confirm Remove actions complete or
+abandon the request. Only an authorized author receives the removal control.
+
+Below 1000px, move profile communications beneath the primary content and stop
+using the sticky position. Below 650px, stack the identity header, telemetry,
+section headers, and content cards into one column. Expand owner actions,
+submission controls, and deletion controls to 44px touch targets. Keep the
+Cancel and Confirm Remove actions side by side when both labels fit. Respect
+`prefers-reduced-motion` without removing static hover, focus, pending, success,
+error, or danger indicators.
+
### Third-party identity buttons
- Follow the identity provider's current official brand and sign-in guidelines.
diff --git a/scripts/db-migrate.mjs b/scripts/db-migrate.mjs
index 5b9d05e..1a13f1a 100644
--- a/scripts/db-migrate.mjs
+++ b/scripts/db-migrate.mjs
@@ -131,6 +131,71 @@ if (usersTable) {
}
}
+const userProfilesTable = await (await database.prepare(
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name='user_profiles'"
+)).get();
+if (userProfilesTable) {
+ const profileColumns = await (await database.prepare("PRAGMA table_info('user_profiles')")).all();
+ if (!profileColumns.some((column) => column.name === 'bio_markdown')) {
+ console.log('Adding Markdown biographies to public agent profiles.');
+ await database.exec("ALTER TABLE user_profiles ADD COLUMN bio_markdown TEXT NOT NULL DEFAULT ''");
+ }
+ if (!profileColumns.some((column) => column.name === 'is_public')) {
+ console.log('Adding public profile visibility controls.');
+ await database.exec('ALTER TABLE user_profiles ADD COLUMN is_public INTEGER NOT NULL DEFAULT 1 CHECK(is_public IN (0, 1))');
+ }
+ if (!profileColumns.some((column) => column.name === 'bio_updated_at')) {
+ await database.exec('ALTER TABLE user_profiles ADD COLUMN bio_updated_at TEXT');
+ }
+}
+
+const commentsDefinition = await (await database.prepare(
+ "SELECT sql FROM sqlite_master WHERE type='table' AND name='comments'"
+)).get();
+if (typeof commentsDefinition?.sql === 'string' && commentsDefinition.sql.includes('build_id')) {
+ console.log('Normalizing build comments into reusable community comment threads.');
+ await database.exec('PRAGMA foreign_keys=OFF');
+ await database.exec('BEGIN');
+ try {
+ await database.exec('ALTER TABLE comments RENAME TO comments_legacy');
+ await database.exec(`CREATE TABLE comment_threads (
+ id TEXT PRIMARY KEY,
+ target_type TEXT NOT NULL CHECK(target_type IN ('build', 'guide', 'profile')),
+ target_id TEXT NOT NULL,
+ created_at TEXT NOT NULL,
+ UNIQUE(target_type, target_id)
+ )`);
+ await database.exec(`CREATE TABLE comments (
+ id TEXT PRIMARY KEY,
+ thread_id TEXT NOT NULL REFERENCES comment_threads(id) ON DELETE CASCADE,
+ author_id TEXT NOT NULL REFERENCES users(id),
+ parent_id TEXT REFERENCES comments(id),
+ body_markdown TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'visible' CHECK(status IN ('visible', 'removed')),
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL,
+ removed_at TEXT,
+ removed_by TEXT REFERENCES users(id),
+ removal_reason TEXT
+ )`);
+ await database.exec(`INSERT INTO comment_threads(id,target_type,target_id,created_at)
+ SELECT 'build:' || build_id,'build',build_id,MIN(created_at)
+ FROM comments_legacy GROUP BY build_id`);
+ await database.exec(`INSERT INTO comments(
+ id,thread_id,author_id,parent_id,body_markdown,status,created_at,updated_at
+ )
+ SELECT id,'build:' || build_id,user_id,NULL,body,'visible',created_at,created_at
+ FROM comments_legacy`);
+ await database.exec('DROP TABLE comments_legacy');
+ await database.exec('COMMIT');
+ } catch (error) {
+ await database.exec('ROLLBACK');
+ throw error;
+ } finally {
+ await database.exec('PRAGMA foreign_keys=ON');
+ }
+}
+
// The project schema deliberately avoids semicolons inside string literals.
const statements = schema
.split(';')
diff --git a/src/app.css b/src/app.css
index dca9b71..9783351 100644
--- a/src/app.css
+++ b/src/app.css
@@ -88,7 +88,7 @@
.site-header .account small i{display:inline-block;width:5px;height:5px;border-radius:50%;background:var(--shd-success);box-shadow:0 0 6px var(--shd-success);margin-right:3px}
.catalogue-page,.guides-page,.profile-page{padding-top:34px}.catalogue-hero{min-height:230px;display:flex;justify-content:space-between;align-items:center;padding:25px 32px;margin-bottom:12px;border:1px solid var(--shd-border);background:radial-gradient(circle at 80% 50%,#332714 0,transparent 30%),linear-gradient(120deg,#141718,#0d1011);clip-path:polygon(0 0,97% 0,100% 18%,100% 100%,3% 100%,0 82%)}.catalogue-hero h1{font-size:clamp(48px,6vw,82px);line-height:.82;letter-spacing:-3px;margin:14px 0}.catalogue-hero h1 em{font-style:normal;color:var(--shd-orange)}.catalogue-hero>div>p:last-child{max-width:580px;color:var(--shd-text-muted);font:15px/1.55 var(--shd-font-editorial)}.catalogue-count{min-width:210px;display:grid;text-align:right}.catalogue-count strong{font-size:38px}.catalogue-count span{font-size:var(--shd-type-telemetry);color:#6d7476;letter-spacing:1.5px;margin:3px 0 18px}.catalogue-count .primary{justify-self:end}.filter-bar{display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:7px;align-items:end;margin:12px 0 18px;padding:11px;background:#101314;border:1px solid #2d3233}.filter-bar label{font-size:var(--shd-type-telemetry);color:#72797b;letter-spacing:1.2px}.filter-bar input,.filter-bar select{height:38px;margin-top:6px;padding:0 10px;font-size:var(--shd-type-telemetry)}.filter-bar button{height:38px;padding:0 14px}.catalogue-grid{max-width:none}.catalogue-grid article{position:relative}.build-open{width:100%;margin-top:15px;padding:10px;border:1px solid #3a4041;background:#101314;color:#f6a821;font-size:var(--shd-type-telemetry);font-weight:bold;letter-spacing:1px}.guide-layout{display:grid;grid-template-columns:1fr 330px;gap:12px}.guide-feature{min-height:360px;padding:35px;position:relative;border:1px solid #303536;border-top:2px solid #f6a821;background:linear-gradient(135deg,#151819,#0d1011)}.guide-code{position:absolute;right:25px;top:22px;color:#4f5658;font-size:var(--shd-type-telemetry);letter-spacing:1.5px}.guide-feature h2{font-size:42px;line-height:.95;margin:20px 0}.guide-feature>p:not(.eyebrow){max-width:650px;color:#858b8d;font:15px/1.6 var(--shd-font-editorial)}.guide-meta{display:flex;gap:8px;position:absolute;left:35px;bottom:30px}.guide-meta span{display:grid;padding:8px 11px;border:1px solid #303536;color:#60686a;font-size:var(--shd-type-telemetry);letter-spacing:1px}.guide-meta b{color:#d7d9d5;font-size:var(--shd-type-telemetry);margin-top:4px}.guide-index{border:1px solid #303536;background:#111415;padding:20px}.guide-index>div{display:grid;grid-template-columns:30px 1fr auto;align-items:center;min-height:54px;border-top:1px solid #292e2f}.guide-index small{color:#535b5d;font-size:var(--shd-type-telemetry)}.guide-index span{font-size:var(--shd-type-telemetry);letter-spacing:.8px}.guide-index b{color:#f6a821}
.profile-command{display:flex;justify-content:space-between;align-items:center;padding:0 0 24px;border-bottom:1px solid #303536}.profile-ident{display:flex;align-items:center;gap:18px}.profile-avatar{width:76px;height:76px;display:block;object-fit:cover;border:1px solid #f6a821;border-radius:50%;background:#0a0c0d;box-shadow:0 0 0 6px #0a0c0d,0 0 0 7px #f6a82155}.profile-ident h1{font-size:39px;margin:4px 0}.role-badge{display:inline-block;padding:4px 8px;border:1px solid #6e5226;color:#f6a821;font-size:var(--shd-type-telemetry);letter-spacing:1.3px}.profile-metrics{display:flex;gap:7px}.profile-metrics div{min-width:77px;height:59px;display:grid;place-items:center;padding:7px;background:#111415;border:1px solid #303536}.profile-metrics strong{font-size:20px}.profile-metrics span{font-size:var(--shd-type-telemetry);color:#697174;letter-spacing:1px}.account-alert{display:flex;gap:16px;margin:12px 0;padding:12px 15px;background:#2a2112;border:1px solid #795c2a;border-left:3px solid #f6a821;font-size:var(--shd-type-telemetry)}.account-alert b{color:#f6a821}.account-alert span{color:#b8ad99}.profile-dashboard{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:12px;margin-top:12px}.profile-feed{display:grid;gap:12px}.profile-module,.account-console{background:#111415;border:1px solid #303536;padding:16px}.module-head{display:flex;justify-content:space-between;align-items:center}.module-head h2,.profile-module h2,.account-console h2{font-size:17px;margin:4px 0 12px}.module-head .secondary{padding:10px 12px}.profile-table{border:1px solid #292e2f}.profile-row{display:grid;grid-template-columns:1.5fr .7fr .7fr .3fr;align-items:center;gap:9px;min-height:47px;padding:7px 11px;border-top:1px solid #292e2f;font-size:var(--shd-type-telemetry)}.profile-row.heading{min-height:30px;border:0;background:#0d1011;color:#636b6d;font-size:var(--shd-type-telemetry);letter-spacing:1px}.profile-row>div{display:grid}.profile-row small{font-size:var(--shd-type-telemetry);color:#596164}.profile-row>span{text-transform:uppercase;color:#858d8f}.profile-row button{border:0;background:transparent;color:#f6a821;font-size:var(--shd-type-telemetry)}.profile-empty{text-align:center;padding:38px}.profile-empty>span{font-size:31px;color:#f6a821}.profile-empty b{display:block;font-size:var(--shd-type-telemetry);letter-spacing:1px;margin:10px}.profile-empty p{color:#71797b;font:13px var(--shd-font-editorial)}.profile-empty a{color:#f6a821;font-size:var(--shd-type-telemetry);font-weight:bold}.profile-columns{display:grid;grid-template-columns:1fr 1fr;gap:12px}.large-count{font-size:32px}.count-label{display:block;font-size:var(--shd-type-telemetry);color:#687073;letter-spacing:1.2px}.account-console{height:max-content}.console-head{display:flex;justify-content:space-between;padding-bottom:11px;margin-bottom:15px;border-bottom:1px solid #303536;color:#747c7e;font-size:var(--shd-type-telemetry);letter-spacing:1.3px}.console-head i{width:6px;height:6px;border-radius:50%;background:#75c08c;box-shadow:0 0 6px #75c08c}.console-action{width:100%;margin-top:9px}.connection-card{display:grid;grid-template-columns:35px 1fr auto;align-items:center;padding:10px;border:1px solid #303536}.discord-node{color:#8993ff;font-size:21px}.connection-card div{display:grid}.connection-card b{font-size:var(--shd-type-telemetry)}.connection-card small{font-size:var(--shd-type-telemetry);color:#656d6f;margin-top:3px}.connection-card a{color:#f6a821;font-size:var(--shd-type-telemetry);font-weight:bold}
-.avatar-picker{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:10px}.avatar-preset-form fieldset{height:100%}.avatar-preset{display:grid;gap:5px;width:100%;height:100%;padding:4px;background:#0b0e0f;border:1px solid #303536;color:#747c7e}.avatar-preset:hover,.avatar-preset:focus-visible{border-color:#8a672b;color:#f6a821}.avatar-preset.selected{border-color:#f6a821;color:#f6a821;box-shadow:inset 0 -2px #f6a821}.avatar-preset img{display:block;width:100%;aspect-ratio:1;object-fit:cover}.avatar-preset span{font-size:var(--shd-type-telemetry);line-height:1.25;text-transform:uppercase;letter-spacing:.4px}.avatar-upload{margin-top:8px}
+.avatar-picker{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin-bottom:10px}.avatar-preset-form,.avatar-preset-form fieldset{min-width:0;height:100%}.avatar-preset{display:grid;gap:5px;width:100%;height:100%;min-width:0;padding:4px;background:#0b0e0f;border:1px solid #303536;color:#747c7e}.avatar-preset:hover,.avatar-preset:focus-visible{border-color:#8a672b;color:#f6a821}.avatar-preset.selected{border-color:#f6a821;color:#f6a821;box-shadow:inset 0 -2px #f6a821}.avatar-preset img{display:block;width:100%;aspect-ratio:1;object-fit:cover}.avatar-preset span{min-width:0;overflow-wrap:anywhere;font-size:var(--shd-type-telemetry);line-height:1.25;text-transform:uppercase;letter-spacing:.4px}.avatar-upload{min-width:0;margin-top:8px}.account-console .image-drop-input{grid-template-columns:64px minmax(0,1fr)}.account-console .image-upload-actions{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;width:100%}.account-console .image-upload-actions button{min-width:0}
.image-drop-input{display:grid;grid-template-columns:64px 1fr auto;align-items:center;gap:10px;min-height:82px;padding:8px;border:1px dashed #4b5152;background:#0b0e0f;cursor:pointer;transition:border-color .15s,background .15s,transform .1s}.image-drop-input:hover,.image-drop-input:focus-visible,.image-drop-input.dragging{border-color:var(--shd-orange);background:#15130e;outline:none}.image-drop-input.dragging{transform:translateY(1px);box-shadow:inset 0 0 0 1px #f6a82155}.image-file-input{position:absolute!important;width:1px!important;height:1px!important;overflow:hidden!important;clip:rect(0 0 0 0)!important;white-space:nowrap!important}.image-upload-preview{display:grid;place-items:center;width:64px;height:64px;border:1px solid #343a3b;background:#070909;color:#62696b;font-size:var(--shd-type-telemetry);text-align:center}.image-upload-preview img{display:block;width:100%;height:100%;object-fit:contain}.image-drop-copy{display:grid;gap:4px;min-width:0}.image-drop-copy b{color:#c6cbcc;font-size:var(--shd-type-telemetry);letter-spacing:1px}.image-drop-copy span{overflow:hidden;color:#747c7e;font-size:var(--shd-type-telemetry);text-overflow:ellipsis;white-space:nowrap}.image-upload-actions{display:flex;gap:5px}.image-upload-actions button{min-width:92px;padding:8px}.image-upload-error{display:block;margin-top:5px;color:var(--shd-danger)}@media(max-width:700px){.image-drop-input{grid-template-columns:56px 1fr}.image-upload-preview{width:56px;height:56px}.image-upload-actions{grid-column:1/-1}.image-upload-actions button{flex:1}}
.icon-upload-field .image-drop-input{grid-template-columns:1fr;gap:6px;text-align:center}.icon-upload-field .image-upload-preview{width:100%;height:72px}.icon-upload-field .image-upload-preview img{width:64px;height:64px}.icon-upload-field .image-upload-actions{display:grid;grid-template-columns:1fr 1fr;width:100%}.icon-upload-field .image-upload-actions button{min-width:0;padding:7px 3px;font-size:var(--shd-type-telemetry)}
.proposal-submitted,.data-access-banner{display:flex;align-items:center;gap:12px;margin-bottom:9px;padding:9px 11px;border-left:3px solid var(--shd-success);background:#101713;color:#a6cbb0}.proposal-submitted b,.data-access-banner b{font-size:var(--shd-type-telemetry);letter-spacing:1px}.proposal-submitted span,.data-access-banner span{font-size:var(--shd-type-telemetry);color:#7e9d86}.data-access-banner{border-left-color:#657174;background:#111617;color:#aeb7b9}.data-access-banner.contributor{border-left-color:var(--shd-orange);background:#19150e;color:#d8bc87}.data-access-error{margin:-3px 0 9px}.data-readonly form[method=post] button[type=submit],.data-readonly form[method=post] button:not([type]){filter:grayscale(1);opacity:.45}.admin-sidebar nav a.has-pending{grid-template-columns:minmax(0,1fr) 18px 18px}.admin-sidebar nav a.has-pending em{grid-column:2;display:grid;place-items:center;min-width:18px;height:18px;padding:0 5px;border:1px solid #a26c19;background:#211a0e;color:#ffc45d;font-size:var(--shd-type-telemetry);font-style:normal;font-weight:900;text-align:center}.admin-sidebar nav a.has-pending b{grid-column:3;justify-self:end}.admin-sidebar nav a.has-pending:hover em,.admin-sidebar nav a.has-pending.active em{border-color:var(--shd-orange);background:#2b2111;color:#fff1d0}.moderation-row{grid-template-columns:minmax(240px,1.5fr) minmax(140px,.8fr) 150px 80px 75px}.moderation-review{width:min(1120px,100%);max-height:calc(100vh - 32px);overflow:auto}.moderation-metadata{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin:10px 0}.moderation-metadata span{display:grid;gap:4px;padding:8px;border:1px solid var(--shd-border);color:var(--shd-text-muted);font-size:var(--shd-type-telemetry)}.moderation-metadata b{color:var(--shd-text);font-size:var(--shd-type-telemetry)}.moderation-review details{margin:10px 0;border:1px solid var(--shd-border);background:#0b0e0f}.moderation-review summary{padding:9px;color:var(--shd-orange);font-size:var(--shd-type-telemetry);cursor:pointer}.moderation-review pre{max-height:260px;overflow:auto;margin:0;padding:10px;border-top:1px solid var(--shd-border);color:#9ca6a8;font-size:var(--shd-type-telemetry);white-space:pre-wrap}.proposal-change-preview{margin-top:10px}.proposal-preview-head{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border:1px solid var(--shd-border);background:#0d1011}.proposal-preview-head>div{display:grid;gap:3px}.proposal-preview-head span{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:1px}.proposal-preview-head small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry)}.proposal-preview-head>b{color:var(--shd-orange);font-size:var(--shd-type-telemetry)}.proposal-compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:7px}.proposal-editor-preview{min-width:0;border:1px solid var(--shd-border);background:#111415}.proposal-editor-preview.before{border-top:2px solid #596164}.proposal-editor-preview.after{border-top:2px solid var(--shd-orange)}.proposal-editor-preview>header{display:flex;align-items:center;justify-content:space-between;min-height:32px;padding:6px 9px;border-bottom:1px solid var(--shd-border);background:#0d1011}.proposal-editor-preview>header span,.proposal-editor-preview>header b{font-size:var(--shd-type-telemetry);letter-spacing:.9px}.proposal-editor-preview>header b{color:var(--shd-text-muted)}.proposal-editor-preview.after>header b{color:var(--shd-orange)}.proposal-attribute-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;padding:8px}.preview-field{min-width:0;display:grid;align-content:start;gap:5px}.preview-field.identity{grid-column:span 4}.preview-field>span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);letter-spacing:.8px}.preview-field>strong,.preview-field.range>div{min-height:34px;display:flex;align-items:center;padding:7px 8px;border:1px solid #363b3c;background:#0d0f10;color:var(--shd-text);font-size:var(--shd-type-telemetry);overflow-wrap:anywhere}.preview-field.identity>strong{min-height:43px;font-size:13px}.preview-field.range{grid-column:span 2}.preview-field.range>div{display:grid;grid-template-columns:1fr auto 1fr;gap:6px}.preview-field.range strong:last-child{text-align:right}.preview-field.range i{color:#555d5f;font-size:var(--shd-type-telemetry);font-style:normal}.proposal-empty-state{min-height:180px;display:grid;place-content:center;gap:6px;text-align:center}.proposal-empty-state b{color:var(--shd-orange);font-size:var(--shd-type-telemetry);letter-spacing:1px}.proposal-empty-state span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry)}.proposal-editor-preview.generic{margin-top:7px}.proposal-field-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;padding:8px}.proposal-field-summary>div{min-width:0;display:grid;align-content:start;gap:5px;padding:8px;border:1px solid #303536;background:#0d0f10}.proposal-field-summary span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);letter-spacing:.8px}.proposal-field-summary strong{color:var(--shd-text);font-size:var(--shd-type-telemetry);overflow-wrap:anywhere}.proposal-field-summary small{max-height:42px;overflow:hidden;color:#687174;font-size:var(--shd-type-telemetry);overflow-wrap:anywhere}.decision-tabs{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin:10px 0}.decision-tabs button{padding:9px;border:1px solid var(--shd-border);background:#111415;color:var(--shd-text-muted)}.decision-tabs button.active{border-color:var(--shd-orange);color:var(--shd-orange)}@media(max-width:800px){.moderation-row{grid-template-columns:1fr}.moderation-metadata,.proposal-compare-grid{grid-template-columns:1fr}.proposal-field-summary{grid-template-columns:1fr 1fr}}@media(max-width:520px){.proposal-attribute-grid,.proposal-field-summary{grid-template-columns:1fr}.preview-field.identity,.preview-field.range{grid-column:auto}}
diff --git a/src/lib/components/AgentIdentityLink.svelte b/src/lib/components/AgentIdentityLink.svelte
new file mode 100644
index 0000000..0cb0023
--- /dev/null
+++ b/src/lib/components/AgentIdentityLink.svelte
@@ -0,0 +1,44 @@
+
+
+{#if linked}
+
+
+
+ {agent.displayName}
+ {#if meta}{meta}{:else if showId}AGENT {agent.id.slice(0,8).toUpperCase()}{/if}
+
+ ›
+
+{:else}
+
+ NO MATCHING {data.tab === 'comments' ? 'COMMENTS' : 'PROFILE BIOS'}
+ Adjust the status or search filters to inspect another part of the community record.
+
+ {/each}
+
+
+{#if reviewing && moderationAction}
+
+
+
+
COMMUNITY CONTROL / {reviewing.status.toUpperCase()}
+{/if}
+
+
diff --git a/src/routes/isac/users/+page.server.ts b/src/routes/isac/users/+page.server.ts
index 128cdd3..06f0d1a 100644
--- a/src/routes/isac/users/+page.server.ts
+++ b/src/routes/isac/users/+page.server.ts
@@ -26,7 +26,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
deleted_accounts.deleted_at,
(SELECT MAX(created_at) FROM login_events WHERE user_id=users.id AND succeeded=1) AS last_login_at,
(SELECT COUNT(*) FROM builds WHERE owner_id=users.id) AS build_count,
- (SELECT COUNT(*) FROM comments WHERE user_id=users.id) AS comment_count
+ (SELECT COUNT(*) FROM comments WHERE author_id=users.id) AS comment_count
FROM users LEFT JOIN user_status ON user_status.user_id=users.id
LEFT JOIN user_profiles ON user_profiles.user_id=users.id
LEFT JOIN deleted_accounts ON deleted_accounts.user_id=users.id
@@ -76,7 +76,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
(SELECT MAX(created_at) FROM login_events WHERE user_id=users.id AND succeeded=1) AS last_login_at,
(SELECT provider FROM login_events WHERE user_id=users.id AND succeeded=1 ORDER BY created_at DESC LIMIT 1) AS last_login_provider,
(SELECT COUNT(*) FROM builds WHERE owner_id=users.id) AS build_count,
- (SELECT COUNT(*) FROM comments WHERE user_id=users.id) AS comment_count
+ (SELECT COUNT(*) FROM comments WHERE author_id=users.id) AS comment_count
FROM users LEFT JOIN user_status ON user_status.user_id=users.id
LEFT JOIN user_profiles ON user_profiles.user_id=users.id
LEFT JOIN deleted_accounts ON deleted_accounts.user_id=users.id
diff --git a/src/routes/isac/users/[id]/+page.server.ts b/src/routes/isac/users/[id]/+page.server.ts
index 56a892d..1484322 100644
--- a/src/routes/isac/users/[id]/+page.server.ts
+++ b/src/routes/isac/users/[id]/+page.server.ts
@@ -30,12 +30,17 @@ export const load: PageServerLoad = async ({ cookies, params, url }) => {
const loginArgs: unknown[] = [params.id, loginPattern, loginPattern, loginPattern, loginPattern, loginPattern, ...(loginStatus ? [loginStatus === 'success' ? 1 : 0] : [])];
const buildWhere = `owner_id=? AND (title LIKE ? ESCAPE '\\' COLLATE NOCASE OR slug LIKE ? ESCAPE '\\' COLLATE NOCASE)${buildVisibility ? ' AND visibility=?' : ''}`;
const buildArgs: unknown[] = [params.id, pattern(buildQuery), pattern(buildQuery), ...(buildVisibility ? [buildVisibility] : [])];
- const commentWhere = `comments.user_id=? AND (comments.body LIKE ? ESCAPE '\\' COLLATE NOCASE OR builds.title LIKE ? ESCAPE '\\' COLLATE NOCASE)`;
+ const commentWhere = `comments.author_id=? AND (comments.body_markdown LIKE ? ESCAPE '\\' COLLATE NOCASE
+ OR COALESCE(builds.title,target_profiles.display_name,'Agent profile') LIKE ? ESCAPE '\\' COLLATE NOCASE)`;
const commentArgs: unknown[] = [params.id, pattern(commentQuery), pattern(commentQuery)];
const [loginCount, buildCount, commentCount] = await Promise.all([
(await database.prepare(`SELECT COUNT(*) AS count FROM login_events WHERE ${loginWhere}`)).get(...loginArgs),
(await database.prepare(`SELECT COUNT(*) AS count FROM builds WHERE ${buildWhere}`)).get(...buildArgs),
- (await database.prepare(`SELECT COUNT(*) AS count FROM comments JOIN builds ON builds.id=comments.build_id WHERE ${commentWhere}`)).get(...commentArgs)
+ (await database.prepare(`SELECT COUNT(*) AS count FROM comments
+ JOIN comment_threads ON comment_threads.id=comments.thread_id
+ LEFT JOIN builds ON comment_threads.target_type IN ('build','guide') AND builds.id=comment_threads.target_id
+ LEFT JOIN user_profiles target_profiles ON comment_threads.target_type='profile' AND target_profiles.user_id=comment_threads.target_id
+ WHERE ${commentWhere}`)).get(...commentArgs)
]);
const loginPagination = getPagination(url, Number(loginCount?.count ?? 0), 'login');
const buildPagination = getPagination(url, Number(buildCount?.count ?? 0), 'build');
@@ -67,8 +72,13 @@ export const load: PageServerLoad = async ({ cookies, params, url }) => {
FROM login_events LEFT JOIN sessions ON sessions.id=login_events.session_id
WHERE ${loginWhere} ORDER BY login_events.created_at DESC LIMIT ? OFFSET ?`)).all(...loginArgs, loginPagination.perPage, loginPagination.offset),
(await database.prepare(`SELECT id,title,slug,visibility,updated_at FROM builds WHERE ${buildWhere} ORDER BY updated_at DESC LIMIT ? OFFSET ?`)).all(...buildArgs, buildPagination.perPage, buildPagination.offset),
- (await database.prepare(`SELECT comments.id,comments.body,comments.created_at,builds.title,builds.slug
- FROM comments JOIN builds ON builds.id=comments.build_id WHERE ${commentWhere} ORDER BY comments.created_at DESC LIMIT ? OFFSET ?`)).all(...commentArgs, commentPagination.perPage, commentPagination.offset)
+ (await database.prepare(`SELECT comments.id,comments.body_markdown AS body,comments.created_at,
+ COALESCE(builds.title,target_profiles.display_name,'Agent profile') AS title,builds.slug
+ FROM comments
+ JOIN comment_threads ON comment_threads.id=comments.thread_id
+ LEFT JOIN builds ON comment_threads.target_type IN ('build','guide') AND builds.id=comment_threads.target_id
+ LEFT JOIN user_profiles target_profiles ON comment_threads.target_type='profile' AND target_profiles.user_id=comment_threads.target_id
+ WHERE ${commentWhere} ORDER BY comments.created_at DESC LIMIT ? OFFSET ?`)).all(...commentArgs, commentPagination.perPage, commentPagination.offset)
,getAuthenticationMethods(database, params.id)
,(await database.prepare('SELECT email,verified_at,updated_at FROM user_emails WHERE user_id=?')).all(params.id)
,(await database.prepare('SELECT id,name,device_type,backed_up,last_used_at,created_at FROM passkeys WHERE user_id=? ORDER BY created_at DESC')).all(params.id)
diff --git a/src/routes/terms/+page.svelte b/src/routes/terms/+page.svelte
index 5b4bd33..54063af 100644
--- a/src/routes/terms/+page.svelte
+++ b/src/routes/terms/+page.svelte
@@ -70,6 +70,12 @@
under a “Deleted Agent” attribution to preserve community references. You may not
upload unlawful, infringing, malicious, or privacy-invasive content.
+
+ Community Oversight may hide or remove profile biographies, profile messages,
+ avatars, builds, guides, comments, or other user content that violates these terms
+ or presents a safety, privacy, legal, or abuse risk. Removing content does not
+ remove related moderation, audit, or legal records when SHD.build must retain them.
+
@@ -93,9 +99,23 @@
We may process 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, comments, notification messages, read state, and delivery
- preferences. SHD.build stores passwords only as salted Argon2id hashes. Private
- passkey material remains on your authenticator.
+ public biography, profile visibility, public authored-content summaries, profile
+ messages, favorites, ratings, comments, notification messages, read state, and
+ delivery preferences. SHD.build stores passwords only as salted Argon2id hashes.
+ Private passkey material remains on your authenticator.
+
+
+ A public Agent profile displays your public display name, avatar, biography, public
+ builds, published guides, recent public discussion, and profile messages. Other
+ signed-in users may post messages on a public profile when their account has
+ permission to do so. Profile messages display the author's public identity and may
+ be removed by their author or Community Oversight.
+
+
+ You may hide your Agent profile from public discovery and prevent new profile
+ messages. Hiding a profile does not remove your display name or avatar from public
+ builds, guides, or comments you authored. It also does not make existing public
+ content anonymous.
For login security and support, we may record the time, success status,
@@ -198,9 +218,11 @@
When you delete your account, SHD.build removes active sessions, credentials,
verified email, provider profiles, passkeys, permissions, and uploaded avatar. You
may also select an immediate purge of retained login-event history during
- self-deletion. SHD.build may keep public contributions under a “Deleted Agent”
- attribution. SHD.build may retain backups and records for a limited period where
- applicable when fraud prevention, dispute resolution, or law requires them.
+ self-deletion. Account deletion removes the public biography and makes the Agent
+ profile unavailable. SHD.build may keep public builds, guides, and comments under
+ a “Deleted Agent” attribution so community references remain intact. SHD.build may
+ retain backups and records for a limited period when fraud prevention, dispute
+ resolution, or law requires them.
Comments this Agent leaves on builds and guides will appear here.
+