Browse showed 'anon' for every owner because shoo pairwise identities
carry no name. Owners now fall back to their friendly user id, and the
Account card gains a display-name editor (PATCH /api/me {name}, sanitized,
40 chars; also settable via the set_sharing MCP tool). Cron/ladder sim
computes now dispatch as self-invocations via SELF_ORIGIN so each replay
gets its own CPU budget instead of sharing the scheduled() budget, and the
nightly retry batch grows to 32.- commit
- d42fe89fbd18d654acea8e85a04b6428af91b61b
- parent
- f30c0b28fe0f2e0e48e4906207e9c3805cd963ef
- author
- Dami <dami.etoile@gmail.com>
| M | packages/ | 6 | ++++- |
| M | web/ | 2 | +- |
| M | web/ | 4 | ++-- |
| M | web/ | 35 | ++++++++++++++++++++++++++++-- |
| M | worker/ | 8 | +++++++ |
| M | worker/ | 3 | +++ |
| M | worker/ | 4 | ++-- |
| M | worker/ | 8 | ++++++- |
| M | worker/ | 25 | ++++++++++++++++++++-- |
| M | wrangler.jsonc | 5 | +++-- |
@@ -213,7 +213,11 @@ export const UpdateLogicMetaInput = z.object({
213213 sourceShared: z.boolean().optional(),214214 });215215 216-export const SetSharingInput = z.object({ publicSharing: z.boolean() });216+export const SetSharingInput = z.object({217+ publicSharing: z.boolean().optional(),218+ /** Public display name shown on Browse listings. Null clears. */219+ name: z.string().max(40).nullable().optional(),220+});217221 218222 export const HeartInput = z.object({ id: z.string().min(1) });219223 @@ -65,7 +65,7 @@ export const api = {
6565 loginLocal: (name: string) => req<UserInfo>('POST', '/api/session', { local: name }),6666 logout: () => req<void>('DELETE', '/api/session'),6767 me: () => req<UserInfo>('GET', '/api/me'),68- patchMe: (publicSharing: boolean) => req<UserInfo>('PATCH', '/api/me', { publicSharing }),68+ patchMe: (patch: { publicSharing?: boolean; name?: string | null }) => req<UserInfo>('PATCH', '/api/me', patch),6969 /** REST-only account deletion; confirm must equal the user id. */7070 deleteAccount: (confirm: string) => req<void>('DELETE', '/api/me', { confirm }),7171 @@ -240,7 +240,7 @@ function renderList(main: HTMLElement, ctx: AppCtx): void {
240240 ),241241 item.sourceShared ? h('span', { class: 'src-badge', title: 'source shared' }, 'SRC') : null,242242 ),243- h('td', { class: 'muted owner-cell' }, item.owner.name ?? 'anon'),243+ h('td', { class: 'muted owner-cell' }, item.owner.name ?? item.owner.id),244244 h('td', null, modelChip(item.model)),245245 h(246246 'td',@@ -339,7 +339,7 @@ function paintDetail(scroll: HTMLElement, ctx: AppCtx, d: PublicScriptDetail): v
339339 h(340340 'div',341341 { class: 'row detail-meta' },342- h('span', { class: 'muted' }, `by ${d.owner.name ?? 'anon'}`),342+ h('span', { class: 'muted' }, `by ${d.owner.name ?? d.owner.id}`),343343 modelChip(d.model),344344 h('span', { class: 'mono' }, `rating ${Math.round(d.rating)}`),345345 heartButton(ctx, d),@@ -62,16 +62,47 @@ function paintAccount(card: HTMLElement, ctx: AppCtx) {
6262 onchange: async () => {6363 msg.textContent = '';6464 try {65- ctx.setUser(await api.patchMe(toggle.checked));65+ ctx.setUser(await api.patchMe({ publicSharing: toggle.checked }));6666 } catch (err) {6767 toggle.checked = !toggle.checked;6868 msg.textContent = err instanceof Error ? err.message : String(err);6969 }7070 },7171 });72+ const nameInput = h('input', {73+ type: 'text',74+ value: user.name ?? '',75+ placeholder: user.id,76+ maxLength: 40,77+ });78+ const nameMsg = h('span', { class: 'muted' });7279 card.append(7380 h('h2', null, 'Account'),74- h('p', { class: 'row' }, h('span', { class: 'mono muted' }, user.id), h('strong', null, user.name ?? user.externalId)),81+ h('p', { class: 'row' }, h('span', { class: 'mono muted' }, user.id)),82+ h(83+ 'div',84+ { class: 'row', style: 'margin-bottom:0.5rem' },85+ nameInput,86+ h(87+ 'button',88+ {89+ class: 'small',90+ onclick: async () => {91+ nameMsg.textContent = '';92+ try {93+ const v = nameInput.value.trim();94+ ctx.setUser(await api.patchMe({ name: v === '' ? null : v }));95+ nameMsg.textContent = 'saved';96+ } catch (err) {97+ nameMsg.textContent = err instanceof Error ? err.message : String(err);98+ }99+ },100+ },101+ 'Save name',102+ ),103+ nameMsg,104+ ),105+ h('p', { class: 'muted' }, 'Your display name on Browse listings; blank shows your user id.'),75106 h(76107 'label',77108 { class: 'row toggle-row' },@@ -187,6 +187,14 @@ export async function mergeUsers(db: D1Database, fromId: string, toId: string):
187187 ]);188188 }189189 190+export async function updateUserName(191+ db: D1Database,192+ userId: string,193+ name: string | null,194+): Promise<void> {195+ await db.prepare(`UPDATE users SET name = ?2 WHERE id = ?1`).bind(userId, name).run();196+}197+190198 export async function upsertUser(191199 db: D1Database,192200 externalId: string,@@ -19,4 +19,7 @@ export interface Env {
1919 * COOKIE_DOMAIN-scoped session then works on every *.tribes.pub host.2020 */2121 AUTH_ORIGIN?: string;22+ /** This deployment's own public origin — used by cron jobs to dispatch23+ * sim computes as fresh self-invocations (each with its own CPU budget). */24+ SELF_ORIGIN?: string;2225 }@@ -19,7 +19,7 @@ import type { Env } from './env';
1919 import * as db from './db';2020 import { nanoid, nowSec, randomDecimalSeed } from './util';2121 import { friendlyId } from './friendly-id';22-import { computeSimMetrics } from './simrun';22+import { dispatchCompute } from './sweep';2323 2424 /** Hard cap on ladder sims per nightly run (each replays inline on the cron). */2525 export const LADDER_MAX_SIMS = 16;@@ -138,7 +138,7 @@ export async function runLadder(env: Env, maxSims: number = LADDER_MAX_SIMS): Pr
138138 created++;139139 // Inline (cron CPU budget is generous but finite — LADDER_MAX_SIMS caps us);140140 // this also applies the Elo updates for the night.141- await computeSimMetrics(env, simId);141+ await dispatchCompute(env, simId);142142 }143143 console.log(JSON.stringify({ event: 'ladder', created, candidates: listed.length }));144144 return created;@@ -427,7 +427,13 @@ export function createService(env: Env, deps: ServiceDeps): TribesService {
427427 },428428 429429 async setSharing(user: AuthedUser, input: SetSharingInputT): Promise<UserInfo> {430- await db.setUserSharing(D, user.userId, input.publicSharing);430+ if (input.publicSharing !== undefined) {431+ await db.setUserSharing(D, user.userId, input.publicSharing);432+ }433+ if (input.name !== undefined) {434+ const name = input.name === null ? null : sanitizeMeta(input.name, 40);435+ await db.updateUserName(D, user.userId, name === '' ? null : name);436+ }431437 const row = await db.getUserById(D, user.userId);432438 if (!row) throw new ServiceError('not_found', 'user no longer exists');433439 return userInfoOf(row);@@ -138,7 +138,7 @@ export async function backfillR2(env: Env): Promise<{ sources: number; metrics:
138138 * cron retries a bounded batch each night. Metrics-GET views retry too;139139 * this catches the ones nobody looked at.140140 */141-export async function retryDeadPublicSims(env: Env, limit = 8): Promise<number> {141+export async function retryDeadPublicSims(env: Env, limit = 32): Promise<number> {142142 const rows = await env.DB143143 .prepare(144144 `SELECT id FROM sims@@ -151,8 +151,29 @@ export async function retryDeadPublicSims(env: Env, limit = 8): Promise<number>
151151 .all<{ id: string }>();152152 let retried = 0;153153 for (const row of rows.results) {154- await computeSimMetrics(env, row.id); // internally idempotent + fault-tolerant154+ await dispatchCompute(env, row.id);155155 retried++;156156 }157157 return retried;158158 }159+160+/**161+ * Run a sim compute in its OWN invocation. A replay can eat most of a CPU162+ * budget by itself, so the cron never computes inline — it GETs the sim's163+ * metrics endpoint on our own origin, whose 404 path schedules the compute164+ * in that request's waitUntil. Inline fallback only when SELF_ORIGIN is165+ * unset (tests/local).166+ */167+export async function dispatchCompute(env: Env, simId: string): Promise<void> {168+ if (env.SELF_ORIGIN !== undefined && env.SELF_ORIGIN !== '') {169+ try {170+ await fetch(`${env.SELF_ORIGIN}/api/sims/${simId}/metrics`, {171+ headers: { accept: 'application/json' },172+ });173+ return;174+ } catch (err) {175+ console.warn('compute dispatch failed, falling back inline:', simId, err);176+ }177+ }178+ await computeSimMetrics(env, simId);179+}@@ -59,7 +59,7 @@
5959 "r2_buckets": [{ "binding": "BLOBS", "bucket_name": "tribes-blobs-dev" }],6060 "triggers": { "crons": ["17 4 * * *"] },6161 "observability": { "enabled": true },62- "vars": { "DEV_AUTH": "0" }62+ "vars": { "DEV_AUTH": "0", "SELF_ORIGIN": "https://dev.tribes.pub" }6363 },6464 "prod": {6565 "name": "tribes-prod",@@ -87,7 +87,8 @@
8787 "vars": {8888 "DEV_AUTH": "0",8989 "COOKIE_DOMAIN": ".tribes.pub",90- "AUTH_ORIGIN": "https://tribes.pub"90+ "AUTH_ORIGIN": "https://tribes.pub",91+ "SELF_ORIGIN": "https://tribes.pub"9192 }9293 }9394 }