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:
41
libs/web/tracker/domain/src/lib/models/sharing.ts
Normal file
41
libs/web/tracker/domain/src/lib/models/sharing.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ReleaseForecast, SprintPulse } from './insights';
|
||||
|
||||
/** The blocks a project can show to its clients, in the order they are shown. */
|
||||
export const SHARED_BLOCKS = [
|
||||
'flow',
|
||||
'sprint',
|
||||
'releases',
|
||||
'epics',
|
||||
'activity',
|
||||
] as const;
|
||||
export type SharedBlock = (typeof SHARED_BLOCKS)[number];
|
||||
|
||||
/** The project's link for clients: its secret and what it shows. */
|
||||
export interface DashboardShare {
|
||||
projectId: string;
|
||||
token: string;
|
||||
blocks: SharedBlock[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** What a client sees through the link (blocks not chosen come as null). */
|
||||
export interface SharedDashboard {
|
||||
projectKey: string;
|
||||
projectName: string;
|
||||
generatedAt: string;
|
||||
blocks: SharedBlock[];
|
||||
flow: { status: string; name: string; color: string; count: number }[] | null;
|
||||
sprint: SprintPulse | null;
|
||||
releases: ReleaseForecast[] | null;
|
||||
epics:
|
||||
| {
|
||||
key: string;
|
||||
title: string;
|
||||
dueDate: string | null;
|
||||
done: number;
|
||||
total: number;
|
||||
percent: number;
|
||||
}[]
|
||||
| null;
|
||||
activity: { day: string; count: number }[] | null;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user