- 5 tasks: revert vaultLoop to SQL, replace broadcast backfill with per-entry re-fetch, wire into entry points, delete unused helper and its test, smoke check - refetchRunningBalances walks /log pagination and UPDATEs each row from its own data.balance - Drops the fetchLiveVaultBalance helper (queried /money which is total-vault not per-user) - The previous fix's broadcast backfill was clobbering per-entry historical balances with a single value
11 KiB
Revert to Per-User Balance via /log + Per-Entry Re-fetch — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Correctly compute per-user vault share. The /money endpoint returns the total vault across all users — wrong for per-user attribution. Revert vaultLoop to per-user SQL MAX(running_balance). Replace the broken broadcast backfill with a true per-entry re-fetch that updates each row's running_balance from its own data.balance. Delete the unused fetchLiveVaultBalance helper and its test.
Architecture: vaultLoop returns MAX(running_balance) per user (where each row's running_balance comes from that entry's own data.balance in the v2 /log response). The schema's running_balance column is the source of truth. A new refetchRunningBalances helper re-fetches each user's full /log and updates every row's running_balance from its per-entry data.balance. The fetchLiveVaultBalance helper (introduced in the previous plan) is deleted along with its test, since it queried the wrong endpoint.
Tech Stack: PHP 8.1+, SQLite3 via PDO, cURL. No new dependencies.
Global Constraints
- PHP 8.1+ syntax only.
- Database schema (
vaulttable withid TEXT PRIMARY KEYandrunning_balance INTEGER) is unchanged. INSERT … ON CONFLICT(id) DO NOTHINGis the only INSERT form used for log entries.- API authentication:
Authorization: ApiKey <key>header. - All helpers outside the rewritten ones stay byte-identical unless the task explicitly says otherwise.
- Commits per task. Do not push.
Task 1: Revert vaultLoop to per-user SQL
Files:
- Modify:
includes/utilities.php(vaultLooponly)
Interfaces:
-
vaultLoop($name=null): returns the per-user vault share (per therunning_balancecolumn). All-users case sums per-user MAXes. -
Step 1: Replace
vaultLoopwith the SQL-based implementation
In includes/utilities.php, replace the existing vaultLoop function (which currently calls fetchLiveVaultBalance) with:
function vaultLoop ($name=null) {
$pdo = getDatabaseConnection();
if ($name === null) {
$stmt = $pdo->query(
'SELECT COALESCE(SUM(max_balance), 0) FROM '
. '(SELECT MAX(running_balance) AS max_balance FROM vault GROUP BY user)'
);
return (int)$stmt->fetchColumn();
}
$stmt = $pdo->prepare('SELECT MAX(running_balance) FROM vault WHERE user = :user');
$stmt->bindValue(':user', $name);
$stmt->execute();
return (int)$stmt->fetchColumn();
}
This is the same SQL-based implementation from the previous balance-fix plan, before /money was added.
- Step 2: Verify the file parses
Run: php -l includes/utilities.php
Expected: No syntax errors detected.
- Step 3: Run the existing golden-file test (no regression)
Run: php tests/process_log_entries_test.php
Expected: OK: processLogEntries v2 golden-file test passed (100 entries).
(The live_balance_test.php is expected to FAIL after this commit because fetchLiveVaultBalance is now unused but still defined. That's OK — we'll delete the test in Task 4. The golden-file test is the load-bearing one.)
- Step 4: Commit
git add includes/utilities.php
git commit -m "Revert vaultLoop to per-user SQL MAX(running_balance)"
Task 2: Replace backfillRunningBalances with refetchRunningBalances
Files:
- Modify:
includes/utilities.php(replace the existing function body)
Interfaces:
-
refetchRunningBalances(PDO $pdo, string $user): void- Counts rows for the user where
running_balance IS NULL. If 0, returns. - Otherwise, fetches the user's full
/loghistory (paginating with_metadata.links.nextuntil null), and for each entry runs anUPDATE vault SET running_balance = X WHERE id = Yto set the per-entry value fromdata.balance. Each fetched entry has its own historical balance, which is the correct semantics. - Calls
executeApiCallandvalidateApiResponse(existing helpers). - On API failure, throws the underlying exception (caller catches via try/catch in
index.php).
- Counts rows for the user where
-
Step 1: Replace the function
In includes/utilities.php, find the existing backfillRunningBalances function and replace it with:
/**
* Re-fetch the running_balance for each entry of a user by walking the
* v2 /log paginated endpoint.
*
* For each entry the API returns, we run a single UPDATE setting that
* entry's `running_balance` to its `data.balance`. The pre-existing
* broadcast approach gave every row of a user the same value, which
* made the historical view misleading; this restores per-entry accuracy.
*
* If the user has zero rows with `running_balance IS NULL`, returns
* without making any HTTP call. On API failure, the underlying
* exception propagates to the caller's try/catch in index.php.
*
* @param PDO $pdo Database connection.
* @param string $user The user whose NULL rows should be refilled.
*
* @return void
*/
function refetchRunningBalances($pdo, $user) {
if (!array_key_exists($user, USER_KEYS)) {
throw new ApiKeyMissingException("User does not have an API key configured.");
}
$countStmt = $pdo->prepare('SELECT COUNT(*) FROM vault WHERE user = :user AND running_balance IS NULL');
$countStmt->bindValue(':user', $user);
$countStmt->execute();
if ((int)$countStmt->fetchColumn() === 0) {
return;
}
$updateStmt = $pdo->prepare(
'UPDATE vault SET running_balance = :balance WHERE id = :id'
);
$url = 'https://api.torn.com/v2/user?selections=log&log=5850,5851';
do {
$responseData = executeApiCall($url, USER_KEYS[$user]);
validateApiResponse($responseData);
foreach ($responseData['log'] as $entry) {
$id = $entry['id'] ?? null;
if (!$id || !isset($entry['data']['balance'])) {
continue;
}
$updateStmt->bindValue(':balance', (int)$entry['data']['balance']);
$updateStmt->bindValue(':id', $id);
$updateStmt->execute();
}
$url = $responseData['_metadata']['links']['next'] ?? null;
} while ($url !== null);
}
- Step 2: Verify the file parses
Run: php -l includes/utilities.php
Expected: No syntax errors detected.
- Step 3: Run the golden-file test (no regression)
Run: php tests/process_log_entries_test.php
Expected: OK: processLogEntries v2 golden-file test passed (100 entries).
- Step 4: Commit
git add includes/utilities.php
git commit -m "Replace broadcast backfill with per-entry re-fetch for running_balance"
Task 3: Wire refetchRunningBalances into entry points
Files:
- Modify:
functions.php(backfillUserLogsandsyncUserLogs)
Interfaces:
-
Same as before, but the post-pagination call is now to
refetchRunningBalances(notbackfillRunningBalances). -
Step 1: Update
backfillUserLogs
In functions.php, replace the last line of backfillUserLogs (backfillRunningBalances($pdo, $user);) with refetchRunningBalances($pdo, $user);.
- Step 2: Update
syncUserLogs
Same edit in syncUserLogs.
- Step 3: Verify the file parses
Run: php -l functions.php
Expected: No syntax errors detected.
- Step 4: Commit
git add functions.php
git commit -m "Invoke refetchRunningBalances after sync/backfill"
Task 4: Delete unused fetchLiveVaultBalance and its test
Files:
- Modify:
includes/utilities.php(delete the helper function and the test-hook shim line at top of file) - Delete:
tests/live_balance_test.php
Interfaces:
-
fetchLiveVaultBalanceis removed entirely. Any callers (none after Task 1's revert) won't find it. -
The
$GLOBALS['liveBalanceTestHook']shim line is removed. -
Step 1: Delete the test hook shim
In includes/utilities.php, near the top of the file (right after the doc-block and before function dbNew), find and delete the line:
// Test hook for fetchLiveVaultBalance(). When set, this closure is called
// instead of executeApiCall. Set $GLOBALS['liveBalanceTestHook'] to a
// closure($url, $apiKey): array in test code. Production code leaves it null.
$GLOBALS['liveBalanceTestHook'] = $GLOBALS['liveBalanceTestHook'] ?? null;
- Step 2: Delete the
fetchLiveVaultBalancefunction
Find and delete the entire fetchLiveVaultBalance function (including its docblock) at the end of includes/utilities.php.
- Step 3: Delete the test file
git rm tests/live_balance_test.php
- Step 4: Verify the file parses
Run: php -l includes/utilities.php
Expected: No syntax errors detected.
- Step 5: Run the golden-file test (no regression)
Run: php tests/process_log_entries_test.php
Expected: OK: processLogEntries v2 golden-file test passed (100 entries).
- Step 6: Verify the test file is gone
Run: ls tests/
Expected: only process_log_entries_test.php.
- Step 7: Commit
git add includes/utilities.php tests/live_balance_test.php
git commit -m "Remove unused fetchLiveVaultBalance and its test"
Task 5: End-to-end smoke check
Files: No code changes. Verification only.
- Step 1: Run the test suite
Run: php tests/process_log_entries_test.php
Expected: OK: processLogEntries v2 golden-file test passed (100 entries).
- Step 2: Probe the test instance
curl -s http://127.0.0.1:8001/ | grep -E "<h2>|<h3>|<h4>" | head -15
Expected: per-user balances come from MAX(running_balance) per user (which after the re-fetch will be the historical entry's actual data.balance, not the broadcast value). For Zarathos whose log last saw data.balance=$69,235,831, that's what shows. For Symos whose most recent entry has data.balance=$160,627,669 (or whatever the latest API returns), that's what shows. Both values reflect the per-entry historical balance, not the broadcast approximation.
- Step 3: Verify no v1 references remain
cd "C:\Users\ksolo\Projects\Games\Torn\Torn Vault Tracker"
grep -n "firstRun\|?key=\|fetchLiveVaultBalance" functions.php index.php includes/utilities.php includes/exceptions.php tests/
Expected: no matches (including no references to the deleted fetchLiveVaultBalance).
- Step 4: Commit if any incidental fixes were needed
If Step 2 or 3 produced any required fixes, commit them. Otherwise, no commit.
Self-Review Notes
Spec coverage:
| Requirement | Task |
|---|---|
Per-user balance via SQL MAX(running_balance) |
Task 1 |
| Replace broadcast backfill with per-entry re-fetch | Task 2 |
| Wire re-fetch into entry points | Task 3 |
Delete unused fetchLiveVaultBalance and its test |
Task 4 |
| End-to-end smoke check | Task 5 |
Placeholder scan: No "TODO", "TBD", "implement later".
Type consistency: refetchRunningBalances(PDO $pdo, string $user): void matches the call sites in Task 3. The vaultLoop SQL matches the documented contract.
Risks addressed:
- The broadcast-backfill defect is fixed by per-entry re-fetch.
- Unused code is removed (dead-weight cleanup).
- The
vaultLooprevert doesn't introduce regressions because the existing golden-file test still passes.