Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0abd3e3352 | ||
|
|
c526a7af39 | ||
|
|
67e844ec84 |
@@ -40,11 +40,6 @@ BETTER_AUTH_SECRET=change-me-to-a-random-32-char-string
|
|||||||
BETTER_AUTH_URL=http://localhost:14242
|
BETTER_AUTH_URL=http://localhost:14242
|
||||||
|
|
||||||
|
|
||||||
# ─── Web App (Next.js) ───────────────────────────────────────────────────────
|
|
||||||
# Public gateway URL — accessible from the browser, not just the server.
|
|
||||||
NEXT_PUBLIC_GATEWAY_URL=http://localhost:14242
|
|
||||||
|
|
||||||
|
|
||||||
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
|
# ─── OpenTelemetry ───────────────────────────────────────────────────────────
|
||||||
# OTLP HTTP endpoint (otel-collector or any OpenTelemetry-compatible backend)
|
# OTLP HTTP endpoint (otel-collector or any OpenTelemetry-compatible backend)
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
||||||
|
|||||||
+14
-27
@@ -32,11 +32,6 @@ variables:
|
|||||||
# non-excluded change still builds, so no transitive dep can silently go stale.
|
# non-excluded change still builds, so no transitive dep can silently go stale.
|
||||||
# (Woodpecker: `when` entries are OR'd; `path` applies to push/PR only — hence
|
# (Woodpecker: `when` entries are OR'd; `path` applies to push/PR only — hence
|
||||||
# the separate `event: tag` entry.)
|
# the separate `event: tag` entry.)
|
||||||
# #1407: ONE shared anchor for all three image steps. A second main-only
|
|
||||||
# anchor previously gated build-web/build-appservice, so next-lane pushes
|
|
||||||
# published gateway sha images with no web/appservice counterpart — no
|
|
||||||
# sha-parity set existed for next-lane containerized deploys. Every image
|
|
||||||
# step now builds on next too (sha-only destinations, enforced per step).
|
|
||||||
- &image_build_when
|
- &image_build_when
|
||||||
- event: tag
|
- event: tag
|
||||||
- event: [push, manual]
|
- event: [push, manual]
|
||||||
@@ -49,6 +44,16 @@ variables:
|
|||||||
- '.woodpecker/**'
|
- '.woodpecker/**'
|
||||||
- event: [push, manual]
|
- event: [push, manual]
|
||||||
branch: next
|
branch: next
|
||||||
|
- &main_image_build_when
|
||||||
|
- event: tag
|
||||||
|
- event: [push, manual]
|
||||||
|
branch: main
|
||||||
|
path:
|
||||||
|
exclude:
|
||||||
|
- 'packages/mosaic/**'
|
||||||
|
- 'docs/**'
|
||||||
|
- '**/*.md'
|
||||||
|
- '.woodpecker/**'
|
||||||
|
|
||||||
when:
|
when:
|
||||||
- branch: [main, next]
|
- branch: [main, next]
|
||||||
@@ -469,7 +474,7 @@ steps:
|
|||||||
|
|
||||||
build-appservice:
|
build-appservice:
|
||||||
image: gcr.io/kaniko-project/executor:debug
|
image: gcr.io/kaniko-project/executor:debug
|
||||||
when: *image_build_when
|
when: *main_image_build_when
|
||||||
environment:
|
environment:
|
||||||
REGISTRY_USER:
|
REGISTRY_USER:
|
||||||
from_secret: REGISTRY_USERNAME
|
from_secret: REGISTRY_USERNAME
|
||||||
@@ -483,17 +488,8 @@ steps:
|
|||||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||||
- |
|
- |
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/appservice:sha-${CI_COMMIT_SHA:0:7}"
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/appservice:sha-${CI_COMMIT_SHA:0:7}"
|
||||||
if [ "$CI_COMMIT_BRANCH" = "next" ]; then
|
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
||||||
echo "[publish] FATAL: next appservice publish must be sha-only; refusing tag '$CI_COMMIT_TAG'" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "[publish] next appservice publish is sha-only"
|
|
||||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
|
||||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:latest"
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:latest"
|
||||||
elif [ -z "$CI_COMMIT_TAG" ]; then
|
|
||||||
echo "[publish] FATAL: appservice image publish may only run for main, next, or tag events" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:$CI_COMMIT_TAG"
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/appservice:$CI_COMMIT_TAG"
|
||||||
@@ -513,7 +509,7 @@ steps:
|
|||||||
|
|
||||||
build-web:
|
build-web:
|
||||||
image: gcr.io/kaniko-project/executor:debug
|
image: gcr.io/kaniko-project/executor:debug
|
||||||
when: *image_build_when
|
when: *main_image_build_when
|
||||||
environment:
|
environment:
|
||||||
REGISTRY_USER:
|
REGISTRY_USER:
|
||||||
from_secret: REGISTRY_USERNAME
|
from_secret: REGISTRY_USERNAME
|
||||||
@@ -527,17 +523,8 @@ steps:
|
|||||||
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
- echo "{\"auths\":{\"git.mosaicstack.dev\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASS\"}}}" > /kaniko/.docker/config.json
|
||||||
- |
|
- |
|
||||||
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/web:sha-${CI_COMMIT_SHA:0:7}"
|
DESTINATIONS="--destination git.mosaicstack.dev/mosaicstack/stack/web:sha-${CI_COMMIT_SHA:0:7}"
|
||||||
if [ "$CI_COMMIT_BRANCH" = "next" ]; then
|
if [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
|
||||||
echo "[publish] FATAL: next web publish must be sha-only; refusing tag '$CI_COMMIT_TAG'" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "[publish] next web publish is sha-only"
|
|
||||||
elif [ "$CI_COMMIT_BRANCH" = "main" ]; then
|
|
||||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:latest"
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:latest"
|
||||||
elif [ -z "$CI_COMMIT_TAG" ]; then
|
|
||||||
echo "[publish] FATAL: web image publish may only run for main, next, or tag events" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
if [ -n "$CI_COMMIT_TAG" ]; then
|
if [ -n "$CI_COMMIT_TAG" ]; then
|
||||||
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:$CI_COMMIT_TAG"
|
DESTINATIONS="$DESTINATIONS --destination git.mosaicstack.dev/mosaicstack/stack/web:$CI_COMMIT_TAG"
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
import 'reflect-metadata';
|
|
||||||
import { type CanActivate, type ExecutionContext, type INestApplication } from '@nestjs/common';
|
|
||||||
import { FastifyAdapter, type NestFastifyApplication } from '@nestjs/platform-fastify';
|
|
||||||
import { Test } from '@nestjs/testing';
|
|
||||||
import request from 'supertest';
|
|
||||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
|
||||||
import { TeamsController } from './teams.controller.js';
|
|
||||||
import { TeamsService } from './teams.service.js';
|
|
||||||
|
|
||||||
const teamAlpha = { id: 'team-alpha', name: 'Alpha' };
|
|
||||||
const teamBeta = { id: 'team-beta', name: 'Beta' };
|
|
||||||
|
|
||||||
// user-1 is a member of team-alpha only; admin-1 has role admin.
|
|
||||||
let currentUser: { id: string; role?: string } = { id: 'user-1' };
|
|
||||||
|
|
||||||
const teamsServiceMock = {
|
|
||||||
findAll: vi.fn(() => Promise.resolve([teamAlpha, teamBeta])),
|
|
||||||
findAllForUser: vi.fn((userId: string) =>
|
|
||||||
Promise.resolve(userId === 'user-1' ? [teamAlpha] : []),
|
|
||||||
),
|
|
||||||
findById: vi.fn((id: string) => Promise.resolve([teamAlpha, teamBeta].find((t) => t.id === id))),
|
|
||||||
listMembers: vi.fn(() => Promise.resolve([{ teamId: 'team-alpha', userId: 'user-1' }])),
|
|
||||||
isMember: vi.fn((teamId: string, userId: string) =>
|
|
||||||
Promise.resolve(teamId === 'team-alpha' && userId === 'user-1'),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const authGuard: CanActivate = {
|
|
||||||
canActivate(context: ExecutionContext): boolean {
|
|
||||||
const requestContext = context
|
|
||||||
.switchToHttp()
|
|
||||||
.getRequest<{ user?: { id: string; role?: string } }>();
|
|
||||||
requestContext.user = currentUser;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('teams endpoints are scoped to membership', () => {
|
|
||||||
let app: INestApplication;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
const moduleRef = await Test.createTestingModule({
|
|
||||||
controllers: [TeamsController],
|
|
||||||
providers: [{ provide: TeamsService, useValue: teamsServiceMock }],
|
|
||||||
})
|
|
||||||
.overrideGuard(AuthGuard)
|
|
||||||
.useValue(authGuard)
|
|
||||||
.compile();
|
|
||||||
|
|
||||||
app = moduleRef.createNestApplication<NestFastifyApplication>(new FastifyAdapter());
|
|
||||||
await app.init();
|
|
||||||
await app.getHttpAdapter().getInstance().ready();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
currentUser = { id: 'user-1' };
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await app.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('GET /api/teams returns only the teams the user belongs to', async () => {
|
|
||||||
const response = await request(app.getHttpServer()).get('/api/teams');
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual([teamAlpha]);
|
|
||||||
expect(teamsServiceMock.findAll).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('GET /api/teams returns every team for an admin', async () => {
|
|
||||||
currentUser = { id: 'admin-1', role: 'admin' };
|
|
||||||
const response = await request(app.getHttpServer()).get('/api/teams');
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual([teamAlpha, teamBeta]);
|
|
||||||
expect(teamsServiceMock.findAllForUser).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('GET /api/teams/:teamId returns 403 for a non-member', async () => {
|
|
||||||
const response = await request(app.getHttpServer()).get('/api/teams/team-beta');
|
|
||||||
expect(response.status).toBe(403);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('GET /api/teams/:teamId returns 404 for a missing team', async () => {
|
|
||||||
const response = await request(app.getHttpServer()).get('/api/teams/team-missing');
|
|
||||||
expect(response.status).toBe(404);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('GET /api/teams/:teamId returns the team for a member', async () => {
|
|
||||||
const response = await request(app.getHttpServer()).get('/api/teams/team-alpha');
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual(teamAlpha);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('GET /api/teams/:teamId/members returns 403 for a non-member and members for a member', async () => {
|
|
||||||
const denied = await request(app.getHttpServer()).get('/api/teams/team-beta/members');
|
|
||||||
expect(denied.status).toBe(403);
|
|
||||||
expect(teamsServiceMock.listMembers).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
const allowed = await request(app.getHttpServer()).get('/api/teams/team-alpha/members');
|
|
||||||
expect(allowed.status).toBe(200);
|
|
||||||
expect(allowed.body).toEqual([{ teamId: 'team-alpha', userId: 'user-1' }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('GET /api/teams/:teamId/members/:userId allows a self-lookup on any team', async () => {
|
|
||||||
const response = await request(app.getHttpServer()).get('/api/teams/team-beta/members/user-1');
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual({ isMember: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('GET /api/teams/:teamId/members/:userId denies looking up another user on a foreign team', async () => {
|
|
||||||
const response = await request(app.getHttpServer()).get('/api/teams/team-beta/members/user-2');
|
|
||||||
expect(response.status).toBe(403);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('an admin can look up any membership', async () => {
|
|
||||||
currentUser = { id: 'admin-1', role: 'admin' };
|
|
||||||
const response = await request(app.getHttpServer()).get('/api/teams/team-alpha/members/user-1');
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual({ isMember: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,68 +1,30 @@
|
|||||||
import {
|
import { Controller, Get, Param, UseGuards } from '@nestjs/common';
|
||||||
Controller,
|
|
||||||
ForbiddenException,
|
|
||||||
Get,
|
|
||||||
NotFoundException,
|
|
||||||
Param,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { AuthGuard } from '../auth/auth.guard.js';
|
import { AuthGuard } from '../auth/auth.guard.js';
|
||||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
|
||||||
import { TeamsService } from './teams.service.js';
|
import { TeamsService } from './teams.service.js';
|
||||||
|
|
||||||
type RequestUser = { id: string; role?: string };
|
|
||||||
|
|
||||||
@Controller('api/teams')
|
@Controller('api/teams')
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
export class TeamsController {
|
export class TeamsController {
|
||||||
constructor(private readonly teams: TeamsService) {}
|
constructor(private readonly teams: TeamsService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async list(@CurrentUser() user: RequestUser) {
|
async list() {
|
||||||
if (user.role === 'admin') {
|
return this.teams.findAll();
|
||||||
return this.teams.findAll();
|
|
||||||
}
|
|
||||||
return this.teams.findAllForUser(user.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':teamId')
|
@Get(':teamId')
|
||||||
async findOne(@Param('teamId') teamId: string, @CurrentUser() user: RequestUser) {
|
async findOne(@Param('teamId') teamId: string) {
|
||||||
return this.getAccessibleTeam(teamId, user);
|
return this.teams.findById(teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':teamId/members')
|
@Get(':teamId/members')
|
||||||
async listMembers(@Param('teamId') teamId: string, @CurrentUser() user: RequestUser) {
|
async listMembers(@Param('teamId') teamId: string) {
|
||||||
await this.getAccessibleTeam(teamId, user);
|
|
||||||
return this.teams.listMembers(teamId);
|
return this.teams.listMembers(teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':teamId/members/:userId')
|
@Get(':teamId/members/:userId')
|
||||||
async checkMembership(
|
async checkMembership(@Param('teamId') teamId: string, @Param('userId') userId: string) {
|
||||||
@Param('teamId') teamId: string,
|
|
||||||
@Param('userId') userId: string,
|
|
||||||
@CurrentUser() user: RequestUser,
|
|
||||||
) {
|
|
||||||
// A user may always ask about their own membership; anything else is
|
|
||||||
// team-scoped like the other routes.
|
|
||||||
if (userId !== user.id) {
|
|
||||||
await this.getAccessibleTeam(teamId, user);
|
|
||||||
}
|
|
||||||
const isMember = await this.teams.isMember(teamId, userId);
|
const isMember = await this.teams.isMember(teamId, userId);
|
||||||
return { isMember };
|
return { isMember };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Team-scoped access: admins see any team; everyone else only teams they
|
|
||||||
* are a member of. NotFoundException when the team does not exist and
|
|
||||||
* ForbiddenException when the user lacks access (same convention as the
|
|
||||||
* projects controller).
|
|
||||||
*/
|
|
||||||
private async getAccessibleTeam(teamId: string, user: RequestUser) {
|
|
||||||
const team = await this.teams.findById(teamId);
|
|
||||||
if (!team) throw new NotFoundException('Team not found');
|
|
||||||
if (user.role === 'admin') return team;
|
|
||||||
const isMember = await this.teams.isMember(teamId, user.id);
|
|
||||||
if (!isMember) throw new ForbiddenException('Not a member of this team');
|
|
||||||
return team;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { eq, and, inArray, type Db, teams, teamMembers, projects } from '@mosaicstack/db';
|
import { eq, and, type Db, teams, teamMembers, projects } from '@mosaicstack/db';
|
||||||
import { DB } from '../database/database.module.js';
|
import { DB } from '../database/database.module.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -56,21 +56,6 @@ export class TeamsService {
|
|||||||
return this.db.select().from(teams);
|
return this.db.select().from(teams);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* List only the teams the user is a member of.
|
|
||||||
*/
|
|
||||||
async findAllForUser(userId: string) {
|
|
||||||
const memberRows = await this.db
|
|
||||||
.select({ teamId: teamMembers.teamId })
|
|
||||||
.from(teamMembers)
|
|
||||||
.where(eq(teamMembers.userId, userId));
|
|
||||||
|
|
||||||
const teamIds = memberRows.map((r) => r.teamId);
|
|
||||||
if (teamIds.length === 0) return [];
|
|
||||||
|
|
||||||
return this.db.select().from(teams).where(inArray(teams.id, teamIds));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a team by ID.
|
* Find a team by ID.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -376,9 +376,8 @@ Session cleanup is scoped to one session identifier and only removes that sessio
|
|||||||
|
|
||||||
### Web App
|
### Web App
|
||||||
|
|
||||||
| Variable | Default | Description |
|
The web app uses origin-relative paths only; it needs no gateway URL variable.
|
||||||
| ------------------------- | ------------------------ | -------------------------------------- |
|
(`NEXT_PUBLIC_GATEWAY_URL` is obsolete and no longer read by anything.)
|
||||||
| `NEXT_PUBLIC_GATEWAY_URL` | `http://localhost:14242` | Gateway URL used by the Next.js client |
|
|
||||||
|
|
||||||
### Coordination
|
### Coordination
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
# WebUI Phase P — File / Folder Structure & Migration Map
|
# WebUI Phase P — File / Folder Structure & Migration Map
|
||||||
|
|
||||||
> **Status:** living document — first pass. Structure and increment status are verified against
|
> **Status:** living document. Structure and increment status are verified against `next` as of
|
||||||
> `next` as of merge `8c27024d`. Details (per-surface component inventories, exact route tables,
|
> merge `3bd490c0` (2026-08-26). Details (per-surface component inventories, exact route tables,
|
||||||
> test matrices) are still being fleshed out; extend the stub sections below rather than rewriting
|
> test matrices) are still being fleshed out; extend the stub sections below rather than rewriting
|
||||||
> the verified structure.
|
> the verified structure.
|
||||||
|
|
||||||
@@ -29,7 +29,8 @@ apps/web/
|
|||||||
├── routes.tsx # ── SPA React Router route table
|
├── routes.tsx # ── SPA React Router route table
|
||||||
├── spa/ # ── NEW SPA surfaces
|
├── spa/ # ── NEW SPA surfaces
|
||||||
│ ├── guards.tsx # guest / authenticated route guards
|
│ ├── guards.tsx # guest / authenticated route guards
|
||||||
│ ├── pages/ # login, register, sso-callback (P2); chat + error boundary (P3)
|
│ ├── pages/ # login, register, sso-callback (P2); chat + error boundary (P3);
|
||||||
|
│ │ # projects, project-detail, tasks (P4-1)
|
||||||
│ └── chat/ # P3 typed chat: use-chat-connection, commands-panel,
|
│ └── chat/ # P3 typed chat: use-chat-connection, commands-panel,
|
||||||
│ # session-panel, message-transcript, tool-call-list, composer
|
│ # session-panel, message-transcript, tool-call-list, composer
|
||||||
│
|
│
|
||||||
@@ -80,14 +81,14 @@ build path is retired.
|
|||||||
|
|
||||||
## 5. Increment map (P1–P6)
|
## 5. Increment map (P1–P6)
|
||||||
|
|
||||||
| # | Increment | Branch | Status |
|
| # | Increment | Branch | Status |
|
||||||
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ------------------------- |
|
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||||
| **P1** | Vite + React Router skeleton beside Next (entry, router, guards, vitest) | `feat/webui-p1-vite-skeleton` | ✅ merged — PR **#1143** |
|
| **P1** | Vite + React Router skeleton beside Next (entry, router, guards, vitest) | `feat/webui-p1-vite-skeleton` | ✅ merged — PR **#1143** |
|
||||||
| **P2** | SPA data layer + same-origin auth (login/register/SSO pages, guards, relative api/socket/auth-client) | `feat/webui-p2-data-auth` | ✅ merged — PR **#1144** |
|
| **P2** | SPA data layer + same-origin auth (login/register/SSO pages, guards, relative api/socket/auth-client) | `feat/webui-p2-data-auth` | ✅ merged — PR **#1144** |
|
||||||
| **P3** | Typed SPA **chat** (`spa/chat/*`, `chat-contract.ts`, chat page + error boundary) | `feat/webui-p3-chat` | 🚧 in progress (unmerged) |
|
| **P3** | Typed SPA **chat** (`spa/chat/*`, `chat-contract.ts`, chat page + error boundary) | `feat/webui-p3-chat` | ✅ merged — PR **#1151** (+ repair PR **#1154**) |
|
||||||
| **P4** | Port **projects / tasks / settings / admin** dashboard surfaces into the SPA | _tbd_ | ⏳ not started |
|
| **P4** | Port **projects / tasks / settings / admin** dashboard surfaces into the SPA | `feat/webui-p4-1` | 🚧 in progress — P4-1 (read-only projects + tasks) merged, PR **#1153**; settings/admin remain |
|
||||||
| **P5** | **Cutover**: Gateway serves the Vite `dist` on `:14242`; flip `dev`/`build` to vite; **remove** the legacy Next `app/` tree + `next.config.ts` | _tbd_ | ⏳ not started |
|
| **P5** | **Cutover**: Gateway serves the Vite `dist` on `:14242`; flip `dev`/`build` to vite; **remove** the legacy Next `app/` tree + `next.config.ts` | _tbd_ | ⏳ not started |
|
||||||
| **P6** | CI / images (trails): build the SPA in CI, ship images | _tbd_ | ⏳ trails |
|
| **P6** | CI / images (trails): build the SPA in CI, ship images | _tbd_ | ⏳ trails |
|
||||||
|
|
||||||
Each increment follows the same delivery pipeline: brief traceable to the RFC → author →
|
Each increment follows the same delivery pipeline: brief traceable to the RFC → author →
|
||||||
**independent** integrator verification (build+test+typecheck+lint) → **independent** code + security
|
**independent** integrator verification (build+test+typecheck+lint) → **independent** code + security
|
||||||
|
|||||||
Reference in New Issue
Block a user