The Redis consumer used to leave a failed event pending and go on to the
next ones, retrying it only at the next start. With Mongo down for a
while, a later change of the same ticket was then projected first, and
the version check dropped the earlier one for good (a status change
could vanish from the read model), besides the read side lagging until
a restart.
Now a failed event is retried in place, waiting longer each time (up to
30 s), and the next one waits for it. Failures of the infrastructure
(Mongo, Redis, the network: isTransientError) wait as long as it takes,
so the read side catches up by itself; an event that keeps failing on
its own goes, after 5 attempts, to <stream>:dead with its error, so it
does not hold up the rest.
GET /api/health reports the way from write to read (events waiting in
the outbox, pending to be projected, set aside) and says `degraded`,
still with 200, when something is stuck.
Tried for real: with Mongo stopped, two status changes went in; the
first failed to project with ECONNREFUSED, and once Mongo was back the
ticket's history had both steps, in order, without restarting the API.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only what the header needs from the start stays at the root: the
session, my projects for the command palette, the inbox and push for
the bell, and their adapters. The home page's facades (my work, the
rest of the home, my views, the calendar), Explore's and the proposal
form's are provided by their routes, whose injectors keep them for
when you come back. The other adapters come with the tracker's routes,
now loaded lazily.
The web libs declare "sideEffects": false, so a chunk carries what it
uses and not everything an index.ts exports: main goes from 1.31 MB
to 432 kB of the same initial total, which is still held up by @pi/ui
(one module, with all of Material) and the GraphQL documents, both
needed by the header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Services run commands through a CommandRunner that runs one again (up to
5 times, waiting a little longer each time) when its aggregate changed
underneath, e.g. a saga writing to the same ticket. Each command loads its
aggregate fresh and stores its events in one transaction, so a
ConcurrencyError means nothing was stored and running it again is safe.
People no longer get a 409 for it, and the e2e suite stops flaking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Bulk editing gets «Release…» (open releases or none), and the list
view a release filter, also reachable from each release («Ver en la
lista», ?release=).
- Archiving a release asks where its unfinished tickets go, as closing
a sprint does: ReleaseArchivedEvent carries `moveTo` (an open release
of the same project, or none) and the saga moves them there; finished
ones stay. In GraphQL, REST and the MCP's archive_release.
- Chapter 10 lists the next features and the ideas to weigh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Mongo read models are @Schema/@Prop classes (SchemaFactory), still
loose: the props declared are the indexed or shaped ones.
- CommandBus and QueryBus are used by the application services alone.
Duplicating a ticket, bulk updates, CSV imports, proposals and
emptying the trash chain several commands, so they are service
methods now and their composite commands are gone; every command
handler is atomic. No forwardRef is needed.
- The due-date and trash timers are entry points: they live in the
presenters and call the services. The automations event handler has
its own file and calls AutomationsService.
- The tracker's user directory asks IdentityService; the composition
root hands the identity application module to the tracker
infrastructure (withIdentity), with no global module.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Postgres: every table has a uuid `id` (gen_random_uuid()); the event
store keeps its relay order in `seq`, and the lookup tables their
uniqueness in constraints of their own.
- MongoDB: `_id` is Mongo's ObjectId; `id` is the domain id (the same
uuid as the aggregate's stream in Postgres), unique, and every query
goes by it. Production starts from empty databases.
- Web: linking tickets and hanging one from a parent are picked from a
searchable select (links across projects, parents within the project)
instead of typing a key; every checkbox is @pi/ui's pi-checkbox.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Like a commenter, whoever opens a ticket and whoever it is assigned to
(also on reassigning) follow it from then on, so they hear of its
priority, due date and sprint changes and find it under "Que sigo".
Existing tickets are left as they are. Docs, Word and PDF updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plan sprints with dates and a goal, run one at a time per project, and
put tickets in them from the ticket page. The Sprints tab follows the one
under way with its progress and a burndown in tickets or points against
the ideal line. Closing a sprint sends what was left to the next one or
back to the backlog; the board can be filtered by sprint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tickets can hang from another one of the same project, three levels deep
at most (epic → ticket → subtask). The ticket page lists its subtasks with
their progress, creates new ones and moves the ticket under an epic; a new
Épicas tab shows every epic and how far it has got. Each ticket also gets a
checklist for the small steps that don't deserve a ticket.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mention project members in comments with @, follow and unfollow tickets,
and get told when something changes on the tickets you follow. Commenting
on a ticket starts following it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>