feat: a dashboard for clients, behind a secret link

Owners and admins turn it on in Configuración → General, pick what it
shows (tickets per status, the sprint under way, release forecasts,
open epics and activity per day) and can renew or drop the link. Whoever
has it sees /share/{token} without an account: only those blocks, no
people nor single tickets but the epics, no links into the app, read
again every minute. A renewed or dropped link says it no longer works.

REST (GET/PUT/DELETE /api/projects/{project}/share and the public
GET /api/shared/dashboards/{token}) and GraphQL (dashboardShare,
shareDashboard, unshareDashboard, sharedDashboard). The token is kept
in dashboard_shares and goes with the project.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 10:39:26 +02:00
parent 4dc673997d
commit 8fe581ff3e
44 changed files with 3299 additions and 12 deletions

View File

@@ -24,6 +24,7 @@ describe('appRoutes', () => {
'',
'portfolio',
'explore',
'share/:token',
'cartera',
'explorar',
'p/:key',
@@ -31,10 +32,12 @@ describe('appRoutes', () => {
expect(children[0].canActivate).toContain(authGuard);
expect(children[1].canActivate).toContain(authGuard);
expect(children[2].canActivate).toBeUndefined();
expect(children[5].canActivate).toBeUndefined();
// The page for clients needs no session: its link is the key.
expect(children[3].canActivate).toBeUndefined();
expect(children[6].canActivate).toBeUndefined();
// The Spanish paths of shared links lead to the English ones.
expect(children[3].redirectTo).toBe('portfolio');
expect(children[4].redirectTo).toBe('explore');
expect(children[4].redirectTo).toBe('portfolio');
expect(children[5].redirectTo).toBe('explore');
});
it('gives each page its own state, not the root', async () => {