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

@@ -1,5 +1,10 @@
import { Home } from '../models/home';
import { PortfolioProject, ProjectInsights } from '../models/insights';
import {
DashboardShare,
SharedBlock,
SharedDashboard,
} from '../models/sharing';
import { Observable } from 'rxjs';
import {
Activity,
@@ -368,6 +373,21 @@ export abstract class CalendarGateway {
abstract revoke(): Observable<void>;
}
/** The project's dashboard for clients, behind a secret link. */
export abstract class SharingGateway {
/** The project's link, if it has one (owners and admins). */
abstract share(projectId: string): Observable<DashboardShare | null>;
/** Turns it on or changes its blocks; `renew` makes a new secret. */
abstract save(
projectId: string,
blocks: SharedBlock[],
renew: boolean,
): Observable<DashboardShare>;
abstract remove(projectId: string): Observable<void>;
/** What a link shows, for anyone (null: no such link). */
abstract shared(token: string): Observable<SharedDashboard | null>;
}
export abstract class NotificationsGateway {
abstract mine(
unreadOnly: boolean,