fix(submit): preserve multi-machine submissions without reshaping daily breakdown#389
fix(submit): preserve multi-machine submissions without reshaping daily breakdown#389
Conversation
…ly breakdown Mainline stored a single submission row per user, so a later submit from another machine could overwrite flat per-client breakdowns for overlapping days. This scopes submissions by stable source identity, upgrades a lone legacy unsourced row in place on first source-aware submit, and aggregates profile/embed/leaderboard reads across rows while keeping the existing daily_breakdown source_breakdown shape flat. Constraint: Must keep the current database and existing daily_breakdown.source_breakdown shape Constraint: Must remain compatible with legacy unsourced rows during cutover Rejected: Nested per-device JSON in daily_breakdown | broader read-path churn and token-identity pitfalls Rejected: Bundle /api/me/stats and remote TUI sync | unrelated scope increase for the overwrite fix Confidence: high Scope-risk: moderate Reversibility: messy Directive: Keep embed/profile/leaderboard reads source-row aware; do not reintroduce single-row-per-user assumptions Tested: cargo fmt --all --check; cargo clippy -p tokscale-cli --all-features -- -D warnings; cargo test -p tokscale-cli; bunx vitest run packages/frontend/__tests__/api/submit.test.ts packages/frontend/__tests__/api/submitAuth.test.ts packages/frontend/__tests__/api/usersProfile.test.ts packages/frontend/__tests__/lib/dbHelpers.test.ts packages/frontend/__tests__/lib/getUserEmbedStats.test.ts; bunx vitest run packages/frontend/__tests__/lib/getLeaderboard.test.ts packages/frontend/__tests__/lib/getLeaderboardAllTime.test.ts; targeted frontend eslint on changed files Not-tested: Full frontend typecheck remains blocked by the pre-existing packages/frontend/src/components/BlackholeHero.tsx asset import typing error; live database migration rehearsal against production-like Postgres
…h PR review This reverts commit 344c05d from main so the multi-machine submission change can land through the normal PR path instead of a direct push. Constraint: User requested reverting the direct push and reopening the change as a PR Constraint: Must restore main without losing the already-validated patch Rejected: Force-reset main | destructive history rewrite on a published branch Rejected: Leave change on main and open a follow-up PR | does not satisfy the requested rollback Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep the replacement PR branch aligned with commit 344c05d content; do not sneak in extra scope during re-land Tested: git revert --no-commit 344c05d; git status review Not-tested: Re-running the full verification matrix after reverting main (revert only removes the already-tested patch)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Rollout checklist for this PR:
Verification completed before opening the PR:
Known unrelated pre-existing issue:
|
|
Release-note friendly summary:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a50538060a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Suggested short merge description:
|
(cherry picked from commit 7a035ac)
(cherry picked from commit d09ab0a)
(cherry picked from commit 6126e02)
…ests Constraint: Keep the cherry-picked PR #388 snapshot lint-clean under this repo's frontend ESLint rules Rejected: Leave the original variable name | fails @next/next/no-assign-module-variable in local lint Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep this as a tiny follow-up on top of the original authored commits; do not fold broader changes into the credit-preserving rewrite Tested: packages/frontend/node_modules/.bin/eslint --config packages/frontend/eslint.config.mjs packages/frontend/__tests__/lib/getUserEmbedStats.test.ts Not-tested: Full frontend verification matrix (history rewrite only; behavior unchanged from prior verified branch)
a505380 to
a339495
Compare
|
Credit note: this PR branch now preserves the original authored work from #388 by @IvGolovach via cherry-pick history, instead of a single squashed commit. Preserved authored commits on this branch:
Small follow-up by me on top:
That means commit-level authorship is now properly attributed to the original author, and the branch keeps a small separate follow-up for the local lint-only rename. |
|
Attribution note: This PR is based on and now preserves the original authored work from #388 by @IvGolovach. The branch history was rewritten so the original source-scoped submission commits are carried forward directly, with one small follow-up commit by me for local lint compatibility in a test file. Original authored work preserved here:
Small follow-up on top:
So the implementation credit should primarily go to @IvGolovach / #388, with my contribution limited to the branch re-land + the tiny lint-only follow-up. |
The source-id lock previously trusted a matching live PID indefinitely. If the PID had been recycled by an unrelated process, an old lock file could block first-time source ID generation until submit fell back to unsourced payloads. Constraint: Source-id initialization should stay resilient without introducing a broader lock format migration Rejected: Trust PID liveness forever | stale lock can survive PID reuse and block initialization Rejected: Remove any lock older than the short stale threshold | risks breaking a legitimately active locker on a slow filesystem Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep a hard age-based escape hatch even when PID probes report alive; PID alone is not a stable lock owner identity Tested: cargo fmt --all --check; cargo clippy -p tokscale-cli --all-features -- -D warnings; cargo test -p tokscale-cli test_should_remove_stale_source_id_lock -- --nocapture Not-tested: Full tokscale-cli test suite rerun after this narrow auth-lock change
Source-scoped submissions make it possible to inspect usage by machine, so this adds a dedicated sources/devices view on profile pages and a matching API that aggregates per-source totals and recent contribution history. Constraint: Must build on the source-scoped submission model without reshaping daily_breakdown.source_breakdown Constraint: Must fit the existing profile page flow with minimal extra round-trips Rejected: Force all source detail into the existing /api/users/[username] payload | keeps the core profile response leaner and separates concerns Rejected: Wait for a separate per-source detail API before shipping UI | unnecessary delay for a useful first device view Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep device/source viewing aligned with source_id as the stable identity and source_name as display-only metadata Tested: bunx vitest run packages/frontend/__tests__/api/userSources.test.ts packages/frontend/__tests__/api/usersProfile.test.ts; packages/frontend/node_modules/.bin/eslint --config packages/frontend/eslint.config.mjs src/app/u/[username]/page.tsx src/app/u/[username]/ProfilePageClient.tsx src/app/api/users/[username]/sources/route.ts src/components/profile/index.tsx __tests__/api/userSources.test.ts Not-tested: Full frontend typecheck still blocked by the pre-existing packages/frontend/src/components/BlackholeHero.tsx asset import typing error
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/frontend/src/app/u/[username]/page.tsx">
<violation number="1" location="packages/frontend/src/app/u/[username]/page.tsx:71">
P2: A thrown error from the new `getSourceData` call will reject `Promise.all` and fail the whole profile page render instead of falling back to an empty sources list.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const [data, sourceData] = await Promise.all([ | ||
| getProfileData(username), | ||
| getSourceData(username), | ||
| ]); |
There was a problem hiding this comment.
P2: A thrown error from the new getSourceData call will reject Promise.all and fail the whole profile page render instead of falling back to an empty sources list.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/frontend/src/app/u/[username]/page.tsx, line 71:
<comment>A thrown error from the new `getSourceData` call will reject `Promise.all` and fail the whole profile page render instead of falling back to an empty sources list.</comment>
<file context>
@@ -52,11 +68,19 @@ export async function generateMetadata({ params }: { params: Promise<{ username:
export default async function ProfilePage({ params }: { params: Promise<{ username: string }> }) {
const { username } = await params;
- const data = await getProfileData(username);
+ const [data, sourceData] = await Promise.all([
+ getProfileData(username),
+ getSourceData(username),
</file context>
| const [data, sourceData] = await Promise.all([ | |
| getProfileData(username), | |
| getSourceData(username), | |
| ]); | |
| const [data, sourceData] = await Promise.all([ | |
| getProfileData(username), | |
| getSourceData(username).catch(() => ({ sources: [] })), | |
| ]); |
The first device-view pass bundled summary and detail payloads into one sources endpoint. This separates the lightweight source list from the heavier per-source detail response so profile pages can show device cards without shipping full contribution histories for every machine up front. Constraint: Must keep source/device views aligned with the source-scoped submission model already on this branch Constraint: Must avoid bloating the profile page payload with every source's full contribution history Rejected: Keep a single /sources endpoint with embedded detail for all sources | unnecessary payload growth and tighter coupling between card list and detail graph Rejected: Drop server-side initial source detail entirely | worse first-load UX for the default selected source Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep /sources for summaries and /sources/[sourceId] for detailed histories; if more device UI is added, build on this separation rather than rejoining the payloads Tested: bunx vitest run packages/frontend/__tests__/api/userSources.test.ts packages/frontend/__tests__/api/userSourceDetail.test.ts packages/frontend/__tests__/api/usersProfile.test.ts; packages/frontend/node_modules/.bin/eslint --config packages/frontend/eslint.config.mjs src/app/u/[username]/page.tsx src/app/u/[username]/ProfilePageClient.tsx src/app/api/users/[username]/sources/route.ts src/app/api/users/[username]/sources/[sourceId]/route.ts src/app/api/users/[username]/sources/shared.ts src/components/profile/index.tsx __tests__/api/userSources.test.ts __tests__/api/userSourceDetail.test.ts Not-tested: Full frontend typecheck remains blocked by the pre-existing packages/frontend/src/components/BlackholeHero.tsx asset import typing error
|
Update: the source/device profile work on this branch has been refined. What changed on top of the previous device-view pass:
New commit:
Verification for this follow-up:
Known unchanged pre-existing issue:
|
The device/source view now has lightweight summary and detail endpoints, but external consumers still need a compact per-source payload for cards, embeds, badges, or quick previews. This adds a dedicated summary route so clients can fetch one source's headline metrics without pulling the full contribution history. Constraint: Must build on the split source summary/detail API shape already on this branch Constraint: Must stay lightweight and avoid returning the full per-day history payload Rejected: Reuse the full source detail endpoint for summary consumers | unnecessary payload size for badge/embed/preview use cases Rejected: Add source summary fields only to the top-level user profile API | couples a focused source capability back into a broader profile response Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep source summary routes compact; if more source-level consumers appear, expand this route before bloating the detail response Tested: bunx vitest run packages/frontend/__tests__/api/userSources.test.ts packages/frontend/__tests__/api/userSourceDetail.test.ts packages/frontend/__tests__/api/userSourceSummary.test.ts packages/frontend/__tests__/api/usersProfile.test.ts; packages/frontend/node_modules/.bin/eslint --config packages/frontend/eslint.config.mjs src/app/api/users/[username]/sources/route.ts src/app/api/users/[username]/sources/[sourceId]/route.ts src/app/api/users/[username]/sources/[sourceId]/summary/route.ts src/app/api/users/[username]/sources/shared.ts __tests__/api/userSources.test.ts __tests__/api/userSourceDetail.test.ts __tests__/api/userSourceSummary.test.ts Not-tested: Full frontend typecheck remains blocked by the pre-existing packages/frontend/src/components/BlackholeHero.tsx asset import typing error
|
Follow-up update on the same branch:
New commit:
Verification for this follow-up:
Known unchanged pre-existing issue:
|
The branch already exposed a lightweight source summary route, but the profile UI still consumed only the summary list and full detail payloads. This wires the selected device panel to fetch and display a compact preview from `/sources/[sourceId]/summary`, so the new endpoint is used for actual UI affordances rather than existing only for future consumers. Constraint: Must reuse the lightweight source summary endpoint instead of duplicating top-client/top-model derivation in the client Constraint: Must preserve the existing default selected-device UX Rejected: Continue showing only the full detail panel | leaves the new summary endpoint unused by the profile UI Rejected: Move summary-only fields back into the source list payload | defeats the endpoint separation introduced earlier Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep quick-preview metadata sourced from the summary endpoint so summary/detail responsibilities stay distinct Tested: bunx vitest run packages/frontend/__tests__/api/userSourceSummary.test.ts packages/frontend/__tests__/api/userSources.test.ts packages/frontend/__tests__/api/userSourceDetail.test.ts packages/frontend/__tests__/api/usersProfile.test.ts; packages/frontend/node_modules/.bin/eslint --config packages/frontend/eslint.config.mjs src/app/u/[username]/page.tsx src/app/u/[username]/ProfilePageClient.tsx src/app/api/users/[username]/sources/[sourceId]/summary/route.ts __tests__/api/userSourceSummary.test.ts Not-tested: Full frontend typecheck remains blocked by the pre-existing packages/frontend/src/components/BlackholeHero.tsx asset import typing error
|
Another small follow-up landed on the same branch:
New commit:
Quick-preview fields now shown from the summary route:
Verification for this follow-up:
Known unchanged pre-existing issue:
|
|
Merge-ready summary: This PR now covers three coherent layers of the same feature set:
Verification completed on this branch:
Known unchanged pre-existing issue:
From a scope perspective, this is a good stopping point: the source-scoped submission change, its follow-up hardening, and the profile source/device UX now form a coherent, reviewable unit without drifting further into unrelated polish. |
The source routes used `"__legacy__"` as both the unsourced sentinel and a possible user-provided `sourceId`, which made a real `sourceId="__legacy__"` collide with legacy rows. This namespaces real source keys with a prefix and routes the detail API through the shared client alias normalizer. Constraint: Must preserve support for legacy unsourced rows while keeping source URLs stable enough for the new profile UI Rejected: Keep `__legacy__` as a raw dual-purpose key | real source rows can collide with the legacy sentinel and become ambiguous Rejected: Introduce a random/non-deterministic surrogate per source key | makes routing and hydration harder for the profile UI Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat route-facing source keys as encoded transport values, not raw source IDs; all real IDs should remain namespaced away from the legacy sentinel Tested: bunx vitest run packages/frontend/__tests__/api/userSources.test.ts packages/frontend/__tests__/api/userSourceDetail.test.ts packages/frontend/__tests__/api/userSourceSummary.test.ts packages/frontend/__tests__/api/usersProfile.test.ts; packages/frontend/node_modules/.bin/eslint --config packages/frontend/eslint.config.mjs src/app/api/users/[username]/sources/shared.ts src/app/api/users/[username]/sources/[sourceId]/route.ts __tests__/api/userSources.test.ts __tests__/api/userSourceDetail.test.ts __tests__/api/userSourceSummary.test.ts Not-tested: Full frontend typecheck remains blocked by the pre-existing packages/frontend/src/components/BlackholeHero.tsx asset import typing error
Summary
submissionsrows withsource_id/source_nameinstead of collapsing every account into a single rowdaily_breakdown.source_breakdownflat and avoid bundling#329-style/api/me/stats/ remote TUI sync scopeWhy
mainstored a singlesubmissionsrow per user and/api/submitselected that row byuserIdonly. That meant a later submit from another machine could overwrite overlapping flat per-client breakdowns for the same user/day instead of preserving both machines' contributions.What changed
Database / schema
submissions.source_idsubmissions.source_name(user_id, source_id)for source-scoped rowsdaily_breakdown.source_breakdownunchangedSubmit flow
meta.sourceId/meta.sourceNameundefined409when a scoped account later submits without source identityRead aggregation
CLI
TOKSCALE_SOURCE_IDTOKSCALE_SOURCE_NAMEScope notes
#329-style/api/me/statsdaily_breakdown.source_breakdownshapeVerification
cargo fmt --all --checkcargo clippy -p tokscale-cli --all-features -- -D warningscargo test -p tokscale-clibunx vitest run packages/frontend/__tests__/api/submit.test.ts packages/frontend/__tests__/api/submitAuth.test.ts packages/frontend/__tests__/api/usersProfile.test.ts packages/frontend/__tests__/lib/dbHelpers.test.ts packages/frontend/__tests__/lib/getUserEmbedStats.test.tsbunx vitest run packages/frontend/__tests__/lib/getLeaderboard.test.ts packages/frontend/__tests__/lib/getLeaderboardAllTime.test.tsKnown residual risks
409after an account enters source-scoped modesubmissionsrow per usersubmission_hashuniqueness under source scoping may need a follow-up decisionsourceNameRollout notes
409Summary by cubic
Scopes submissions by stable source identity to preserve multi‑machine submits without changing the
daily_breakdownshape. Adds device views and per‑source APIs (list, detail, lightweight summary), uses the summary endpoint for device previews, aggregates reads across rows, and namespaces route-facing source keys to avoid__legacy__collisions.New Features
(user_id, source_id)with in‑place upgrade for a lone legacy unsourced row; returns 409 when a scoped account submits without source identity; per‑day merge unchanged./api/users/[username]/sources(summaries),/api/users/[username]/sources/[sourceId](detail), and/api/users/[username]/sources/[sourceId]/summary(lightweight per‑source summary); keepdaily_breakdown.source_breakdownflat./sources/[sourceId]/summaryendpoint.tokscale-cli): persist a stable source ID at~/.config/tokscale/source-idwith PID+age lock and stale timeout; includemeta.sourceId/meta.sourceNamein submit/export; supportTOKSCALE_SOURCE_IDandTOKSCALE_SOURCE_NAME.Migration
submissions.source_id/source_name; drop single‑row‑per‑user uniqueness; add partial unique index for unsourced rows and unique(user_id, source_id)for scoped rows; no change todaily_breakdown.source_breakdown.Written for commit 13d13e8. Summary will update on new commits.