# Module Contracts: User Profile Dashboard Filter

## Contract Scope

This document defines semantic contracts for modules introduced or extended by this feature.  
Contracts are aligned with:

- feature requirements in [`spec.md`](../spec.md),
- UX states in [`ux_reference.md`](../ux_reference.md),
- entities in [`data-model.md`](../data-model.md).

---

# [DEF:backend.src.models.profile:Module]
# @TIER: STANDARD
# @SEMANTICS: profile, preferences, persistence, user, dashboard-filter
# @PURPOSE: Persist one dashboard filter preference record per authenticated user.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.models.auth
# @INVARIANT: Exactly one preference record exists per user_id.
class UserDashboardPreference:
    # @PURPOSE: Store bound Superset username and default filter toggle.
    # @PRE: user_id references existing user.
    # @POST: Record enforces one-to-one relation with user.
    def __init__(self): ...
# [/DEF:backend.src.models.profile:Module]

---

# [DEF:backend.src.core.superset_profile_lookup:Module]
# @TIER: STANDARD
# @SEMANTICS: superset, users, lookup, environment, normalization
# @PURPOSE: Query account candidates from selected Superset environment for profile binding.
# @LAYER: Infrastructure
# @RELATION: DEPENDS_ON -> backend.src.core.superset_client
# @RELATION: CALLS -> backend.src.core.utils.network.APIClient.request
# @INVARIANT: Account lookup never exposes upstream raw payload directly; output is normalized.
class SupersetAccountLookupAdapter:
    # @PURPOSE: Query Superset users list endpoint with pagination/sort options.
    # @PRE: Environment credentials are valid and authenticated.
    # @POST: Returns normalized account candidate page.
    def get_users_page(self): ...

    # @PURPOSE: Normalize Superset user payload to canonical candidate shape.
    # @PRE: raw payload may differ across Superset versions.
    # @POST: Returns stable fields {username, display_name, email, is_active}.
    def normalize_user_payload(self): ...
# [/DEF:backend.src.core.superset_profile_lookup:Module]

---

# [DEF:backend.src.services.profile_service:Module]
# @TIER: CRITICAL
# @SEMANTICS: profile, service, validation, ownership, filtering
# @PURPOSE: Enforce profile preference rules, account lookup orchestration, and actor matching semantics.
# @LAYER: Domain
# @RELATION: DEPENDS_ON -> backend.src.models.profile
# @RELATION: DEPENDS_ON -> backend.src.core.superset_client.profile_lookup_extension
# @RELATION: DEPENDS_ON -> backend.src.models.auth
# @INVARIANT: User can mutate only own preference record; matching uses trim+case-insensitive logic.
# @TEST_CONTRACT: ProfilePreferenceUpdateRequest -> ProfilePreferenceResponse
# @TEST_FIXTURE: valid_profile_update -> {"user_id":"u-1","superset_username":"John_Doe","show_only_my_dashboards":true}
# @TEST_EDGE: enable_without_username -> toggle=true with empty username returns validation error
# @TEST_EDGE: cross_user_mutation -> attempt to update another user preference returns forbidden
# @TEST_EDGE: lookup_env_not_found -> unknown environment_id returns not found
# @TEST_INVARIANT: normalization_consistency -> VERIFIED_BY: [valid_profile_update, enable_without_username]
class ProfileService:
    # @PURPOSE: Read authenticated user's current preference.
    # @PRE: current_user is authenticated.
    # @POST: Returns existing preference or default non-configured view.
    def get_my_preference(self): ...

    # @PURPOSE: Save authenticated user's preference with validation and normalization.
    # @PRE: current_user is authenticated; request payload is provided.
    # @POST: Preference persisted for current user only.
    def update_my_preference(self): ...

    # @PURPOSE: Lookup Superset account candidates in selected environment.
    # @PRE: environment_id exists and is reachable.
    # @POST: Returns normalized candidate list and paging metadata.
    def lookup_superset_accounts(self): ...

    # @PURPOSE: Determine if dashboard row matches bound username.
    # @PRE: owners and modified_by values are available from dashboard projection.
    # @POST: True when normalized username matches owners OR modified_by.
    def matches_dashboard_actor(self): ...
# [/DEF:backend.src.services.profile_service:Module]

---

# [DEF:backend.src.api.routes.profile:Module]
# @TIER: CRITICAL
# @SEMANTICS: api, profile, preferences, self-service, account-lookup
# @PURPOSE: Expose self-scoped profile preference read/update and environment-based Superset account lookup.
# @LAYER: API
# @RELATION: DEPENDS_ON -> backend.src.services.profile_service
# @RELATION: DEPENDS_ON -> backend.src.dependencies.get_current_user
# @INVARIANT: Endpoints are self-scoped; no target-user mutation path is accepted.
# @TEST_CONTRACT: ProfileRouteRequest -> ProfileRouteResponse
# @TEST_FIXTURE: get_my_preference_ok -> {"auth_user":"u-1","expected_status":200}
# @TEST_EDGE: unauthorized_request -> missing token returns 401
# @TEST_EDGE: invalid_username_payload -> returns 422 or domain validation error
# @TEST_EDGE: superset_lookup_upstream_error -> returns non-blocking lookup error payload
# @TEST_INVARIANT: self_scope_enforced -> VERIFIED_BY: [get_my_preference_ok, unauthorized_request]
module ProfileRouter:
    # @PURPOSE: GET own profile preference.
    # @PRE: Auth token valid.
    # @POST: Returns preference for authenticated user.
    def get_preferences(self): ...

    # @PURPOSE: PATCH own profile preference.
    # @PRE: Auth token valid and payload provided.
    # @POST: Persists preference and returns normalized result.
    def update_preferences(self): ...

    # @PURPOSE: GET Superset account candidates by selected environment.
    # @PRE: Auth token valid and environment_id provided.
    # @POST: Returns normalized account candidates or lookup warning metadata.
    def lookup_superset_accounts(self): ...
# [/DEF:backend.src.api.routes.profile:Module]

---

# [DEF:backend.src.api.routes.dashboards.profile_filter_extension:Module]
# @TIER: CRITICAL
# @SEMANTICS: dashboards, filter, profile-default, override
# @PURPOSE: Extend dashboards list contract with profile-default filter controls and effective filter metadata.
# @LAYER: API
# @RELATION: DEPENDS_ON -> backend.src.api.routes.dashboards
# @RELATION: CALLS -> backend.src.services.profile_service.matches_dashboard_actor
# @INVARIANT: Profile default filter auto-apply is allowed only for main dashboards list context.
# @TEST_CONTRACT: DashboardsListRequest -> DashboardsListResponse
# @TEST_FIXTURE: profile_filter_applied -> {"apply_profile_default":true,"override_show_all":false}
# @TEST_EDGE: override_show_all -> effective_filter_applied=false even when saved toggle=true
# @TEST_EDGE: no_match_results -> returns empty dashboards with valid pagination metadata
# @TEST_INVARIANT: pagination_integrity_under_filter -> VERIFIED_BY: [profile_filter_applied, no_match_results]
module DashboardProfileFilterExtension:
    # @PURPOSE: Parse profile-default query controls from request.
    # @PRE: Request query is provided.
    # @POST: Produces deterministic filter context.
    def parse_profile_filter_context(self): ...

    # @PURPOSE: Apply OR-based actor matching over owners and modified_by.
    # @PRE: Dashboard projection includes owners and modified_by fields.
    # @POST: Response list and totals reflect effective filter.
    def apply_profile_filter(self): ...
# [/DEF:backend.src.api.routes.dashboards.profile_filter_extension:Module]

---

<!-- [DEF:frontend.src.routes.profile.+page:Module] -->
<!--
@TIER: CRITICAL
@SEMANTICS: profile-page, preferences, lookup, save, ux-states
@PURPOSE: Provide profile UI for binding Superset account and saving default "my dashboards" preference.
@LAYER: UI
@RELATION: BINDS_TO -> frontend.src.lib.api
@RELATION: BINDS_TO -> frontend.src.lib.i18n
@INVARIANT: Save action never updates another user's preference.
@UX_STATE: Default -> Current preference shown with environment selector, account input, and toggle.
@UX_STATE: LookupLoading -> Account candidate list shows loading indicator for selected environment.
@UX_STATE: LookupError -> Inline warning shown; manual username entry remains enabled.
@UX_STATE: Saving -> Save button disabled and shows progress.
@UX_STATE: SaveSuccess -> Success toast appears and form reflects persisted values.
@UX_STATE: SaveError -> Error feedback shown without losing user input.
@UX_FEEDBACK: Toasts for save result, inline validation and lookup fallback messaging.
@UX_RECOVERY: User can retry lookup, manually enter username, or disable toggle to continue.
@TEST_CONTRACT: ProfilePageInteraction -> PreferencePersisted
@TEST_FIXTURE: bind_account_happy_path -> {"env_id":"dev","candidate":"j.doe","toggle":true}
@TEST_EDGE: lookup_failed_manual_fallback -> save still possible with manually entered username
@TEST_EDGE: invalid_username -> save blocked with inline validation error
@TEST_EDGE: cancel_changes -> persisted state remains unchanged
@TEST_INVARIANT: lookup_does_not_block_manual_save -> VERIFIED_BY: [lookup_failed_manual_fallback, bind_account_happy_path]
-->
module ProfilePage:
  /** @PURPOSE Render profile preference form and orchestrate lookup/save flows. */
  function renderAndHandleState() {}
<!-- [/DEF:frontend.src.routes.profile.+page:Module] -->

---

<!-- [DEF:frontend.src.routes.dashboards.+page.profile_filter_ui_extension:Module] -->
<!--
@TIER: CRITICAL
@SEMANTICS: dashboards-page, profile-filter-indicator, override, empty-state
@PURPOSE: Apply profile-default filter context on main dashboards page and provide temporary "show all" override.
@LAYER: UI
@RELATION: BINDS_TO -> frontend.src.lib.api
@RELATION: BINDS_TO -> frontend.src.routes.profile.+page
@INVARIANT: Temporary override never mutates saved profile preference.
@UX_STATE: FilterActive -> Visible badge "My Dashboards Only" with clear action.
@UX_STATE: OverrideActive -> Full list shown with hint that preference remains saved.
@UX_STATE: EmptyFiltered -> Friendly no-match message with path to profile adjustment.
@UX_FEEDBACK: Immediate list refresh after profile save and after override toggle.
@UX_RECOVERY: User can clear override or edit profile binding and reload list.
@TEST_CONTRACT: DashboardsFilterUiAction -> DashboardsGridState
@TEST_FIXTURE: default_profile_filter_applied -> {"saved_toggle":true,"username":"j.doe"}
@TEST_EDGE: no_matching_dashboards -> EmptyFiltered state rendered
@TEST_EDGE: temporary_show_all -> list restored while saved preference preserved
@TEST_EDGE: return_to_page -> FilterActive restored from saved preference
@TEST_INVARIANT: override_non_persistent -> VERIFIED_BY: [temporary_show_all, return_to_page]
-->
module DashboardsProfileFilterUI:
  /** @PURPOSE Render active-filter indicator and manage temporary override state. */
  function renderFilterIndicatorAndOverride() {}
<!-- [/DEF:frontend.src.routes.dashboards.+page.profile_filter_ui_extension:Module] -->

---

## Contract Trace (Key Scenario)

Scenario: User binds Superset account from environment and sees filtered dashboards by default.

1. [`frontend.src.routes.profile.+page`](#deffrontendsrcroutesprofilepagemodule) requests account candidates for selected environment.
2. [`backend.src.api.routes.profile`](#defbackendsrcapiroutesprofilemodule) validates self-scope and forwards lookup request.
3. [`backend.src.services.profile_service`](#defbackendsrcservicesprofile_servicemodule) orchestrates lookup via [`backend.src.core.superset_profile_lookup`](#defbackendsrccoresuperset_profile_lookupmodule).
4. User saves preference; profile service normalizes username and persists [`backend.src.models.profile`](#defbackendsrcmodelsprofilemodule).
5. On `/dashboards`, [`backend.src.api.routes.dashboards.profile_filter_extension`](#defbackendsrcapiroutesdashboardsprofile_filter_extensionmodule) applies `owners OR modified_by` matching and returns effective filter metadata.
6. [`frontend.src.routes.dashboards.+page.profile_filter_ui_extension`](#deffrontendsrcroutesdashboardspageprofile_filter_ui_extensionmodule) shows active badge and supports temporary "show all" override.

---

## Edge-case Trace Evidence (2026-03-05)

| Contract Edge / Invariant | Verification Test | Status |
|---|---|---|
| `enable_without_username` validation | `backend/src/api/routes/__tests__/test_profile_api.py::test_patch_profile_preferences_validation_error` | ✅ |
| `cross_user_mutation` guard | `backend/src/api/routes/__tests__/test_profile_api.py::test_patch_profile_preferences_cross_user_denied` | ✅ |
| `lookup_env_not_found` recovery path | `backend/src/api/routes/__tests__/test_profile_api.py::test_lookup_superset_accounts_env_not_found` | ✅ |
| `owners OR modified_by` normalization contract | `backend/src/api/routes/__tests__/test_dashboards.py::test_get_dashboards_profile_filter_contract_owners_or_modified_by` | ✅ |
| `override_show_all` semantics | `backend/src/api/routes/__tests__/test_dashboards.py::test_get_dashboards_override_show_all_contract` | ✅ |
| `no_match_results` under active profile filter | `backend/src/api/routes/__tests__/test_dashboards.py::test_get_dashboards_profile_filter_no_match_results_contract` | ✅ |
| page-scoped override restore UX | `frontend/src/routes/dashboards/__tests__/dashboard-profile-override.integration.test.js` | ✅ |