fix(ai_mail): retire dashboard_sync writer — stop writing the ai_mail dashboard section (prax self-sources)

This commit is contained in:
AIOSAI
2026-06-13 00:13:23 -07:00
parent 37fb07ca16
commit d9a2a48a1e
15 changed files with 33 additions and 196 deletions
+5
View File
@@ -47,6 +47,11 @@ PyPI version — not the changelog header.
`.ai_mail.local/inbox.json` directly (`_read_mail_counts`) for `new_mail`/
`opened_mail`, so the `ai_mail` section is no longer a data dependency and can
be retired. 116 prax tests, seedgo 100%.
- **Retired `@ai_mail`'s dashboard section writer (completes the dashboard
slim).** ai_mail no longer writes to the dashboard — removed
`push_dashboard_update` from 5 call sites and archived `dashboard_sync.py`.
With prax self-sourcing mail counts, the `ai_mail` section now stays gone (a
mail op no longer re-adds it — verified). 737 ai_mail tests.
- **`.backupignore` is now a true `.gitignore` for the backup system — a single
source of truth (FPLAN-0269).** Replaced the hand-rolled `fnmatch`+part-loop
matcher (which broke leading-slash anchoring, `*`-crossing-`/`, dir-only `foo/`,
-15
View File
@@ -60,11 +60,6 @@
"standard": "deep_nesting",
"reason": "2 functions: get_user_by_email() depth 4, get_all_users() depth 4 — registry lookup with path normalization and validation"
},
{
"file": "apps/handlers/email/dashboard_sync.py",
"standard": "handlers",
"reason": "Imports prax.apps.modules.dashboard.write_section — cross-branch module import required for dashboard integration. No ai_mail module wraps this."
},
{
"file": "apps/handlers/email/delivery.py",
"standard": "handlers",
@@ -120,11 +115,6 @@
"standard": "naming",
"reason": "False positive — _append_footer is a function reference stored in a local variable, not a module-level constant."
},
{
"file": "apps/handlers/email/dashboard_sync.py",
"standard": "naming",
"reason": "False positive — _write_section is a lazy-import function reference, not a module-level constant."
},
{
"file": "apps/handlers/email/delivery.py",
"standard": "naming",
@@ -200,11 +190,6 @@
"standard": "deep_nesting",
"reason": "_send_direct() depth 5 (arg parsing with branch resolution, --from flag, --dispatch flag), handle_close() depth 4 (close with archive + dashboard update)"
},
{
"file": "apps/handlers/email/dashboard_sync.py",
"standard": "deep_nesting",
"reason": "_human_readable_age() depth 5, _calculate_section_data() depth 5 — timestamp parsing with multiple fallback formats"
},
{
"file": "apps/handlers/dispatch/dispatch_monitor.py",
"standard": "deep_nesting",
@@ -56,24 +56,17 @@ def batch_close(
def batch_close_post_ops(
branch_path: Path,
push_dashboard_fn: Optional[Callable] = None,
update_central_fn: Optional[Callable] = None,
purge_deleted_fn: Optional[Callable] = None,
) -> None:
"""
Run post-operations after a batch close (dashboard update + purge).
Run post-operations after a batch close (central update + purge).
Args:
branch_path: Path to branch directory
push_dashboard_fn: Optional push_dashboard_update callable
update_central_fn: Optional update_central callable
purge_deleted_fn: Optional purge_deleted_folder callable
"""
if push_dashboard_fn:
try:
push_dashboard_fn(branch_path)
except Exception as e:
logger.warning("[close] push_dashboard_fn failed for %s: %s", branch_path, e)
if update_central_fn:
try:
update_central_fn()
@@ -93,25 +93,18 @@ def on_email_delivered(
new_count: int,
opened_count: int,
total: int,
push_dashboard_fn: Optional[Callable] = None,
update_central_fn: Optional[Callable] = None,
) -> None:
"""
Post-delivery callback: update dashboard and central.
Post-delivery callback: update central.
Args:
branch_path: Path to the branch that received email
new_count: Number of new (unread) messages
opened_count: Number of opened messages
total: Total message count
push_dashboard_fn: Callable for push_dashboard_update
update_central_fn: Callable for update_central
"""
if push_dashboard_fn:
try:
push_dashboard_fn(branch_path)
except Exception as e:
logger.warning("[error_dispatch] dashboard update failed for %s: %s", branch_path, e)
if update_central_fn:
try:
update_central_fn()
@@ -38,13 +38,6 @@ def _get_inbox_lock():
return _inbox_lock
def _get_push_dashboard_update() -> Any:
"""Lazy import push_dashboard_update from dashboard_sync."""
from aipass.ai_mail.apps.handlers.email.dashboard_sync import push_dashboard_update
return push_dashboard_update
def _get_update_central() -> Any:
"""Lazy import update_central."""
from aipass.ai_mail.apps.handlers.central_writer import update_central
@@ -191,13 +184,7 @@ def mark_all_read_and_archive(branch_path: Path) -> Tuple[bool, str, int]:
def _update_dashboard(branch_path: Path, new: int, opened: int, total: int) -> None:
"""Update dashboard ai_mail section with enriched data via write-through API."""
try:
_get_push_dashboard_update()(branch_path)
except Exception as e:
logger.warning("[cleanup] dashboard update failed for %s: %s", branch_path, e)
# Update central after any inbox changes
"""Update central stats after inbox changes."""
try:
_get_update_central()()
except Exception as e:
@@ -267,7 +267,6 @@ def _orchestrate_dispatch_send(args: List[str]) -> bool:
from aipass.ai_mail.apps.handlers.email.delivery import deliver_email_to_branch
from aipass.ai_mail.apps.handlers.email.header import prepend_dispatch_header
from aipass.ai_mail.apps.handlers.email.error_dispatch import dispatch_send_error, on_email_delivered
from aipass.ai_mail.apps.handlers.email.dashboard_sync import push_dashboard_update
from aipass.ai_mail.apps.handlers.users.user import get_current_user
from aipass.ai_mail.apps.handlers.registry.read import get_branch_by_email
@@ -286,7 +285,6 @@ def _orchestrate_dispatch_send(args: List[str]) -> bool:
new_count,
opened_count,
total,
push_dashboard_fn=push_dashboard_update,
update_central_fn=update_central,
)
+4 -9
View File
@@ -23,15 +23,8 @@ import sys
from pathlib import Path
from typing import List
# Infrastructure
_AI_MAIL_DIR = Path(__file__).resolve().parents[2]
_REPO_ROOT = _AI_MAIL_DIR.parents[2]
from aipass.prax import logger
from aipass.cli.apps.modules import console, error
# Handlers - business logic providers
from aipass.ai_mail.apps.handlers.email.dashboard_sync import push_dashboard_update
from aipass.ai_mail.apps.handlers.email.create import load_email_file
from aipass.ai_mail.apps.handlers.email.format import format_email_list_item, format_email_header
from aipass.ai_mail.apps.handlers.email.inbox_ops import load_inbox
@@ -48,6 +41,9 @@ from aipass.ai_mail.apps.handlers.email.close_ops import batch_close, batch_clos
from aipass.ai_mail.apps.handlers.email.inbox_resolve import resolve_inbox_target
from aipass.ai_mail.apps.modules.email_send import handle_send
_AI_MAIL_DIR = Path(__file__).resolve().parents[2]
_REPO_ROOT = _AI_MAIL_DIR.parents[2]
try:
from aipass.ai_mail.apps.handlers.central_writer import update_central
except ImportError as e:
@@ -255,7 +251,7 @@ def handle_close(args: List[str]) -> bool:
except ImportError as e:
logger.warning("[email] purge import unavailable: %s", e)
run_purge = None
batch_close_post_ops(branch_path, push_dashboard_update, update_central, run_purge)
batch_close_post_ops(branch_path, update_central, run_purge)
console.print(f"\nClosed {closed}, failed {failed}")
return True
except Exception as e:
@@ -387,7 +383,6 @@ def print_introspection():
console.print(" - reply.py (get_email_by_id — retrieve email by message ID)")
console.print(" - reply.py (send_reply — send reply to an email)")
console.print(" - header.py (prepend_dispatch_header — prepend dispatch header to message)")
console.print(" - dashboard_sync.py (push_dashboard_update — push email stats to dashboard)")
console.print(" - error_dispatch.py (dispatch_send_error — handle and report send errors)")
console.print(" - error_dispatch.py (on_email_delivered — post-delivery callback handler)")
console.print(" handlers/users/")
@@ -17,14 +17,10 @@ under the size threshold.
from pathlib import Path
from typing import List
_AI_MAIL_DIR = Path(__file__).resolve().parents[2]
_REPO_ROOT = _AI_MAIL_DIR.parents[2]
from aipass.prax import logger
from aipass.cli.apps.modules import console, error
from aipass.trigger.apps.modules.core import trigger
from aipass.ai_mail.apps.handlers.email.dashboard_sync import push_dashboard_update
from aipass.ai_mail.apps.handlers.email.delivery import deliver_email_to_branch
from aipass.ai_mail.apps.handlers.email.create import create_email_file, load_email_file
from aipass.ai_mail.apps.handlers.email.header import prepend_dispatch_header
@@ -40,6 +36,9 @@ from aipass.ai_mail.apps.handlers.email.send import (
from aipass.ai_mail.apps.handlers.email.error_dispatch import dispatch_send_error, on_email_delivered
from aipass.ai_mail.apps.handlers.email.send_args import parse_send_args, resolve_dispatch_target
_AI_MAIL_DIR = Path(__file__).resolve().parents[2]
_REPO_ROOT = _AI_MAIL_DIR.parents[2]
try:
from aipass.ai_mail.apps.handlers.central_writer import update_central
except ImportError as e:
@@ -54,7 +53,6 @@ def _delivery_callback(branch_path, new_count, opened_count, total):
new_count,
opened_count,
total,
push_dashboard_fn=push_dashboard_update,
update_central_fn=update_central,
)
+5 -26
View File
@@ -120,13 +120,11 @@ def test_batch_close_post_ops_all_fns_called(tmp_path: Path):
branch_path = tmp_path / "branch"
branch_path.mkdir()
push_fn = MagicMock()
central_fn = MagicMock()
purge_fn = MagicMock()
mod.batch_close_post_ops(branch_path, push_fn, central_fn, purge_fn)
mod.batch_close_post_ops(branch_path, central_fn, purge_fn)
push_fn.assert_called_once_with(branch_path)
central_fn.assert_called_once_with()
purge_fn.assert_called_once_with(branch_path / ".ai_mail.local")
@@ -137,22 +135,7 @@ def test_batch_close_post_ops_none_fns(tmp_path: Path):
branch_path.mkdir()
# Should not raise
mod.batch_close_post_ops(branch_path, None, None, None)
def test_batch_close_post_ops_push_exception_suppressed(tmp_path: Path):
"""Exception in push_dashboard_fn is caught; other fns still called."""
branch_path = tmp_path / "branch"
branch_path.mkdir()
push_fn = MagicMock(side_effect=RuntimeError("push failed"))
central_fn = MagicMock()
purge_fn = MagicMock()
mod.batch_close_post_ops(branch_path, push_fn, central_fn, purge_fn)
central_fn.assert_called_once()
purge_fn.assert_called_once()
mod.batch_close_post_ops(branch_path, None, None)
def test_batch_close_post_ops_central_exception_suppressed(tmp_path: Path):
@@ -160,13 +143,11 @@ def test_batch_close_post_ops_central_exception_suppressed(tmp_path: Path):
branch_path = tmp_path / "branch"
branch_path.mkdir()
push_fn = MagicMock()
central_fn = MagicMock(side_effect=RuntimeError("central failed"))
purge_fn = MagicMock()
mod.batch_close_post_ops(branch_path, push_fn, central_fn, purge_fn)
mod.batch_close_post_ops(branch_path, central_fn, purge_fn)
push_fn.assert_called_once()
purge_fn.assert_called_once()
@@ -175,13 +156,11 @@ def test_batch_close_post_ops_purge_exception_suppressed(tmp_path: Path):
branch_path = tmp_path / "branch"
branch_path.mkdir()
push_fn = MagicMock()
central_fn = MagicMock()
purge_fn = MagicMock(side_effect=RuntimeError("purge failed"))
mod.batch_close_post_ops(branch_path, push_fn, central_fn, purge_fn)
mod.batch_close_post_ops(branch_path, central_fn, purge_fn)
push_fn.assert_called_once()
central_fn.assert_called_once()
@@ -192,6 +171,6 @@ def test_batch_close_post_ops_partial_fns(tmp_path: Path):
central_fn = MagicMock()
mod.batch_close_post_ops(branch_path, None, central_fn, None)
mod.batch_close_post_ops(branch_path, central_fn, None)
central_fn.assert_called_once_with()
@@ -47,7 +47,6 @@ _H_CREATE = "aipass.ai_mail.apps.handlers.email.create"
_H_DELIVERY = "aipass.ai_mail.apps.handlers.email.delivery"
_H_HEADER = "aipass.ai_mail.apps.handlers.email.header"
_H_ERR = "aipass.ai_mail.apps.handlers.email.error_dispatch"
_H_DASH = "aipass.ai_mail.apps.handlers.email.dashboard_sync"
_H_USERS = "aipass.ai_mail.apps.handlers.users.user"
_H_REG = "aipass.ai_mail.apps.handlers.registry.read"
_H_CENTRAL = "aipass.ai_mail.apps.handlers.central_writer"
@@ -658,7 +657,6 @@ def _send_patches(overrides: dict | None = None) -> ExitStack:
f"{_H_HEADER}.prepend_dispatch_header": MagicMock(return_value="[DISPATCH] Body"),
f"{_H_SEND}.send_to_single": MagicMock(return_value=(True, None)),
f"{_H_ERR}.on_email_delivered": MagicMock(),
f"{_H_DASH}.push_dashboard_update": MagicMock(),
f"{_H_USERS}.get_current_user": MagicMock(return_value={"name": "test"}),
f"{_H_REG}.get_branch_by_email": MagicMock(return_value={"email": "@target"}),
f"{_H_CENTRAL}.update_central": MagicMock(),
@@ -385,7 +385,7 @@ class TestHandleClose:
post_ops_called = []
monkeypatch.setattr(
"aipass.ai_mail.apps.modules.email.batch_close_post_ops",
lambda bp, push_fn, central_fn, purge_fn: post_ops_called.append(True),
lambda bp, central_fn, purge_fn: post_ops_called.append(True),
)
mock_console = MagicMock()
mock_console.print = lambda msg, **kw: None
@@ -1286,7 +1286,7 @@ class TestHandleCloseExtended:
)
monkeypatch.setattr(
"aipass.ai_mail.apps.modules.email.batch_close_post_ops",
lambda bp, push_fn, central_fn, purge_fn: None,
lambda bp, central_fn, purge_fn: None,
)
printed: list[str] = []
errors: list[str] = []
@@ -1338,7 +1338,7 @@ class TestHandleCloseExtended:
post_ops_called: list[bool] = []
monkeypatch.setattr(
"aipass.ai_mail.apps.modules.email.batch_close_post_ops",
lambda bp, push_fn, central_fn, purge_fn: post_ops_called.append(True),
lambda bp, central_fn, purge_fn: post_ops_called.append(True),
)
printed: list[str] = []
mock_console = MagicMock()
@@ -1450,7 +1450,6 @@ class TestDeliveryCallback:
new_count,
opened_count,
total,
push_dashboard_fn=None,
update_central_fn=None,
):
"""Capture on_email_delivered arguments."""
@@ -1460,7 +1459,6 @@ class TestDeliveryCallback:
"new_count": new_count,
"opened_count": opened_count,
"total": total,
"push_dashboard_fn": push_dashboard_fn,
"update_central_fn": update_central_fn,
}
)
@@ -1478,7 +1476,6 @@ class TestDeliveryCallback:
assert delivered_args[0]["new_count"] == 3
assert delivered_args[0]["opened_count"] == 2
assert delivered_args[0]["total"] == 5
assert delivered_args[0]["push_dashboard_fn"] is not None
# ===========================================================================
@@ -155,51 +155,34 @@ def test_dispatch_send_error_passes_correct_email_data(monkeypatch):
# ---- on_email_delivered tests --------------------------------
def test_on_email_delivered_with_both_callbacks():
"""Both callbacks are invoked when provided."""
push_fn = MagicMock()
def test_on_email_delivered_with_central_callback():
"""Central callback is invoked when provided."""
update_fn = MagicMock()
branch_path = "/some/path"
on_email_delivered(branch_path, 3, 1, 10, push_fn, update_fn)
on_email_delivered(branch_path, 3, 1, 10, update_central_fn=update_fn)
push_fn.assert_called_once_with(branch_path)
update_fn.assert_called_once_with()
def test_on_email_delivered_with_none_callbacks():
"""No error when both callbacks are None."""
on_email_delivered("/some/path", 3, 1, 10, None, None)
def test_on_email_delivered_dashboard_failure_does_not_block_central():
"""Dashboard failure does not prevent central update from running."""
push_fn = MagicMock(side_effect=RuntimeError("dashboard broken"))
update_fn = MagicMock()
on_email_delivered("/some/path", 3, 1, 10, push_fn, update_fn)
push_fn.assert_called_once()
update_fn.assert_called_once()
"""No error when callback is None."""
on_email_delivered("/some/path", 3, 1, 10, None)
def test_on_email_delivered_central_failure_does_not_raise():
"""Central update failure is caught silently."""
push_fn = MagicMock()
update_fn = MagicMock(side_effect=RuntimeError("central broken"))
on_email_delivered("/some/path", 3, 1, 10, push_fn, update_fn)
on_email_delivered("/some/path", 3, 1, 10, update_central_fn=update_fn)
push_fn.assert_called_once()
update_fn.assert_called_once()
def test_on_email_delivered_both_fail_no_exception():
"""Both callbacks failing does not raise any exception."""
push_fn = MagicMock(side_effect=RuntimeError("push fail"))
def test_on_email_delivered_central_fail_no_exception():
"""Central callback failing does not raise any exception."""
update_fn = MagicMock(side_effect=RuntimeError("update fail"))
on_email_delivered("/some/path", 3, 1, 10, push_fn, update_fn)
on_email_delivered("/some/path", 3, 1, 10, update_central_fn=update_fn)
push_fn.assert_called_once()
update_fn.assert_called_once()
@@ -42,12 +42,6 @@ def _mock_inbox_lock(monkeypatch):
monkeypatch.setattr(mod, "_get_inbox_lock", lambda: _noop_lock)
@pytest.fixture(autouse=True)
def _mock_dashboard(monkeypatch):
"""Replace _get_push_dashboard_update with a no-op."""
monkeypatch.setattr(mod, "_get_push_dashboard_update", lambda: lambda _bp: None)
@pytest.fixture(autouse=True)
def _mock_central(monkeypatch):
"""Replace _get_update_central with a no-op."""
+1 -69
View File
@@ -1,6 +1,6 @@
"""Tests for miscellaneous handlers -- central_writer.update_central, dispatch status.check_pid_status,
daemon.run_daemon, json_handler.increment_counter/update_data_metrics, delivery.deliver_to_inbox_file,
dashboard_sync.push_dashboard_update, inbox_resolve.resolve_inbox_target."""
inbox_resolve.resolve_inbox_target."""
import json
import os
@@ -14,7 +14,6 @@ import aipass.ai_mail.apps.handlers.central_writer as central_mod
import aipass.ai_mail.apps.handlers.dispatch.daemon as daemon_mod
import aipass.ai_mail.apps.handlers.json_utils.json_handler as json_handler_mod
import aipass.ai_mail.apps.handlers.email.delivery as delivery_mod
import aipass.ai_mail.apps.handlers.email.dashboard_sync as dashboard_mod
from aipass.ai_mail.apps.handlers.central_writer import update_central
from aipass.ai_mail.apps.handlers.dispatch.status import check_pid_status
from aipass.ai_mail.apps.handlers.json_utils.json_handler import (
@@ -22,7 +21,6 @@ from aipass.ai_mail.apps.handlers.json_utils.json_handler import (
update_data_metrics,
)
from aipass.ai_mail.apps.handlers.email.delivery import deliver_to_inbox_file
from aipass.ai_mail.apps.handlers.email.dashboard_sync import push_dashboard_update
from aipass.ai_mail.apps.handlers.email.inbox_resolve import resolve_inbox_target
@@ -61,14 +59,6 @@ def _silence_json_handler_delivery():
yield mock_jh
@pytest.fixture(autouse=True)
def _silence_json_handler_dashboard():
"""Prevent log_operation in dashboard_sync from writing real JSON files."""
with patch("aipass.ai_mail.apps.handlers.email.dashboard_sync.json_handler") as mock_jh:
mock_jh.log_operation.return_value = True
yield mock_jh
@pytest.fixture(autouse=True)
def _silence_json_handler_inbox_resolve():
"""Prevent log_operation in inbox_resolve from writing real JSON files."""
@@ -399,64 +389,6 @@ def test_deliver_to_inbox_file_preserves_existing_messages(tmp_path, _noop_inbox
assert result["messages"][1]["subject"] == "Old email"
# ==============================================================
# push_dashboard_update tests
# ==============================================================
def test_push_dashboard_update_happy_path(tmp_path):
"""Successful dashboard push returns True."""
branch_path = tmp_path / "trigger"
inbox_dir = branch_path / ".ai_mail.local"
inbox_dir.mkdir(parents=True)
inbox_file = inbox_dir / "inbox.json"
inbox_data = {
"messages": [
{"id": "m1", "status": "new", "timestamp": "2026-04-01 10:00:00"},
{"id": "m2", "status": "opened", "timestamp": "2026-04-01 09:00:00"},
]
}
inbox_file.write_text(json.dumps(inbox_data), encoding="utf-8")
mock_write = MagicMock(return_value=True)
with patch.object(dashboard_mod, "_get_write_section", return_value=mock_write):
result = push_dashboard_update(branch_path)
assert result is True
mock_write.assert_called_once()
section_data = mock_write.call_args[0][1]
assert section_data == "ai_mail"
def test_push_dashboard_update_no_inbox(tmp_path):
"""Returns True with zero stats when no inbox exists."""
branch_path = tmp_path / "empty_branch"
branch_path.mkdir()
mock_write = MagicMock(return_value=True)
with patch.object(dashboard_mod, "_get_write_section", return_value=mock_write):
result = push_dashboard_update(branch_path)
assert result is True
mock_write.assert_called_once()
section_data = mock_write.call_args[0][2]
assert section_data["new"] == 0
assert section_data["total"] == 0
def test_push_dashboard_update_catches_exceptions(tmp_path):
"""Returns False on any exception (never raises)."""
branch_path = tmp_path / "broken"
branch_path.mkdir()
with patch.object(dashboard_mod, "_get_write_section", side_effect=RuntimeError("broken")):
result = push_dashboard_update(branch_path)
assert result is False
# ==============================================================
# resolve_inbox_target tests
# ==============================================================