5-min cron processes up to 100 pending rows per pass: private IPs
short-circuit to state=private_ip; same-IP cache (30 days) avoids
duplicate ip-api.com calls; reverse DNS via socket with 1.5s timeout;
HTTP lookup respects ip-api''s X-Rl rate-limit header. Tests cover
private-IP shortcut, cache hit (no HTTP), and internal-state skip --
no network calls needed.
Per-row isolation uses cr.savepoint() instead of cr.commit() because
Odoo 19 TestCursor raises AssertionError on commit/rollback. Recorded
the gotcha as CLAUDE.md rule #14.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds _fc_retention_gc() that deletes rows older than the configured
horizon (default 365 days; 0 = keep forever). Registered as a daily
ir.cron. Tests verify both the delete path and the "keep forever"
short-circuit.
Also documents the Odoo 19 gotcha that ir.cron dropped the numbercall
field (the legacy "-1 = run forever" pattern now raises ValueError at
install time; just omit the field).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mail template + helpers (_fc_alert_*, _fc_recent_failure_count,
_fc_send_failure_alert) wired into _check_credentials so that crossing
the consecutive-failure threshold within the window queues exactly one
mail.mail per attempted login per 60-minute cooldown. Master switch
x_fc_login_audit_alert_enabled honoured. Recipients are members of
base.group_system with a non-empty email and share=False; the
__system__ superuser is excluded by Odoo''s default user filter.
Tests (3 new, 22 total green):
test_failure_burst_queues_one_email
test_cooldown_suppresses_second_alert
test_alert_disabled_master_switch
setUp ensures base.user_admin has an email (fusion-dev''s admin user
ships without one; the only user with an email is __system__, which
is filtered out of standard res.users searches).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>