Commit Graph

9 Commits

Author SHA1 Message Date
9236cb73c6 feat(mcp): MCP server for AI clients, consuming the REST API
New app apps/mcp (NestJS + @nestjs-mcp/server 2): Streamable HTTP at
POST /mcp, stateless.

- 24 tools: projects (mine, public, details with settings, board, stats,
  analytics, activity), tickets (my work, search, details with comments and
  history, create, update, move, assign, release, comment, link), releases
  (list, upcoming, details with progress and burnup, create, publish) and
  users (whoami, search). Admin operations stay in the web.
- No credentials of its own: each call's Authorization header (a Pireon
  token) is forwarded to the API; without it only public projects are read.
- Typed REST client generated from apps/api/openapi.json (openapi-fetch +
  openapi-typescript via `nx run mcp:openapi-types`); API refusals become
  readable tool errors; parameters validated with zod.
- Tests: the real MCP against a simulated API (token forwarding, errors,
  validation, writes).
- Docker target `mcp` (Node 24) and an optional compose service under the
  `mcp` profile; zod declared as a root dependency.
- Docs: chapter 9 «Servidor MCP», references in 01/06/07 and the index;
  Word/PDF regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:59:38 +02:00
71dc2d6f2d fix(api): typed OpenAPI query params and not-found messages that hide private projects
- Query DTO properties with a default value (limit, offset, days, perColumn,
  assignedToMe, search, archived, requiresReason, force) were documented as
  `object` in openapi.json, which broke generated clients. They now declare
  their type explicitly.
- Reading a private project, ticket, release or its comments without access
  answered "Project <internal id> not found", confirming the project exists.
  ReadAccess now reports what the caller asked for (e2e covered).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:59:38 +02:00
e656222b54 feat: public projects, project deletion and sagas
Public projects:
- Project.isPublic (create/update, owners and admins). Anyone can read a
  public project without a token: project, board, tickets, comments,
  history, releases, activity, tracking and statistics, over GraphQL and
  REST. Writes stay for members (401 anonymous, 403 outsiders).
- @OptionalAuth() / @ApiOptionalPireonAuth(): anonymous callers allowed,
  invalid tokens still rejected. ReadAccess lets anonymous viewers read
  public projects only; linked tickets are filtered by visibility.
- publicProjects (GraphQL) and GET /api/explore/projects (REST).
- Web: «Explorar» page, project pages open without a session (read-only
  notice, sign-in hints), public badge, visibility switch in settings,
  no realtime subscriptions without a session.

Project deletion:
- Owner only, confirming the key (deleteProject / DELETE
  /api/projects/{project}?confirm=KEY). The key is freed; the read model
  drops the project and members are notified (web goes home).

Sagas (run by the EventsBridge after projections, durable and ordered):
- ProjectDeletionSaga: project deleted → delete its tickets and releases;
  ticket deleted → purge its comments and drop links from other tickets.
- ReleaseArchivedSaga: unfinished tickets leave an archived release.
- UserDeactivatedSaga: the Pireon webhook records UserDeactivatedEvent in
  the event store (EventRecorder); the tracker unassigns that user's open
  tickets as the system actor.
- Internal idempotent commands; domain refusals are logged and skipped,
  concurrency conflicts retried. Affected streams come from the event store
  (TrackerStreamsRepository).

Tests: domain lifecycle specs, saga specs run like the bridge, facade
tests, e2e for public access, cascade deletion, archived releases and
deactivation webhook. Docs, READMEs, schema.gql/openapi.json and Word/PDF
updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:04:20 +02:00
0e301ad545 feat: per-project workflow settings, statistics and "my work" home
Per-project settings (owners/admins, «Configuración» tab):
- Statuses with category (TODO, IN_PROGRESS, QA, DONE, CANCELLED), colour and
  "requires reason"; configurable transitions; ticket types (icon, colour) and
  areas. Defaults are the previous fixed values; projects created before keep
  them without migration.
- Values are archived, never deleted: tickets, history and statistics keep
  referring to them. Validated in the domain (ProjectSettingsChangedEvent).
- Releases, board, tracking and "my work" use categories, so custom statuses
  keep working. Status changes carry name/category metadata for history.
- GraphQL: status/type/area are strings (breaking for clients sending enum
  literals); Project.settings, updateProjectSettings, statusCategory,
  myIssues(categories). REST: PUT /projects/{project}/settings, category filters.
- Read model: statusCategory on issues (backfilled at startup).

Statistics («Estadísticas» tab):
- New issue_timeline projection; pure functions compute created vs resolved,
  cumulative flow, cycle/lead time (avg + p85), throughput per person, QA
  pass/fail and release burnup. GraphQL Project.analytics(days),
  Release.burnup; REST /projects/{project}/analytics, /releases/{id}/burnup.
- Web: SVG charts with validated light/dark palette, crosshair tooltip,
  keyboard navigation, legend and data table.

Home "Mi trabajo": my open tickets across projects grouped by category (QA
failures first), tickets waiting for QA, upcoming releases and "Ir a…".

Tests: domain/application specs, e2e scenario for settings and analytics,
facade tests. Docs, READMEs, schema.gql/openapi.json and Word/PDF updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:53:45 +02:00
cbaad17fc7 fix(auth): separate Pireon clients for authorization code and password
Pireon OAuth clients allow a single grant type. With one PIREON_CLIENT_ID of
type password, email + password worked but "Sign in with Pireon" failed with
`unauthorized_client`.

- PIREON_CLIENT_ID/SECRET: authorization_code client (Sign in with Pireon,
  Swagger UI). New PIREON_PASSWORD_CLIENT_ID/SECRET: password client, falling
  back to the former so existing setups keep working.
- Refresh tries both clients: a refresh token only works with its issuer.
- PIREON_PASSWORD_GRANT=false now also rejects password grants in the API
  (before it only hid the form).
- /api/auth/config returns clientId: null when there is no authorization_code
  client, and the web hides "Sign in with Pireon".
- docker-compose passes the new variables; docs 05/07, .env.example,
  openapi.json; unit tests for the client selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:41:50 +02:00
7532938b4e fix(docker): reach services by unique aliases, not bare service names
In Dokploy, services with a domain join the shared dokploy-network, where
another project also has an `api`. nginx proxied /api to `api:3000`, so
Docker DNS returned both containers and half of the requests went to the
other app: 404 "Cannot POST /api/auth/token" on sign-in and "sign-in not
configured" when /api/auth/config was answered by it.

Services now get task-stream-* aliases on the project network and nginx and
the API use them (task-stream-api, -postgres, -mongo, -redis). The API's
OPENAPI_FILE is empty in the container; docs 07 explain the aliases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:26:47 +02:00
341241a52f feat(api): REST API with OpenAPI for other apps, behind Pireon OAuth
A REST adapter next to GraphQL (presenters/http) that calls the same
application services, so validation, permissions and domain errors are the
same: projects (members, board, stats, activity), issues (search, workflow,
assignee, release, links, comments, history), comments, releases and users.
Projects and issues can be addressed by id or key (WEB, WEB-12).

- Swagger UI at /api/docs, document at /api/docs-json and apps/api/openapi.json
  (rewritten by `nx serve api`, like schema.gql) for client generation.
- Every route but health/auth/webhook needs a Pireon token: the document
  declares Pireon OAuth2 (authorization code + PKCE, password) through the
  /api/auth/token proxy, and a bearer scheme for apps with their own client.
- configureApp() shares main.ts setup with the e2e, which now also covers REST.
- SWAGGER_ENABLED / OPENAPI_FILE config; docs 03/05 and library READMEs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:26:47 +02:00
bf0294a188 fix(docker): read the Nexus token from NEXUS_TOKEN instead of ~/.npmrc
Dokploy builds failed with "failed to stat /root/.npmrc": the compose build
secret pointed at ${HOME}/.npmrc, which only exists on a developer machine.
The token now comes from the NEXUS_TOKEN environment variable (Dokploy's
Environment tab or .env), is written to a temporary npmrc and removed in the
same RUN, so it never lands in an image layer.

Also removes a temporary UI dev-server spec that was committed by mistake,
and documents the variable (.env.example, docs 06/07, Word/PDF export).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:11:01 +02:00
566f415fa7 first commit 2026-10-03 13:40:26 +02:00