# TenantPilot Workspace

TenantPilot is an Intune management platform built around a stable Laravel application in
`apps/platform` and, starting with Spec 183, a standalone public Astro website in
`apps/website`. The repository root is now the official JavaScript workspace entry point and
orchestrates app-local commands without becoming a runtime itself.

## Multi-App Topology

- `apps/platform`: the Laravel 12 + Filament v5 + Livewire v4 product runtime
- `apps/website`: the Astro v6 public website runtime
- repo root: workspace manifests, documentation, scripts, editor tooling, and `docker-compose.yml`
- `./scripts/platform-sail`: platform-only compatibility helper for tooling that cannot set `cwd`

Website-track guardrails for independent evolution live in
[`docs/strategy/website-working-contract.md`](docs/strategy/website-working-contract.md).

## Official Root Commands

- Install workspace-managed JavaScript dependencies: `corepack pnpm install`
- Start the platform stack and Laravel panel Vite watcher: `corepack pnpm dev:platform`
- Start the website dev server: `corepack pnpm dev:website`
- Start platform Vite + website together: `corepack pnpm dev`
- Build the website: `corepack pnpm build:website`
- Build platform frontend assets inside Sail: `corepack pnpm build:platform`

## App-Local Commands

### Platform

- Install PHP dependencies: `cd apps/platform && composer install`
- Start Sail: `cd apps/platform && ./vendor/bin/sail up -d`
- Generate the app key: `cd apps/platform && ./vendor/bin/sail artisan key:generate`
- Apply incremental migrations on an already cut-over local database: `cd apps/platform && ./vendor/bin/sail artisan migrate`
- Initialize or reset the local database: `cd apps/platform && ./vendor/bin/sail artisan migrate:fresh --seed`
- Run frontend watch/build inside Sail: `corepack pnpm dev:platform`, `cd apps/platform && ./vendor/bin/sail pnpm dev`, or `cd apps/platform && ./vendor/bin/sail pnpm build`
- Run tests: `cd apps/platform && ./vendor/bin/sail artisan test --compact`

### Website

- Start the dev server: `cd apps/website && pnpm dev`
- Build the static site: `cd apps/website && pnpm build`

## Test Suite Governance

### Canonical Lane Commands

- Preferred repo-root wrappers:
  - `./scripts/platform-test-lane fast-feedback`
  - `./scripts/platform-test-lane confidence`
  - `./scripts/platform-test-lane heavy-governance`
  - `./scripts/platform-test-lane browser`
  - `./scripts/platform-test-lane profiling`
  - `./scripts/platform-test-lane junit`
- Regenerate the latest report artifacts without re-running the lane:
  - `./scripts/platform-test-report fast-feedback`
  - `./scripts/platform-test-report confidence`
  - `./scripts/platform-test-report heavy-governance`
  - `./scripts/platform-test-report browser`
  - `./scripts/platform-test-report profiling`
  - `./scripts/platform-test-report junit`
- Trend-aware report refresh options:
  - `--history-file=/absolute/path/to/<lane>-latest.trend-history.json` seeds one prior comparable window explicitly.
  - `--history-bundle=/absolute/path/to/bundle-or-zip` hydrates the newest matching `trend-history.json` from a staged artifact bundle.
  - `--fetch-latest-history` asks the wrapper to download the most recent comparable bundle from Gitea when `TENANTATLAS_GITEA_TOKEN` or `GITEA_TOKEN` is available.
  - `--skip-latest-history` keeps the run intentionally cold-start so the summary reports `unstable` instead of guessing at trend state.
- App-local equivalents remain available through Sail Composer scripts:
  - `cd apps/platform && ./vendor/bin/sail composer run test`
  - `cd apps/platform && ./vendor/bin/sail composer run test:confidence`
  - `cd apps/platform && ./vendor/bin/sail composer run test:heavy`
  - `cd apps/platform && ./vendor/bin/sail composer run test:browser`
  - `cd apps/platform && ./vendor/bin/sail composer run test:profile`
  - `cd apps/platform && ./vendor/bin/sail composer run test:junit`
- The root wrapper is the safer default for long lanes because it pins Composer to `--timeout=0`.

### Trend Summary Reading

- `healthy`: enough comparable samples exist, the lane is comfortably under budget, and recent variance stays inside the documented noise floor.
- `budget-near`: the lane is still within budget, but headroom has entered the lane's near-budget band and needs attention before it becomes a repeated blocker.
- `trending-worse`: multiple comparable samples are worsening above the lane variance floor even though the lane is not yet clearly over budget.
- `regressed`: the lane is over budget or repeatedly worsening enough that ordinary noise is no longer a credible explanation.
- `unstable`: the report intentionally refuses a stronger label because history is too short, the comparison fingerprint changed, or the recent window is noisy.
- Recalibration is separate from health. Reports can emit candidate, approved, or rejected baseline or budget decisions, but repository truth never moves automatically.
- Hotspot evidence may be unavailable on a given cycle. When that happens the summary must say so explicitly, and `profiling` or `junit` remain the preferred support-lane follow-up paths.

### Workflow Expectation

- Every runtime-changing or test-affecting spec, plan, and task set MUST record actual test-purpose classification, target validation lane(s), fixture-cost risks, any heavy-governance or browser expansion, any heavy-family visibility change, and any budget/baseline/trend follow-up.
- Test classification follows the real proving purpose of the change, not the filename or folder.
- Minimal fixtures and minimal infrastructure are the default; database, Livewire, Filament, provider, workspace, membership, or session-heavy setup must stay explicit and opt-in.
- Review treats wrong lane fit, hidden default cost, accidental heavy-family growth, or undocumented runtime drift as merge issues, not later cleanup.
- Routine lane recalibration belongs inside the affecting feature spec or PR; open a dedicated follow-up spec only when recurring pain or structural lane changes justify it.

### Spec 288 Cutover Proof

- Spec `288` is an enforcement-only package. It owns the named no-legacy guards, the two named browser smokes, and contributor-facing quality-gate wording. It does not own runtime cutover repair, Package Execution work, or a full-suite stabilization pass.
- The pinned no-legacy heavy-governance proof is:
  `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/Spec288NoLegacyRouteAndHelperGuardTest.php tests/Feature/Guards/Spec288ProviderCoreAndRoleAuthorityGuardTest.php tests/Feature/Guards/AdminWorkspaceRoutesGuardTest.php tests/Feature/Guards/ProviderBoundaryPlatformCoreGuardTest.php tests/Feature/ProviderConnections/LegacyRedirectTest.php tests/Feature/ManagedEnvironment/LegacyTenantCoreGuardTest.php tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php tests/Feature/Rbac/ProviderConnectionWorkspaceFirstPolicyTest.php tests/Feature/Filament/ManagedEnvironmentAccessScopeManagementTest.php tests/Feature/Guards/BrowserLaneIsolationTest.php tests/Feature/Guards/CiLaneFailureClassificationContractTest.php tests/Feature/Guards/CiHeavyBrowserWorkflowContractTest.php tests/Unit/Auth/NoRoleStringChecksTest.php)`
- The pinned browser proof is:
  `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && REPO_ROOT="$(git rev-parse --show-toplevel)" && (cd "$REPO_ROOT/apps/platform" && ./vendor/bin/sail artisan test --compact tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php)`
- Keep the source-scan inventory narrow and explicit. The Spec `288` guard family is limited to the named route/helper and provider/role guard files; do not widen it with broad `tests/Feature/Guards` path allowlists or repo-wide tenant-panel helper bans.
- When `heavy-governance` or `browser` reports broader baseline fallout outside those named proofs, classify it in the lane output and follow up separately. Under Spec `288`, that broader fallout remains classification-only and does not by itself expand repair ownership.

### Authoring And Review Guardrails

- Start with the smallest honest surface: `Unit` for isolated logic, `Feature` for HTTP, Livewire, Filament, jobs, or non-browser integration, `heavy-governance` for intentionally expensive governance scans, and `Browser` only for end-to-end workflow coverage.
- Specs and plans must state the affected lanes or a deliberate `N/A`, the family impact, the setup-cost impact, and the narrowest reviewer command.
- If database, Livewire, Filament, provider setup, workspace or membership context, session state, capability context, or browser coverage is required, say why a narrower proof is insufficient.
- Keep shared helpers, factories, seeds, fixtures, and defaults cheap by default. Full-context setup should stay behind explicit opt-ins instead of becoming the default path.
- Extend an existing heavy or browser family only when the behavior truly matches it. New heavy families, new browser scope, revived expensive defaults, or material lane-cost shifts require explicit escalation.
- Low-impact docs-only or template-only work may answer the governance prompts with `N/A` or `none`; do not invent runtime impact where none exists.
- Review should end with one explicit outcome: `keep`, `split`, `document-in-feature`, `follow-up-spec`, or `reject-or-split`.
- Use `document-in-feature` for contained drift or cost that belongs in the active feature. Use `follow-up-spec` for recurring pain or structural lane-model changes. Use `reject-or-split` when hidden cost is still unresolved.

### CI Trigger Matrix

- Pull requests (`opened`, `reopened`, `synchronize`) run only `./scripts/platform-test-lane fast-feedback` through `.gitea/workflows/test-pr-fast-feedback.yml` and block on test, wrapper, artifact, and mature Fast Feedback budget failures.
- Pushes to `dev` run only `./scripts/platform-test-lane confidence` through `.gitea/workflows/test-main-confidence.yml`; test and artifact failures block, while budget drift remains warning-first.
- Heavy Governance runs live in `.gitea/workflows/test-heavy-governance.yml` and stay isolated to manual plus scheduled triggers. The schedule remains gated behind `TENANTATLAS_ENABLE_HEAVY_GOVERNANCE_SCHEDULE=1` until the first successful manual validation.
- Browser runs live in `.gitea/workflows/test-browser.yml` and stay isolated to manual plus scheduled triggers. The schedule remains gated behind `TENANTATLAS_ENABLE_BROWSER_SCHEDULE=1` until the first successful manual validation.
- Fast Feedback uses a documented CI variance allowance of `15s` before budget overrun becomes blocking. Confidence uses `30s`, Browser uses `20s`, and Heavy Governance keeps warning-first or trend-only handling while its CI baseline stabilizes.

### CI Artifact Bundles

- Lane-local artifacts are still generated in `apps/platform/storage/logs/test-lanes` as `*-latest.*` files.
- CI workflows stage deterministic upload bundles through `./scripts/platform-test-artifacts` into `.gitea-artifacts/<workflow-profile>` before upload.
- Every governed CI lane now publishes `summary.md`, `budget.json`, `report.json`, `junit.xml`, and `trend-history.json`. `profiling` may additionally publish `profile.txt`.
- The report refresh step hydrates the most recent comparable `trend-history.json` before regenerating the current summary when CI credentials allow it, then republishes the refreshed bounded history for the next run.
- Artifact publication failures are first-class blocking failures for pull request and `dev` workflows.

### Recorded Baselines

| Scope | Wall clock | Budget | Notes |
|-------|------------|--------|-------|
| Full suite baseline | `2624.60s` | reference only | Current broad-suite measurement used as the budget anchor |
| `fast-feedback` | `176.74s` | `200s` | More than 50% below the current full-suite baseline |
| `confidence` | `394.38s` | `450s` | Broader non-browser pre-merge lane |
| `heavy-governance` | `83.66s` | `120s` | Seed heavy family lane for architecture, deprecation, ops UX, and action-surface scans |
| `browser` | `128.87s` | `150s` | Dedicated browser smoke and workflow lane |
| `junit` | `380.14s` | `450s` | Parallel machine-readable report lane for the confidence scope |
| `profiling` | `2701.51s` | `3000s` | Serial slow-test drift lane with profile output |

Artifacts are written under `apps/platform/storage/logs/test-lanes` and kept out of git except for the checked-in skeleton `.gitignore`.

### Honest Taxonomy Rules

- `Unit`: isolated logic, helpers, and low-cost domain behavior.
- `Feature`: HTTP, Livewire, Filament, jobs, and non-browser integration slices.
- `Browser`: only end-to-end browser smoke and workflow coverage under `tests/Browser`.
- `heavy-governance`: intentionally expensive architecture, deprecation, ops UX, and wide contract scans. The first seeded batch is `tests/Architecture`, `tests/Deprecation`, `tests/Feature/078`, `tests/Feature/090`, `tests/Feature/144`, `tests/Feature/OpsUx`, `tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php`, `tests/Feature/Guards/ActionSurfaceContractTest.php`, `tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php`, and `tests/Feature/ProviderConnections/CredentialLeakGuardTest.php`.

### Fixture Cost Guidance

- `createUserWithTenant()` now defaults to the explicit cheap `minimal` profile.
- Use `createMinimalUserWithTenant()` in high-usage callers that only need tenant membership and workspace/session wiring.
- Use `createStandardUserWithTenant()` or `fixtureProfile: 'standard'` when a test needs a default Microsoft provider connection without credentials, cache resets, or UI context.
- Use `createFullUserWithTenant()` or `fixtureProfile: 'full'` when a test intentionally needs provider, credential, cache-reset, and UI-context side effects together.
- Use `OperationRun::factory()->minimal()` for system-style runs and `OperationRun::factory()->withUser($user)` only when the initiator identity is materially part of the assertion.
- Use `BackupSet::factory()->full()` only when the test really needs backup items; the default backup-set factory path now stays item-free.
- `provider-enabled`, `credential-enabled`, `ui-context`, and `heavy` remain available only as temporary transition aliases while the first migration packs are landing.

### DB Reset and Seed Rules

- Default lanes use SQLite `:memory:` with `RefreshDatabase` as the reset strategy.
- The isolated PostgreSQL coverage remains the `Pgsql` suite and is reserved for schema or foreign-key assertions.
- Spec 279 managed-environment core cutover is destructive for old local databases. If a local database still contains legacy `tenants`, `tenant_user`, `tenant_memberships`, or `user_tenant_preferences` tables, reset it with `cd apps/platform && ./vendor/bin/sail artisan migrate:fresh --seed` instead of trying to preserve that schema.
- Keep seeds out of default lanes. Opt into seeded fixtures only inside the test that needs business-truth seed data.
- Schema-baseline or dump-based acceleration remains a follow-up investigation, not a default requirement for the current lane model.

## Port Overrides

- Platform HTTP and Vite ports: set `APP_PORT` and or `VITE_PORT` before `corepack pnpm dev:platform` or `cd apps/platform && ./vendor/bin/sail up -d`
- Website dev server port: set `WEBSITE_PORT` before `corepack pnpm dev:website` or pass `--port <port>` to `cd apps/website && pnpm dev`
- Parallel local development keeps both apps isolated, even when one or both ports are overridden

## Platform Setup Notes

- Filament admin: `/admin` (seed user `test@example.com`, set password via factory or `artisan tinker`).
- Microsoft Graph (Intune) env vars:
  - `GRAPH_TENANT_ID`
  - `GRAPH_CLIENT_ID`
  - `GRAPH_CLIENT_SECRET`
  - `GRAPH_SCOPE` (default `https://graph.microsoft.com/.default`)
  - Without these, the `NullGraphClient` runs in dry mode (no Graph calls).
  - **Required API Permissions**: See [docs/PERMISSIONS.md](docs/PERMISSIONS.md) for complete list
    - **Missing permissions?** Scope tags will show as "Unknown (ID: X)" - add `DeviceManagementRBAC.Read.All`
- Deployment (Dokploy, staging → production):
  - Containerized deploy; ensure Postgres + Redis are provisioned (see `docker-compose.yml` for local baseline).
  - Run application commands from `apps/platform`, including `php artisan filament:assets`.
  - Run migrations on staging first, validate backup/restore flows, then promote to production.
  - Ensure queue workers are running for jobs (e.g., policy sync) after deploy.
  - Keep secrets/env in Dokploy, never in code.

## Platform relocation rollout notes

- Open branches that still touch legacy root app paths should merge `dev` first, then remap file moves from `app/`, `bootstrap/`, `config/`, `database/`, `lang/`, `public/`, `resources/`, `routes/`, `storage/`, and `tests/` into `apps/platform/...`.
- Keep using merge-based catch-up on shared feature branches; do not rebase long-lived shared branches just to absorb the relocation.
- VS Code tasks expose the official root workspace commands, while MCP launchers remain platform-only and delegate through `./scripts/platform-sail`.

## Bulk operations (Feature 005)

- Bulk actions are available in Filament resource tables (Policies, Policy Versions, Backup Sets, Restore Runs).
- Destructive operations require type-to-confirm at higher thresholds (e.g. `DELETE`).
- Long-running bulk ops are queued; the bottom-right progress widget polls for active runs.

### Troubleshooting

- **Progress stuck on “Queued…”** usually means the queue worker is not running (or not processing the queue you expect).
  - Prefer using the Sail/Docker worker (see `docker-compose.yml`) rather than starting an additional local `php artisan queue:work`.
  - Check worker status/logs: `cd apps/platform && ./vendor/bin/sail ps` and `cd apps/platform && ./vendor/bin/sail logs -f queue`.
- **Exit code 137** for `queue:work` typically means the process was killed (often OOM). Increase Docker memory/limits or run the worker inside the container.
- **Moved app but old commands still fail** usually means the command is still being run from repo root. Switch to `cd apps/platform && ...` or use `./scripts/platform-sail ...` only for tooling that cannot set `cwd`.

## Rollback checklist

1. Revert the relocation commit or merge on your feature branch instead of hard-resetting shared history.
2. Preserve any local app env overrides before switching commits: `cp apps/platform/.env /tmp/tenantatlas.platform.env.backup` if needed.
3. Stop local containers and clean generated artifacts: `cd apps/platform && ./vendor/bin/sail down -v`, then remove `apps/platform/vendor`, `apps/platform/node_modules`, `apps/platform/public/build`, and `apps/platform/public/hot` if they need a clean rebuild.
4. After rollback, restore the matching env file for the restored topology and rerun the documented setup flow for that commit.
5. Notify owners of open feature branches that the topology changed so they can remap outstanding work before the next merge from `dev`.

## Deployment unknowns

- Dokploy build context for a repo-root compose file plus an app-root Laravel runtime still needs staging confirmation.
- Production web, queue, and scheduler working directories must be verified explicitly after the move; do not assume repo root and app root behave interchangeably.
- Any Dokploy volume mounts or storage persistence paths that previously targeted repo-root `storage/` must be reviewed against `apps/platform/storage/`.

### Configuration

- `TENANTPILOT_BULK_CHUNK_SIZE` (default `10`): job refresh/progress chunk size.
- `TENANTPILOT_BULK_POLL_INTERVAL_SECONDS` (default `3`): Livewire polling interval for the progress widget (clamped to 1–10s).

## Intune RBAC Onboarding Wizard

- Entry point: Tenant detail in Filament (`Setup Intune RBAC` in the ⋯ ActionGroup). Visible only for active tenants with `app_client_id`.
- Flow (synchronous, delegated):
  1) Configure Role (default Policy/Profile Manager), Scope (global or scope group), Group mode (create default `TenantPilot-Intune-RBAC` or pick existing security-enabled group). Review planned changes.
  2) Delegated admin login (short-lived token, **not** stored in DB/cache).
  3) Execute: resolve service principal, ensure/validate security group, ensure membership, ensure/create/patch Intune role assignment; persists IDs on tenant for idempotency; no queue.
  4) Post-verify: forces fresh token, runs canary reads (deviceConfigurations/deviceCompliancePolicies; CA canary only if feature enabled), updates health and warnings (scope-limited, CA disabled, manual assignment required).
- Safety/notes: least-privilege default, idempotent reruns, “already exists” treated as success. If service principal missing, run Admin consent first. Scope-limited setups may yield partial inventory/restore; warnings are surfaced in UI and health panel.

## Graph Contract Registry & Drift Guard

- Registry: `config/graph_contracts.php` defines per-type contracts (resource paths, allowed `$select`/`$expand`, @odata.type family, create/update methods, id field, hydration).
- Client behavior:
  - Sanitizes `$select`/`$expand` to allowed fields; logs warnings on trim.
  - Derived @odata.type values within the family are accepted for preview/restore routing.
  - Capability fallback: on 400s related to select/expand, retries without those clauses and surfaces warnings.
- Drift check: `cd apps/platform && php artisan graph:contract:check [--tenant=]` runs lightweight probes against contract endpoints to detect capability/shape issues; useful in staging/CI (prod optional).
- If Graph returns capability errors, TenantPilot downgrades safely, records warnings/audit entries, and avoids breaking preview/restore flows.

## Policy Settings Display

- Policy detail pages render normalized settings instead of raw JSON:
  - OMA-URI/custom policies → path/value table
  - Settings Catalog → flattened key/value entries
  - Standard objects → labeled key/value view with metadata filtered
- Version detail pages show both pretty-printed JSON and normalized settings.
- Warnings surface malformed snapshots or @odata.type mismatches before restore.

## Policy JSON Viewer (Feature 002)

- **Location**: Policy View pages (`/admin/policies/{record}`)
- **Capability**: Pretty-printed JSON snapshot viewer with copy-to-clipboard
- **Settings Catalog Enhancement**: Dual-view tabs (Settings table + JSON viewer) for Settings Catalog policies
- **Features**:
  - Copy JSON to clipboard with success message
  - Large payload detection (>500 KB) with warning badge and auto-collapse
  - Dark mode support integrated with Filament design system
  - Browser native search (Cmd+F / Ctrl+F) for finding specific keys or values
  - Scrollable container with max height to prevent page overflow
- **Usage**: See `specs/002-filament-json/quickstart.md` for detailed examples and configuration
- **Performance**: Optimized for payloads up to 1 MB; auto-collapse improves initial render for large snapshots
