Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5974540522 | ||
|
|
a991026ad7 |
@@ -20,8 +20,8 @@ import { completeHanzoIdLogin } from '../helpers/app-login'
|
||||
* • studio.hanzo.ai — the shared `hz` shell carries the same dock.
|
||||
* • hanzo.bot — the read-only api.hanzo.ai Developers dock.
|
||||
* • hanzo.team — Chat (Chunter) + Tracker + the seeded AI bots (des/dev/vi).
|
||||
* • tracker.hanzo.ai — the UNIFIED peer host: SSO lands in the SAME workspace
|
||||
* as hanzo.team, same bots (the standalone empty Huly was retired).
|
||||
* • tracker.hanzo.ai — cloud's OWN project boards: the native board + timeline
|
||||
* SPA and its /v1/tracker API, one process, one origin. No Huly workspace.
|
||||
*
|
||||
* Auth is the ONE shared hanzo.id session: sign into the console once, and every
|
||||
* other Hanzo surface in the same context completes SSO silently. Real content is
|
||||
@@ -203,28 +203,41 @@ test.describe('120 — workbench + unified surfaces (live, logged-in)', () => {
|
||||
await page.goto(`${TEAM_URL}/workbench/${ws}/chunter`, { waitUntil: 'domcontentloaded' })
|
||||
await expect(page.locator('text=Channels').first()).toBeVisible({ timeout: 30_000 })
|
||||
await shot(page, 'team-chat')
|
||||
|
||||
// Store the workspace id for the unified-tracker assertion.
|
||||
process.env.__E2E_TEAM_WS = ws
|
||||
})
|
||||
|
||||
test('tracker.hanzo.ai is unified — SSO lands in the SAME workspace + bots', async () => {
|
||||
test('tracker.hanzo.ai serves cloud\'s native board, not a Huly workspace', async () => {
|
||||
// The bare root is the ONLY redirect: 302 to the SPA's base path, the same
|
||||
// shape tasks.hanzo.ai has. Asserted on the raw response, before any hop, so
|
||||
// a broken rule cannot hide behind something else answering 200.
|
||||
const root = await page.request.get('https://tracker.hanzo.ai/', { maxRedirects: 0 })
|
||||
expect(root.status(), 'bare root must send the browser to the SPA').toBe(302)
|
||||
expect(root.headers()['location']).toBe('https://tracker.hanzo.ai/tracker/')
|
||||
|
||||
// The SPA itself is served by cloud, from this host — not fetched from a
|
||||
// second origin. index.html must reference /tracker/assets/, which is what
|
||||
// proves the bundle was built for the prefix this host serves.
|
||||
const spa = await page.request.get('https://tracker.hanzo.ai/tracker/')
|
||||
expect(spa.status(), 'cloud must serve the tracker SPA').toBe(200)
|
||||
const html = await spa.text()
|
||||
expect(html).toContain('Hanzo Tracker')
|
||||
expect(html, 'bundle built for the /tracker/ base').toContain('/tracker/assets/')
|
||||
|
||||
// Its API is the SAME origin — that is the whole design: cloud mints the
|
||||
// validated org from the IAM session before either half runs, so the page
|
||||
// sends no tenancy of its own.
|
||||
const health = await page.request.get('https://tracker.hanzo.ai/v1/tracker/health')
|
||||
expect(health.status(), '/v1/tracker answers on this host').toBe(200)
|
||||
|
||||
// The old Huly data plane is GONE from this host. /v1/team belonged to the
|
||||
// team-go Ingress rule that no longer names tracker.hanzo.ai, so cloud
|
||||
// answers it as an unknown route rather than proxying a second tracker.
|
||||
const team = await page.request.get('https://tracker.hanzo.ai/v1/team/bots', { maxRedirects: 0 })
|
||||
expect(team.status(), 'no Huly team backend on this host').not.toBe(200)
|
||||
|
||||
// And it renders: the board list for the signed-in session's own org.
|
||||
await page.goto('https://tracker.hanzo.ai/', { waitUntil: 'domcontentloaded', timeout: 60_000 })
|
||||
await page.getByText('Continue with Hanzo').click().catch(() => {})
|
||||
await completeHanzoIdLogin(page, TENANT_USER, TENANT_PASSWORD)
|
||||
await page.waitForURL(/tracker\.hanzo\.ai\/workbench\//, { timeout: 45_000 })
|
||||
const ws = page.url().match(/workbench\/([^/]+)/)?.[1] ?? ''
|
||||
|
||||
// SAME workspace id as hanzo.team → proves one shared data plane, not a clone.
|
||||
if (process.env.__E2E_TEAM_WS) expect(ws).toBe(process.env.__E2E_TEAM_WS)
|
||||
|
||||
// Same seeded bots reachable from THIS host (same cloud /v1/team backend).
|
||||
const bots = await page.evaluate(async () => {
|
||||
const r = await fetch('/v1/team/bots', { credentials: 'same-origin' })
|
||||
return r.ok ? await r.json() : null
|
||||
})
|
||||
expect(bots?.bots?.length ?? 0, 'same bots as hanzo.team').toBeGreaterThan(0)
|
||||
await expect(page.locator('text=Tracker').first()).toBeVisible({ timeout: 30_000 })
|
||||
await shot(page, 'tracker-unified')
|
||||
await page.waitForURL(/tracker\.hanzo\.ai\/tracker\//, { timeout: 45_000 })
|
||||
await expect(page.getByRole('heading', { name: /boards/i }).first()).toBeVisible({ timeout: 30_000 })
|
||||
await shot(page, 'tracker-native-boards')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -584,6 +584,15 @@ http:
|
||||
permanent: false
|
||||
regex: ^https?://tasks\.hanzo\.ai/?$
|
||||
replacement: https://tasks.hanzo.ai/tasks/
|
||||
tracker-root-redirect:
|
||||
# tracker.hanzo.ai/ → /tracker/ — the embedded Tracker SPA is served by the
|
||||
# unified cloud binary (apps/tracker/ui) under the base path /tracker/, so
|
||||
# the bare root has no route; send it to the SPA entrypoint (302). Exactly
|
||||
# the shape tasks-root-redirect above has, for exactly the same reason.
|
||||
redirectRegex:
|
||||
permanent: false
|
||||
regex: ^https?://tracker\.hanzo\.ai/?$
|
||||
replacement: https://tracker.hanzo.ai/tracker/
|
||||
favicon-to-hanzo:
|
||||
# The login flow passes through hanzo.id (favicon = a heart) then
|
||||
# iam.hanzo.ai (serves HTML for /favicon.ico), so the browser tab shows
|
||||
@@ -1760,7 +1769,7 @@ http:
|
||||
- websecure
|
||||
middlewares:
|
||||
- team-love-strip
|
||||
rule: (Host(`hanzo.team`) || Host(`api.hanzo.team`) || Host(`team.hanzo.ai`) || Host(`team-v2.hanzo.ai`) || Host(`tracker.hanzo.ai`)) && PathPrefix(`/_love`)
|
||||
rule: (Host(`hanzo.team`) || Host(`api.hanzo.team`) || Host(`team.hanzo.ai`) || Host(`team-v2.hanzo.ai`)) && PathPrefix(`/_love`)
|
||||
service: team-love
|
||||
tls: {}
|
||||
gallery-hanzo-ai:
|
||||
@@ -2863,11 +2872,50 @@ http:
|
||||
rule: Host(`tasks-api.hanzo.ai`)
|
||||
service: tasks-api-hanzo-ai
|
||||
tls:
|
||||
# tracker.hanzo.ai is now a PEER HOST of the unified team workspace, served
|
||||
# by the team-go k8s Ingress (infra/k8s/team-go/ingress.yaml) → the SAME
|
||||
# cloud /v1/team backend + front SPA + bots as hanzo.team. The old
|
||||
# file-provider router + `tracker` service (the empty standalone Huly in ns
|
||||
# `tracker`) are RETIRED — do not re-add them; the standalone had no data.
|
||||
# tracker.hanzo.ai — the org's project boards, served by cloud.
|
||||
#
|
||||
# This host previously served the Huly `front` SPA as a peer of the team
|
||||
# workspace, which made it a SECOND tracker over a second data plane. cloud's
|
||||
# apps/tracker is the one canonical project tracker — org-scoped, IAM-gated,
|
||||
# per-(org,project) — and it now serves BOTH halves of this host from ONE
|
||||
# process: the SPA at /tracker/* and its API at /v1/tracker/*.
|
||||
#
|
||||
# That is why there is no path split here and no second backend: the whole
|
||||
# host goes to cloud, so the page and the data it reads are same-origin. It
|
||||
# has to be. cloud mints the validated org from the IAM session before either
|
||||
# half runs, so the SPA sends no tenancy of its own; splitting the host
|
||||
# across two backends would put the UI on an origin that has to carry a token
|
||||
# and name an org, which is the client-supplied tenancy the tracker refuses.
|
||||
#
|
||||
# Priority 1000 so this beats any leftover routing for the host while the
|
||||
# team-go Ingress rule is being removed (the two land in one commit, but
|
||||
# Traefik loads the file provider and the k8s provider independently).
|
||||
#
|
||||
# The -root router is the bare `/` case only, at a LOWER priority so every
|
||||
# other path falls through to the plain router untouched. Same pair, same
|
||||
# order, as tasks.hanzo.ai.
|
||||
#
|
||||
# Reversible: drop both routers + the tracker-root-redirect middleware and
|
||||
# re-add the host to infra/k8s/team-go/ingress.yaml.
|
||||
tracker-hanzo-ai-root:
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- tracker-root-redirect
|
||||
- hsts-headers
|
||||
priority: 1100
|
||||
rule: Host(`tracker.hanzo.ai`) && Path(`/`)
|
||||
service: cloud-api-hanzo-ai
|
||||
tls:
|
||||
tracker-hanzo-ai:
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- hsts-headers
|
||||
priority: 1000
|
||||
rule: Host(`tracker.hanzo.ai`)
|
||||
service: cloud-api-hanzo-ai
|
||||
tls:
|
||||
services:
|
||||
agency-hanzo-ai:
|
||||
loadBalancer:
|
||||
|
||||
@@ -57,11 +57,6 @@ spec:
|
||||
- api.hanzo.team
|
||||
- team.hanzo.ai
|
||||
- team-v2.hanzo.ai
|
||||
# tracker.hanzo.ai is a PEER host of the ONE unified team workspace — a
|
||||
# focused (Trello-like) tracker door onto the SAME cloud /v1/team backend +
|
||||
# front SPA + bots as hanzo.team. The empty standalone Huly (ns tracker) it
|
||||
# replaced is retired. Native + same-origin (no cross-origin CORS/WS).
|
||||
- tracker.hanzo.ai
|
||||
issuerRef:
|
||||
kind: ClusterIssuer
|
||||
name: letsencrypt-prod-cf
|
||||
@@ -138,7 +133,6 @@ spec:
|
||||
- api.hanzo.team
|
||||
- team.hanzo.ai
|
||||
- team-v2.hanzo.ai
|
||||
- tracker.hanzo.ai
|
||||
secretName: team-tls
|
||||
rules:
|
||||
- host: hanzo.team
|
||||
@@ -361,62 +355,3 @@ spec:
|
||||
name: front-ext
|
||||
port:
|
||||
number: 8080
|
||||
# tracker.hanzo.ai — PEER host of the unified team workspace (same cloud-ext
|
||||
# backend + front-ext SPA + bots as hanzo.team). Same rule set as the aliases
|
||||
# above; retires the empty standalone Huly. A focused tracker door, one data
|
||||
# plane, native + same-origin.
|
||||
- host: tracker.hanzo.ai
|
||||
http:
|
||||
paths:
|
||||
- path: /v1/team
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: cloud-ext
|
||||
port:
|
||||
number: 8000
|
||||
- path: /v1/billing
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: cloud-ext
|
||||
port:
|
||||
number: 8000
|
||||
- path: /v1/usage
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: cloud-ext
|
||||
port:
|
||||
number: 8000
|
||||
- path: /collaborator/rpc
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: cloud-ext
|
||||
port:
|
||||
number: 8000
|
||||
- path: /collaborator
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: cloud-ext
|
||||
port:
|
||||
number: 8000
|
||||
# The virtual office. apps/meet mints LiveKit room tokens in the cloud
|
||||
# binary; without this carve /v1/meet falls to the `/` catch-all below
|
||||
# and the SPA answers index.html — a 200 that is not meet.
|
||||
- path: /v1/meet
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: cloud-ext
|
||||
port:
|
||||
number: 8000
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: front-ext
|
||||
port:
|
||||
number: 8080
|
||||
|
||||
@@ -37,7 +37,7 @@ spec:
|
||||
- key: app
|
||||
operator: In
|
||||
values:
|
||||
- front # the team SPA static server (hanzo.team, team.hanzo.ai, tracker.hanzo.ai)
|
||||
- front # the team SPA static server (hanzo.team, team.hanzo.ai, team-v2.hanzo.ai)
|
||||
ingress:
|
||||
- {} # allow all sources — see the note above on hostNetwork pod identity
|
||||
policyTypes:
|
||||
|
||||
Reference in New Issue
Block a user