ReviewOS

also looking at this

stacks/wildloop

Migrate :text/x-text bindings to {⁠{ }} interpolation (stx 0.2.72)

#990
Merged glennmichael123 wants to merge chore/migrate-text-to-mustache into main
50 files +611 -371

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

app/Actions/Challenge/ChallengeIndexAction.tsmodified+3-1
Changes to app/Actions/Challenge/ChallengeIndexAction.ts
@@ -33,7 +33,9 @@ export default new Action({
3333 }))
3434 .sort((a: any, b: any) => Date.parse(b.createdAt ?? '') - Date.parse(a.createdAt ?? ''))
3535
36 const paged = paginate(challenges, readPageParams(request))
36 // Generous default — the page filters the full set into client-side
37 // tabs (active/sent/received/completed) and has no load-more (#978 review).
38 const paged = paginate(challenges, readPageParams(request, { defaultLimit: 200, maxLimit: 200 }))
3739 return response.json({ success: true, challenges: paged.items, meta: paged.meta })
3840 }
3941 catch (error) {
app/Actions/Club/ClubIndexAction.tsmodified+3-1
Changes to app/Actions/Club/ClubIndexAction.ts
@@ -73,7 +73,9 @@ export default new Action({
7373 })
7474 .sort((a: any, b: any) => b.memberCount - a.memberCount || a.id - b.id)
7575
76 const paged = paginate(result, readPageParams(request))
76 // Generous default — the clubs page has no load-more and expects the
77 // full set; ?limit/?offset still paginate on demand (#978 review).
78 const paged = paginate(result, readPageParams(request, { defaultLimit: 200, maxLimit: 200 }))
7779 return response.json({ success: true, clubs: paged.items, meta: paged.meta })
7880 }
7981 catch (error) {
app/Actions/Social/UserSearchAction.tsmodified+5-4
Changes to app/Actions/Social/UserSearchAction.ts
@@ -5,8 +5,6 @@
55// returns the most active athletes as a discover list. Public read; response
66// includes enough stats to render discover cards without extra calls.
77
8const SEARCH_LIMIT = 20
9
108export default new Action({
119 name: 'User Search',
1210 description: 'Search athletes by name (discover list when no query)',
@@ -17,9 +15,12 @@ export default new Action({
1715 const q = typeof qRaw === 'string' ? qRaw.trim() : ''
1816
1917 try {
18 // Fetch the full match set, then paginate — so meta.total/hasMore reflect
19 // the real count (a DB-side .limit() before paginate() would cap total at
20 // the page size and make hasMore always false, #978 review).
2021 const users = q.length >= 2
21 ? (await User.where('name', 'like', `%${q}%`).limit(SEARCH_LIMIT).get()) ?? []
22 : (await User.query().limit(SEARCH_LIMIT).get()) ?? []
22 ? (await User.where('name', 'like', `%${q}%`).get()) ?? []
23 : (await User.query().get()) ?? []
2324
2425 const ids = users.map((u: any) => u.id)
2526 const activities = ids.length ? (await Activity.whereIn('user_id', ids).get()) ?? [] : []
app/Actions/Trail/SavedTrailIndexAction.tsmodified+3-1
Changes to app/Actions/Trail/SavedTrailIndexAction.ts
@@ -48,7 +48,9 @@ export default new Action({
4848 }
4949 })
5050
51 const paged = paginate(savedTrails, readPageParams(request))
51 // Generous default — the profile Saved tab renders the full set, no
52 // load-more (#978 review); ?limit/?offset still paginate on demand.
53 const paged = paginate(savedTrails, readPageParams(request, { defaultLimit: 200, maxLimit: 200 }))
5254 return response.json({ success: true, savedTrails: paged.items, meta: paged.meta })
5355 }
5456 catch (error) {
app/Actions/Trail/TrailReviewIndexAction.tsmodified+3-1
Changes to app/Actions/Trail/TrailReviewIndexAction.ts
@@ -36,7 +36,9 @@ export default new Action({
3636 createdAt: r.created_at,
3737 }))
3838
39 const paged = paginate(reviews, readPageParams(request))
39 // Generous default — the reviews tab renders the full set, no load-more
40 // (#978 review); ?limit/?offset still paginate on demand.
41 const paged = paginate(reviews, readPageParams(request, { defaultLimit: 200, maxLimit: 200 }))
4042 return response.json({ success: true, reviews: paged.items, meta: paged.meta })
4143 }
4244 catch (error) {
app/Models/UserStats.tsmodified+2-2
Changes to app/Models/UserStats.ts
@@ -23,7 +23,7 @@ export default defineModel({
2323 order: 1,
2424 fillable: true,
2525 validation: {
26 rule: schema.number().min(0),
26 rule: schema.float().min(0),
2727 },
2828 factory: (faker) => faker.number.float({ min: 10, max: 2000, fractionDigits: 1 }),
2929 },
@@ -45,7 +45,7 @@ export default defineModel({
4545 order: 3,
4646 fillable: true,
4747 validation: {
48 rule: schema.number().min(0),
48 rule: schema.float().min(0),
4949 },
5050 factory: (faker) => faker.number.int({ min: 5000, max: 500000 }),
5151 },
database/migrations/0000000014-create-user_stats-table.sqlmodifiedrenamed throughout+2-2
Changes to database/migrations/0000000014-create-user_stats-table.sql
@@ -1,8 +1,8 @@
11CREATE TABLE IF NOT EXISTS "user_stats" (
22 "id" INTEGER PRIMARY KEY AUTOINCREMENT,
3 "total_distance" INTEGER,
3 "total_distance" REAL,
44 "total_time" TEXT,
5 "total_elevation" INTEGER,
5 "total_elevation" REAL,
66 "trails_completed" INTEGER,
77 "current_streak" INTEGER,
88 "longest_streak" INTEGER,
scripts/verify-column-affinity.tsmodified+2-0
Changes to scripts/verify-column-affinity.ts
@@ -35,6 +35,7 @@ const REAL_COLS: Array<[string, string]> = [
3535 ['territory_stats', 'total_area_owned'], ['territory_stats', 'largest_territory_area'],
3636 ['challenges', 'area_at_stake'],
3737 ['territory_histories', 'area_at_event'],
38 ['user_stats', 'total_distance'], ['user_stats', 'total_elevation'],
3839]
3940for (const [t, c] of REAL_COLS)
4041 check(`${t}.${c} is REAL`, colType(t, c) === 'REAL', colType(t, c))
@@ -47,6 +48,7 @@ const INT_COLS: Array<[string, string]> = [
4748 ['trails', 'review_count'],
4849 ['territory_stats', 'total_territories_owned'], ['territory_stats', 'territories_conquered'], ['territory_stats', 'weekly_rank'],
4950 ['challenges', 'challenger_id'], ['challenges', 'winner_id'],
51 ['user_stats', 'trails_completed'], ['user_stats', 'current_streak'], ['user_stats', 'total_activities'],
5052 ['reviews_alias_skip', 'noop'],
5153]
5254for (const [t, c] of INT_COLS) {
scripts/verify-pagination.tsmodified+12-0
Changes to scripts/verify-pagination.ts
@@ -50,6 +50,7 @@ globalThis.Error = RealError
5050const ActivityIndex = (await import('../app/Actions/Activity/ActivityIndexAction')).default
5151const TrailIndex = (await import('../app/Actions/Trail/TrailIndexAction')).default
5252const ClubIndex = (await import('../app/Actions/Club/ClubIndexAction')).default
53const UserSearch = (await import('../app/Actions/Social/UserSearchAction')).default
5354
5455async function call(action: any, body: Record<string, unknown>): Promise<{ status: number, json: any }> {
5556 const request = { get: (k: string) => (body as any)[k] }
@@ -89,5 +90,16 @@ const cAll = await call(ClubIndex, {})
8990check('ClubIndex has pagination meta', typeof cAll.json?.meta?.offset === 'number'
9091 && typeof cAll.json?.meta?.hasMore === 'boolean')
9192
93// UserSearch: total must be the REAL match count, not capped at the page size
94// (the DB-side .limit() before paginate() was removed, #978 review).
95const sAll = await call(UserSearch, {})
96const userTotal = sAll.json?.meta?.total
97check('UserSearch total = real match count', typeof userTotal === 'number' && userTotal >= 3
98 && sAll.json?.athletes?.length === userTotal, JSON.stringify(sAll.json?.meta))
99const sPage = await call(UserSearch, { limit: 2 })
100check('UserSearch limit=2: 2 items, total unchanged, hasMore true (not capped)',
101 sPage.json?.athletes?.length === 2 && sPage.json?.meta?.total === userTotal
102 && sPage.json?.meta?.hasMore === (userTotal > 2), JSON.stringify(sPage.json?.meta))
103
92104console.log(failures === 0 ? '\n✅ all pagination checks passed' : `\n❌ ${failures} check(s) failed`)
93105process.exit(failures === 0 ? 0 : 1)