From 148f4c8f51a32d1232856b91e7e860c34a7a1e0d Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:19:38 +0800 Subject: [PATCH 1/2] =?UTF-8?q?docs(adr):=20ADR-0075=20=E2=80=94=20configu?= =?UTF-8?q?rable,=20opt-out-able=20post-login=20landing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The console hardcodes the post-login landing (CloudAwareRootRedirect's literal PREFERRED_APPS=['cloud_control'] → /apps/cloud_control, else /home launcher), so a deployment can neither opt out of the outer /home nor customize it without forking the SPA, and app `isDefault` is a badge that doesn't route. Propose resolving the landing from a `console.default_landing` setting reusing ADR-0007's SettingsService order (env override → tenant → user → manifest default), values auto | home | app: | page:; wire isDefault into the `auto` resolver; replace CloudAwareRootRedirect with a LandingResolver and demote cloud to a consumer. Back-compat: default `auto` equals today's multi-app /home. Co-Authored-By: Claude Opus 4.8 --- .../adr/0075-post-login-landing-resolution.md | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 docs/adr/0075-post-login-landing-resolution.md diff --git a/docs/adr/0075-post-login-landing-resolution.md b/docs/adr/0075-post-login-landing-resolution.md new file mode 100644 index 0000000000..f4f110e4a5 --- /dev/null +++ b/docs/adr/0075-post-login-landing-resolution.md @@ -0,0 +1,152 @@ +# ADR-0075: Post-login landing resolution (configurable, opt-out-able home) + +- Status: Proposed +- Date: 2026-06-27 +- Deciders: platform +- Related: ADR-0007 (Settings Manifest + K/V Store + Resolver), ADR-0065 (server-driven UI / pages) + +## Context + +After sign-in the console must land the user *somewhere*. Today that choice is +**hardcoded in the console SPA**: + +- `objectui/apps/console/src/components/CloudAwareRootRedirect.tsx` resolves `/` + with a literal `PREFERRED_APPS = ['cloud_control']`: if the `cloud_control` + app exists (cloud deployments) it redirects to `/apps/cloud_control`, + otherwise it falls back to `/home`. +- `/home` renders `DefaultHomePage` (`@object-ui/app-shell` — + `console/home/HomePage.tsx`): a generic workspace launcher ("Your apps", + "Needs your attention", "Recently visited", "Activity"). +- The app schema's `isDefault` (`spec/src/ui/app.zod.ts`) is **only a badge** — + it does not participate in routing. + +This is a framework smell, not a cloud detail. The framework bakes one +product's policy (`cloud_control`) into the shared console, and offers **no +supported way** for a deployment to either: + +1. **Opt out of the outer `/home` launcher** — e.g. our own cloud service goes + straight to its control plane and never wants the launcher; a single-app + deployment should not show a one-tile launcher; and +2. **Customize the landing** — a customer wants their own first screen (their + own page or app) without forking the console SPA. + +The only lever today is editing `CloudAwareRootRedirect` source. + +## Decision + +Make the post-login landing a **resolved policy, not a hardcoded redirect**, +driven by a setting and reusing the existing Settings infrastructure +(ADR-0007). The console's root route reads a `default_landing` value and routes +accordingly; the literal `cloud_control` preference list is deleted. + +### 1. The setting (`console.default_landing`) + +A new Settings Manifest namespace `console` (ships with the console/app-shell +plugin, per ADR-0007 — manifests are code, values live in `sys_setting`) with +one specifier: + +``` +default_landing (select/text) default: "auto" +``` + +Accepted values: + +| Value | Meaning | +|---------------|---------------------------------------------------------------------| +| `auto` | Smart fallback (see §3). The framework default. | +| `home` | The workspace launcher (`DefaultHomePage`). Today's behavior. | +| `app:` | Land directly in an app (e.g. `app:cloud_control`). | +| `page:` | Land on a standalone SDUI page (ADR-0065) — the customer's own home. | + +**Why this gives "元数据驱动 + 部署默认兜底 for free":** ADR-0007's +`SettingsService.get` resolution order is exactly the precedence we want, with +no new config mechanism: + +``` +1. process.env override (source='env', locked) ← deploy-time force (product, e.g. cloud) +2. sys_setting scope=tenant ← customer customization (runtime, per-tenant) +3. sys_setting scope=user ← optional per-user preference +4. manifest specifier.default ("auto") ← framework default +``` + +- **Cloud** sets the env override (or marks `cloud_control` `isDefault` and uses + `auto`) and **deletes its hardcode** — it becomes a plain consumer. +- **A customer** sets a tenant `sys_setting` to `page:my_home` (or `app:x`) from + the Settings UI — **zero console rebuild**. +- **Opt out of home** = set the landing to anything other than `home`/`auto`; + `/home` stays reachable as an explicit route, it just isn't the landing. + +### 2. Wire `isDefault` into routing + +`isDefault` stops being a pure badge: under `auto`, an app marked +`isDefault: true` becomes the landing. This is the metadata-native lever for +"this deployment has an obvious default app" and removes the need to name +`cloud_control` in the console. + +### 3. The `auto` resolver + +`auto` (the default) preserves today's behavior generically and degrades +sensibly: + +1. exactly one app with `isDefault: true` → `/apps/`; +2. else exactly one visible app (`active !== false && hidden !== true`) → + `/apps/` (a single-app deployment should not show a one-tile launcher); +3. else → `/home` (the multi-app default — unchanged from today). + +### 4. Console change + +`CloudAwareRootRedirect` is replaced by a `LandingResolver` that reads the +resolved `console.default_landing` (exposed on the same bootstrap/metadata +payload the shell already fetches, so no extra round-trip on the hot path) and +renders the corresponding redirect/page. `DefaultHomePage` and the `/home` +route are unchanged and always reachable. + +## Consequences + +- The post-login landing becomes a **first-class, supported capability**: + disable-able and customizable per deployment and per tenant, no source forks. +- The cloud-specific hardcode is deleted; cloud is demoted to a consumer that + declares its landing like anyone else. +- `isDefault` gains real semantics (routing), closing a long-standing + badge-only gap. (Migration note: any deployment that set `isDefault` purely + for the badge now also gets `auto` routing to it — see Migration.) +- The earlier "AI-first welcome page" idea is no longer a special case: it is + simply `default_landing = page:welcome`, where that page hosts the ambient + assistant (`POST /api/v1/ai/assistant/chat`, already mounted at the + control-plane). One mechanism, many landings. + +## Migration / back-compat + +- Default is `auto`, whose multi-app branch is byte-for-byte today's `/home` + behavior → **no change for existing multi-app deployments**. +- Cloud: set the landing to `app:cloud_control` (env override) and delete + `PREFERRED_APPS`. Behavior identical to today for cloud users, minus the + hardcode. +- `isDefault`-as-badge deployments: if exactly one app is `isDefault` they now + land on it under `auto`. Audit existing data; if undesirable, set + `default_landing = home` explicitly (one tenant setting). + +## Alternatives considered + +- **Wire `isDefault` only, no setting.** Covers "default app" but not + `page:` (customer custom home) nor per-tenant override — fails the + "customize" requirement. +- **Deploy-time-only config (app-shell prop / env var).** Simpler boundary but + no runtime, per-tenant customization — a multi-tenant runtime can't give two + tenants different homes without two builds. Rejected in favor of the + settings-driven approach (the env override path still exists for product-level + force). +- **New bespoke `landing` metadata type.** Redundant: ADR-0007 settings already + provide storage + the exact resolution order we want. + +## Implementation sketch (phased) + +1. **framework (spec):** `console` settings manifest with `default_landing`; + document `isDefault` routing semantics on `app.zod.ts`. +2. **objectui (console + app-shell):** `LandingResolver` replacing + `CloudAwareRootRedirect`; read resolved setting from bootstrap; implement the + `auto` resolver; keep `/home` + `DefaultHomePage` intact. +3. **cloud:** set `console.default_landing` (env override) to + `app:cloud_control`; delete the `PREFERRED_APPS` hardcode reliance. +4. **(optional, separate)** ship `page:welcome` (AI-first ambient-assistant + landing) as the cloud landing, proving the `page:` path end-to-end. From 617f3f8ae4e887095ee48395ebac56df891cb14a Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sat, 27 Jun 2026 21:22:57 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs(adr):=20ADR-0075=20=E2=80=94=20landing?= =?UTF-8?q?=20is=20metadata-declared=20at=20dev=20time,=20not=20a=20Settin?= =?UTF-8?q?gs=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: the post-login landing is a product-definition decision the developer/ISV makes in metadata/source at build time, not a runtime per-tenant admin preference. Drop the Settings-Manifest namespace + sys_setting story. Resolve from existing app metadata instead: wire `isDefault` into root routing (land in the default app; its existing `homePageId` selects the page), with a single-app/`/home` fallback and an optional deploy-time env override as a secondary ops escape hatch. Concentrates the change in objectui (one resolver replacing the PREFERRED_APPS hardcode); likely no framework spec change. Co-Authored-By: Claude Opus 4.8 --- .../adr/0075-post-login-landing-resolution.md | 200 ++++++++---------- 1 file changed, 92 insertions(+), 108 deletions(-) diff --git a/docs/adr/0075-post-login-landing-resolution.md b/docs/adr/0075-post-login-landing-resolution.md index f4f110e4a5..96ee72fd0c 100644 --- a/docs/adr/0075-post-login-landing-resolution.md +++ b/docs/adr/0075-post-login-landing-resolution.md @@ -1,9 +1,9 @@ -# ADR-0075: Post-login landing resolution (configurable, opt-out-able home) +# ADR-0075: Post-login landing resolution (metadata-declared, opt-out-able home) - Status: Proposed - Date: 2026-06-27 - Deciders: platform -- Related: ADR-0007 (Settings Manifest + K/V Store + Resolver), ADR-0065 (server-driven UI / pages) +- Related: ADR-0065 (server-driven UI / pages), app `isDefault` + `homePageId` (spec/src/ui/app.zod.ts) ## Context @@ -17,136 +17,120 @@ After sign-in the console must land the user *somewhere*. Today that choice is - `/home` renders `DefaultHomePage` (`@object-ui/app-shell` — `console/home/HomePage.tsx`): a generic workspace launcher ("Your apps", "Needs your attention", "Recently visited", "Activity"). -- The app schema's `isDefault` (`spec/src/ui/app.zod.ts`) is **only a badge** — - it does not participate in routing. - -This is a framework smell, not a cloud detail. The framework bakes one -product's policy (`cloud_control`) into the shared console, and offers **no -supported way** for a deployment to either: - -1. **Opt out of the outer `/home` launcher** — e.g. our own cloud service goes - straight to its control plane and never wants the launcher; a single-app - deployment should not show a one-tile launcher; and -2. **Customize the landing** — a customer wants their own first screen (their - own page or app) without forking the console SPA. - -The only lever today is editing `CloudAwareRootRedirect` source. +- The app schema's `isDefault` (`spec/src/ui/app.zod.ts:502`) is **only a + badge** — it does not participate in routing. Apps already carry + `homePageId` (`app.zod.ts:562`, "the navigation item to serve as landing + page") to pick the landing *within* an app. + +This is a framework smell, not a cloud detail: the framework bakes one product's +policy (`cloud_control`) into the shared console, and offers **no supported +way** for a deployment to either opt out of the outer `/home` launcher or land +somewhere custom — short of forking the SPA. + +### Who actually decides this, and when + +The post-login landing is a **product-definition decision made at build / +development time** by whoever builds a product on the framework (the platform +for cloud; an ISV/customer for their own product), declared in **metadata / +source**. It is **not** a runtime, per-tenant, end-admin preference toggled in a +Settings UI. (An earlier draft of this ADR hung it on the Settings Manifest / +`sys_setting` runtime store; that is rejected below — it adds a UI surface and a +per-tenant runtime story nobody needs.) ## Decision -Make the post-login landing a **resolved policy, not a hardcoded redirect**, -driven by a setting and reusing the existing Settings infrastructure -(ADR-0007). The console's root route reads a `default_landing` value and routes -accordingly; the literal `cloud_control` preference list is deleted. - -### 1. The setting (`console.default_landing`) - -A new Settings Manifest namespace `console` (ships with the console/app-shell -plugin, per ADR-0007 — manifests are code, values live in `sys_setting`) with -one specifier: - -``` -default_landing (select/text) default: "auto" -``` - -Accepted values: - -| Value | Meaning | -|---------------|---------------------------------------------------------------------| -| `auto` | Smart fallback (see §3). The framework default. | -| `home` | The workspace launcher (`DefaultHomePage`). Today's behavior. | -| `app:` | Land directly in an app (e.g. `app:cloud_control`). | -| `page:` | Land on a standalone SDUI page (ADR-0065) — the customer's own home. | - -**Why this gives "元数据驱动 + 部署默认兜底 for free":** ADR-0007's -`SettingsService.get` resolution order is exactly the precedence we want, with -no new config mechanism: +Resolve the landing from **existing app metadata**, declared in source by the +product developer. No new settings UI, no per-tenant runtime store. -``` -1. process.env override (source='env', locked) ← deploy-time force (product, e.g. cloud) -2. sys_setting scope=tenant ← customer customization (runtime, per-tenant) -3. sys_setting scope=user ← optional per-user preference -4. manifest specifier.default ("auto") ← framework default -``` +### 1. Wire `isDefault` into root routing (the one behavioral change) -- **Cloud** sets the env override (or marks `cloud_control` `isDefault` and uses - `auto`) and **deletes its hardcode** — it becomes a plain consumer. -- **A customer** sets a tenant `sys_setting` to `page:my_home` (or `app:x`) from - the Settings UI — **zero console rebuild**. -- **Opt out of home** = set the landing to anything other than `home`/`auto`; - `/home` stays reachable as an explicit route, it just isn't the landing. +`isDefault` stops being a pure badge. The root route lands in the app marked +`isDefault: true`; that app's existing `homePageId` then selects the landing +*page* inside it. This is the single new semantic. -### 2. Wire `isDefault` into routing +- **Cloud**: `cloud_control` is already `isDefault: true` → root lands there, + and its `homePageId` serves the welcome / AI page. The `PREFERRED_APPS` + hardcode is deleted; cloud becomes a plain consumer. +- **An ISV/customer product**: mark your app `isDefault` and set its + `homePageId` to your custom landing page (an ADR-0065 SDUI page). "Custom + home" = an app + its `homePageId` — no separate top-level page-landing concept + and no settings namespace required. -`isDefault` stops being a pure badge: under `auto`, an app marked -`isDefault: true` becomes the landing. This is the metadata-native lever for -"this deployment has an obvious default app" and removes the need to name -`cloud_control` in the console. - -### 3. The `auto` resolver - -`auto` (the default) preserves today's behavior generically and degrades -sensibly: +### 2. Fallback resolver (no app declares `isDefault`) 1. exactly one app with `isDefault: true` → `/apps/`; 2. else exactly one visible app (`active !== false && hidden !== true`) → `/apps/` (a single-app deployment should not show a one-tile launcher); -3. else → `/home` (the multi-app default — unchanged from today). +3. else → `/home` (today's multi-app default, unchanged). + +### 3. Ops escape hatch (optional, secondary) + +A deploy-time env override (e.g. `OS_CONSOLE_DEFAULT_LANDING=app:` / +`home`) for the rare case where ops must force a landing without touching +product metadata. This is a secondary path; **metadata is the source of truth**. ### 4. Console change -`CloudAwareRootRedirect` is replaced by a `LandingResolver` that reads the -resolved `console.default_landing` (exposed on the same bootstrap/metadata -payload the shell already fetches, so no extra round-trip on the hot path) and -renders the corresponding redirect/page. `DefaultHomePage` and the `/home` -route are unchanged and always reachable. +`CloudAwareRootRedirect` is replaced by a generic `LandingResolver` implementing +§1–§3 over the metadata the shell already fetches (no extra hot-path round-trip). +`DefaultHomePage` and the `/home` route are unchanged and always reachable. + +## Explicitly NOT doing + +- **No `console.default_landing` Settings-Manifest namespace, no per-tenant + `sys_setting`, no Settings UI.** The landing is a product-definition concern + fixed at dev time, not an admin preference. +- **No "different landing per tenant at runtime."** Out of scope — a product's + landing is declared once in its metadata. (A multi-tenant *host* like cloud + still has exactly one landing: its control plane.) ## Consequences -- The post-login landing becomes a **first-class, supported capability**: - disable-able and customizable per deployment and per tenant, no source forks. +- Minimal change, reusing fields that already exist (`isDefault` + + `homePageId`): the only real work is one resolver in objectui replacing a + hardcode. - The cloud-specific hardcode is deleted; cloud is demoted to a consumer that - declares its landing like anyone else. -- `isDefault` gains real semantics (routing), closing a long-standing - badge-only gap. (Migration note: any deployment that set `isDefault` purely - for the badge now also gets `auto` routing to it — see Migration.) -- The earlier "AI-first welcome page" idea is no longer a special case: it is - simply `default_landing = page:welcome`, where that page hosts the ambient - assistant (`POST /api/v1/ai/assistant/chat`, already mounted at the - control-plane). One mechanism, many landings. + declares its landing like anyone else (and already does, via `isDefault`). +- `isDefault` gains real semantics (routing), closing a badge-only gap. +- The earlier "AI-first welcome page" idea is just `cloud_control`'s + `homePageId` pointing at a page that hosts the ambient assistant + (`POST /api/v1/ai/assistant/chat`, already mounted control-plane-side). One + mechanism, declared in metadata. +- Likely **no framework spec change** beyond documenting `isDefault`'s routing + semantics — the change is concentrated in objectui. ## Migration / back-compat -- Default is `auto`, whose multi-app branch is byte-for-byte today's `/home` - behavior → **no change for existing multi-app deployments**. -- Cloud: set the landing to `app:cloud_control` (env override) and delete - `PREFERRED_APPS`. Behavior identical to today for cloud users, minus the - hardcode. -- `isDefault`-as-badge deployments: if exactly one app is `isDefault` they now - land on it under `auto`. Audit existing data; if undesirable, set - `default_landing = home` explicitly (one tenant setting). +- No app declares `isDefault` → `auto` fallback's multi-app branch is + byte-for-byte today's `/home` behavior → **no change for existing multi-app + deployments**. +- Deployments that set `isDefault` purely for the badge and have exactly one + such app now **land on it**. Audit existing metadata; if undesired, unset + `isDefault` (or use the env override to force `home`). +- Cloud: behavior identical to today (still lands on `cloud_control`), minus the + `PREFERRED_APPS` hardcode. ## Alternatives considered -- **Wire `isDefault` only, no setting.** Covers "default app" but not - `page:` (customer custom home) nor per-tenant override — fails the - "customize" requirement. -- **Deploy-time-only config (app-shell prop / env var).** Simpler boundary but - no runtime, per-tenant customization — a multi-tenant runtime can't give two - tenants different homes without two builds. Rejected in favor of the - settings-driven approach (the env override path still exists for product-level - force). -- **New bespoke `landing` metadata type.** Redundant: ADR-0007 settings already - provide storage + the exact resolution order we want. +- **Settings-Manifest UI namespace + per-tenant `sys_setting` (this ADR's first + draft).** Rejected: the landing is a dev-time product decision, not a runtime + admin preference; it would add a Settings surface and a per-tenant runtime + story that isn't a real requirement. +- **Deploy-time env var as the primary mechanism.** Kept only as a secondary ops + override; metadata declared in product source is the source of truth, so that + a product's landing travels with the product, not its deployment config. +- **A new bespoke top-level `landing` / `page:` metadata.** Redundant: + `isDefault` (which app) + `homePageId` (which page in it) already express it. ## Implementation sketch (phased) -1. **framework (spec):** `console` settings manifest with `default_landing`; - document `isDefault` routing semantics on `app.zod.ts`. -2. **objectui (console + app-shell):** `LandingResolver` replacing - `CloudAwareRootRedirect`; read resolved setting from bootstrap; implement the - `auto` resolver; keep `/home` + `DefaultHomePage` intact. -3. **cloud:** set `console.default_landing` (env override) to - `app:cloud_control`; delete the `PREFERRED_APPS` hardcode reliance. -4. **(optional, separate)** ship `page:welcome` (AI-first ambient-assistant - landing) as the cloud landing, proving the `page:` path end-to-end. +1. **objectui (console + app-shell):** `LandingResolver` replacing + `CloudAwareRootRedirect` — implement §1 (isDefault → app → homePageId), §2 + (fallback), §3 (optional env override); keep `/home` + `DefaultHomePage` + intact. +2. **framework (spec):** document `isDefault`'s routing semantics on + `app.zod.ts` (no new field expected). +3. **cloud:** delete the `PREFERRED_APPS` hardcode reliance; `cloud_control` is + already `isDefault` so behavior is preserved. +4. **(optional, separate)** point `cloud_control.homePageId` at an AI-first + ambient-assistant page, proving the custom-landing path end-to-end.