Skip to content

Startup Init & Reconciliation ​

Business modules declare menus, roles, buttons, and seed data via init_data.py. On every startup, autodiscover invokes each module's init(). Below: the sync semantics for each data category and how a module becomes the single source of truth for its subtree.

Startup flow ​

In app/__init__.py's lifespan, the Redis-elected leader worker runs (in order):

  1. init_menus() — system menus (app/system/init_data.py)
  2. refresh_api_list() — full reconciliation of FastAPI routes ↔ Api table
  3. init_users() — system seed users
  4. iterate each business module's init_data.init() (Inventory, then any new modules)
  5. refresh_all_cache() — reload permission / menu cache

Multi-worker coordination uses Redis lock app:init_lock so only one worker runs init; the rest wait for the done signal. The leader deletes stale locks before each start, so init really runs on every restart.

Sync semantics by data type ​

Data typeHow it syncsField changeAddRemoveRename
API (routes)refresh_api_list full reconcile✅ auto✅ auto✅ auto✅ auto (delete + insert)
Menuensure_menu upsert + optional reconcile_menu_subtree✅ auto✅ auto⚠️ requires reconcile⚠️ requires reconcile
Buttonensure_menu upsert + optional reconcile_menu_subtree✅ auto✅ auto⚠️ requires reconcile⚠️ requires reconcile
Roleensure_role upsert; relations clear-and-readd✅ auto✅ auto❌ manual cleanup❌ manual cleanup
Role grants (menus / buttons / apis)clear-and-readd✅ auto✅ auto✅ auto⚠️ missing emits warning
Business seed data_safe_update_or_create by unique key✅ auto✅ auto❌ manual cleanup❌ manual cleanup

Legend: ✅ takes effect on restart; ⚠️ needs explicit opt-in; ❌ needs SQL / migration.

ensure_menu is forward-applying (upsert only). Removing entries from init_data.py therefore doesn't clean the DB. app/system/services/init_helper.py provides reconcile_menu_subtree to scope an init declaration as the single source of truth for its subtree.

Usage ​

just cli-gen / just cli-crud now writes MODULE_MENU_CHILDREN, ensure_menu() and reconcile_menu_subtree() into the module's generated init_data.py. By default it reconciles the selected model menus, but keeps declared_button_codes=None, so buttons maintained from the Web UI are not cleaned up. When generation uses --button-auth, the CLI also declares create/edit/delete buttons and points declared_button_codes at the declared set, making buttons IaC-managed too.

In your module's init_data.py:

python
from app.system.services import ensure_menu, reconcile_menu_subtree


def _collect_declared_routes(children: list[dict]) -> set[str]:
    result: set[str] = set()
    for item in children:
        result.add(item["route_name"])
        if item.get("children"):
            result.update(_collect_declared_routes(item["children"]))
    return result


def _collect_declared_buttons(children: list[dict]) -> set[str]:
    result: set[str] = set()
    for item in children:
        for btn in item.get("buttons") or []:
            result.add(btn["button_code"])
        if item.get("children"):
            result.update(_collect_declared_buttons(item["children"]))
    return result


async def _init_menu_data() -> None:
    await ensure_menu(
        menu_name="CRM",
        route_name="crm",
        route_path="/crm",
        icon="mdi:account-box",
        order=8,
        children=CRM_MENU_CHILDREN,
    )
    await reconcile_menu_subtree(
        root_route="crm",
        declared_route_names=_collect_declared_routes(CRM_MENU_CHILDREN),
        declared_button_codes=_collect_declared_buttons(CRM_MENU_CHILDREN),
    )

Scope and semantics ​

  • Subtree-scoped: BFS from root_route's menu, only acts within that subtree
  • Menu reconciliation: menus in the subtree whose route_name is not in declared_route_names ∪ {root_route} get delete()d (cascades M2M)
  • Button reconciliation: only buttons mounted on subtree menus; those whose button_code is not in declared_button_codes get delete()d. Pass declared_button_codes=None to skip button reconciliation.
  • Idempotent: re-running with no extras is a no-op

Mental shift ​

After enabling reconcile_menu_subtree, that subtree becomes Infrastructure-as-Code:

  • ✅ edit init_data.py, restart, done
  • ✅ rename = delete-old + insert-new, auto handled
  • ❌ Web-UI-created menus / buttons under that subtree get reaped on next restart

If you need user-driven dynamic menus, do not call reconcile_menu_subtree on that subtree.

ensure_role config-drift warnings ​

ensure_role does clear-and-readd for menus / buttons / apis. If a declared route_name / button_code / (method, path) doesn't resolve in the DB, you get:

ensure_role 'R_DEPT_MGR': missing apis [('patch', '/api/v1/business/inventory/warehouses/{dept_id}/old')] (route signature changed?)

Common causes:

  • A route was renamed / removed but the seed apis wasn't updated
  • A button_code was renamed but the seed buttons wasn't updated
  • A parent route_name changed but the seed menus wasn't updated

Fix on sight — otherwise the role permission silently disappears and users hit mysterious 403s.

Manual cleanup for removals ​

reconcile_menu_subtree only handles menus and buttons. The following still need manual cleanup (SQL or Tortoise migration):

  1. Roles — removing INVENTORY_ROLE_SEEDS entries does NOT delete the Role row, nor does it touch user-role associations
  2. Business seed data — same for INVENTORY_WAREHOUSE_SEEDS / INVENTORY_TAG_SEEDS / INVENTORY_PRODUCT_SEEDS
  3. Cross-subtree orphan buttons — buttons referenced by multiple subtrees can survive cleanup of one of them

These scenarios are rare and removal cascades are subtle (does deleting a role also unbind its users?), so the framework intentionally avoids automation here — write a proper migration.

Summary ​

ScenarioRecommended action
Add menu / button / role / seedEdit init_data.py, restart
Edit menu fields (name / icon / order)Edit init_data.py, restart
Delete menu / buttonEnable reconcile_menu_subtree, edit init_data.py, restart
Rename route_name / button_codeEnable reconcile_menu_subtree, restart auto delete-old + insert-new
Delete role / business seedWrite Tortoise migration
Modify API route pathSync apis list in INVENTORY_ROLE_SEEDS, otherwise warning at startup

基于 MIT 协议发布