Reconcile verified account IDs and make language repairs observable
Magent CI/CD / verify (push) Successful in 1m50s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Skipped

This commit is contained in:
2026-09-11 19:56:32 +12:00
parent 38169b881e
commit de25255ea8
25 changed files with 593 additions and 160 deletions
+65
View File
@@ -0,0 +1,65 @@
"""Audited operator repair. Preview by default; --apply requires explicit authorization."""
import argparse
import asyncio
from contextlib import closing
import json
import os
from pathlib import Path
import sqlite3
from app import db
from app.services import duplicate_accounts as duplicates
from app.services import identity_review as review
async def reconcile(apply, output):
os.umask(0o077)
output.mkdir(parents=True, exist_ok=False, mode=0o700)
if apply:
with closing(db._connect()) as source, closing(sqlite3.connect(output / 'before.sqlite')) as target:
source.backup(target)
changes, blocked, seen = [], [], set()
actor = {'username': 'maintenance:authorized-identity-reconciliation'}
report, local, runtime = await review.review_identities()
initial = report['counts']
while True:
target = next((row for row in report['rows'] if row['candidate_jellyfin_id']
and row['candidate_jellyfin_id'] not in seen
and len(duplicates.identity_group(report, row)) > 1), None)
if not target:
break
identity = target['candidate_jellyfin_id']
seen.add(identity)
ids = [row['user']['id'] for row in duplicates.identity_group(report, target)]
with closing(db._connect()) as conn:
state = duplicates.account_state(conn, ids)
preview = duplicates.build_preview(report, local, runtime, state, target['user']['id'])
if preview['can_confirm']:
result = duplicates.consolidate(preview, report, local, runtime, state, actor) if apply else {
'kept_user_id': preview['keep_id'], 'consolidated': len(ids) - 1}
changes.append(result)
if apply:
report, local, runtime = await review.review_identities()
else:
blocked.append({'ids': ids, 'jellyfin_id': identity, 'issues': preview['issues']})
(output / 'progress.json').write_text(json.dumps({'changes': changes, 'blocked': blocked}))
if len(seen) % 20 == 0:
print('Reviewed groups:', len(seen), 'consolidated rows:', sum(r['consolidated'] for r in changes), flush=True)
if apply:
ready = [row['user']['id'] for row in report['rows'] if row['can_confirm']
and row['basis'] in {'confirmed_id', 'stored_jellyfin_id', 'stored_seerr_id'}]
if ready:
review.save_confirmations(report, local, runtime, ready, actor)
report, _, _ = await review.review_identities()
summary = {'applied': apply, 'before': initial, 'after': report['counts'],
'consolidated_rows': sum(r['consolidated'] for r in changes), 'groups': len(changes), 'blocked': blocked}
(output / 'result.json').write_text(json.dumps(summary, indent=2))
print(json.dumps(summary), flush=True)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--apply', action='store_true')
parser.add_argument('--output', type=Path, required=True, help='New private backup/report directory')
args = parser.parse_args()
asyncio.run(reconcile(args.apply, args.output))
+52
View File
@@ -0,0 +1,52 @@
const assert = require('node:assert/strict');
const { chromium } = require(process.env.PLAYWRIGHT_PACKAGE || 'playwright');
const base = process.env.REVIEW_BASE || 'http://localhost:3114';
(async () => {
const browser = await chromium.launch();
try {
const context = await browser.newContext();
await context.addCookies([{ name: 'magent_logged_in', value: '1', url: base }]);
let finish, original = false, outcome = 'attention'; const writes = [], errors = [];
await context.route('**/api/**', async route => {
const req = route.request(), path = new URL(req.url()).pathname;
const reply = json => route.fulfill({ json });
if (path === '/api/auth/me') return reply({ username: 'Admin', role: 'admin' });
if (path.endsWith('/snapshot')) return reply({ request_id: '3976', title: 'Downfall', request_type: 'movie', state: 'ADDED_TO_ARR', timeline: [], actions: [{ id: 'search_auto', label: 'Search and auto-download', requires_confirmation: false }], presentation: { status: { label: 'Waiting', meaning: 'Waiting for a release.' }, nextStep: { title: 'Search', description: 'Search for a download.', actionIds: ['search_auto'] }, pipeline: [] } });
if (path.endsWith('/language') && req.method() === 'GET') return reply({ language: { code: 'de' }, originalEnabled: original, canChange: true, profileLanguage: original ? 'Original' : 'English' });
if (path.includes('/operations/')) return reply({ id: path.split('/').pop(), label: 'Working on request', status: 'running', events: [{ id: 'search', service: 'Radarr', state: 'active', message: 'Searching indexers for Downfall…' }] });
if (path.endsWith('/actions/search_auto') || path.endsWith('/actions/language')) {
writes.push({ path, payload: req.postData() ? req.postDataJSON() : null });
await new Promise(resolve => { finish = resolve; });
if (path.endsWith('/language')) original = true;
return reply({ status: outcome, message: outcome === 'attention' ? 'Search finished, but no download appeared. Review matching releases and rejection reasons.' : 'Radarr has a download queued for this movie.' });
}
if (path.includes('/events/stream')) return route.fulfill({ contentType: 'text/event-stream', body: ': fixture\n\n' });
return reply({});
});
const page = await context.newPage(); page.on('pageerror', e => errors.push(e.message));
for (const width of [1440, 390]) {
original = false; outcome = 'attention';
await page.setViewportSize({ width, height: 900 }); await page.goto(base + '/requests/3976');
await page.getByRole('heading', { name: 'German audio may need your approval' }).waitFor();
await page.getByRole('button', { name: 'Search and auto-download', exact: true }).first().click();
let dialog = page.getByRole('dialog'); await dialog.waitFor();
assert(await dialog.getByText(/Working on your request/).isVisible(), 'Progress opens automatically');
await dialog.getByText('Searching indexers for Downfall…', { exact: true }).waitFor(); finish();
await dialog.getByText('Search finished, but no download appeared. Review matching releases and rejection reasons.', { exact: true }).waitFor();
await dialog.getByRole('button', { name: 'Dismiss activity' }).click();
outcome = 'downloading';
await page.getByRole('button', { name: 'Use German audio & search', exact: true }).click();
dialog = page.getByRole('dialog'); await dialog.waitFor();
await dialog.getByText('Searching indexers for Downfall…', { exact: true }).waitFor(); finish();
await dialog.getByText('Radarr has a download queued for this movie.', { exact: true }).waitFor();
if (process.env.REVIEW_DIR) await page.screenshot({ path: `${process.env.REVIEW_DIR}/downfall-progress-${width}.png`, animations: 'disabled' });
await dialog.getByRole('button', { name: 'Dismiss activity' }).click();
await page.getByRole('heading', { name: 'German audio enabled' }).waitFor();
assert.notEqual(await page.evaluate(() => document.body.style.overflow), 'hidden');
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
}
assert(writes.filter(w => w.path.endsWith('/language')).every(w => w.payload.acceptOriginalLanguage === true && w.payload.languageCode === 'de'));
assert.deepEqual(errors, []);
console.log('Passed: desktop/mobile automatic repair overlay, live events, no-download outcome, prominent German audio choice, saved state, queue confirmation and scroll restoration. All APIs intercepted.');
} finally { await browser.close(); }
})().catch(e => { console.error(e); process.exitCode = 1; });
+4 -3
View File
@@ -26,14 +26,15 @@ const base = process.env.REVIEW_BASE || 'http://localhost:3114';
await page.getByLabel('Title', { exact: true }).fill('Pans Labyrinth');
await page.getByRole('button', { name: 'Search Seerr' }).click();
await page.getByRole('button', { name: /Pan's Labyrinth/ }).click();
await page.getByRole('heading', { name: 'Check the audio language' }).waitFor();
await page.getByRole('heading', { name: 'Choose your audio language' }).waitFor();
assert(await page.locator('.request-language-notice').getByText('Spanish', { exact: true }).isVisible());
const consent = page.getByRole('checkbox');
const consent = page.getByRole('radio', { name: /Original Spanish audio/ });
assert(await page.getByRole('button', { name: 'Request movie', exact: true }).isDisabled());
assert(!await consent.isChecked());
await consent.check();
await page.getByRole('button', { name: 'Change title', exact: true }).click();
await page.getByRole('button', { name: /Pan's Labyrinth/ }).click();
await page.getByRole('heading', { name: 'Check the audio language' }).waitFor();
await page.getByRole('heading', { name: 'Choose your audio language' }).waitFor();
assert(!await consent.isChecked(), 'Consent resets when changing titles');
await consent.check();
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));