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

@@ -74,6 +74,13 @@ export interface PushSubscriptionDoc {
keys: { p256dh: string; auth: string };
createdAt: string;
}
export interface DashboardShareDoc {
/** The project's id. */
id: string;
token: string;
blocks: string[];
createdAt: string;
}
export interface CalendarFeedDoc {
/** The user's id. */
id: string;
@@ -103,6 +110,7 @@ export const ReadModels = {
Automation: 'Automation',
AutomationRun: 'AutomationRun',
CalendarFeed: 'CalendarFeed',
DashboardShare: 'DashboardShare',
PushSubscription: 'PushSubscription',
} as const;
@@ -370,6 +378,19 @@ class CalendarFeedDocument {
export const CalendarFeedSchema =
SchemaFactory.createForClass(CalendarFeedDocument);
/** One per project: the secret of its link for clients, and what it shows. */
@Schema({ ...LOOSE, collection: 'dashboard_shares' })
class DashboardShareDocument {
/** The project's id. */
@Prop({ type: String, required: true, unique: true }) id!: string;
@Prop({ type: String, required: true, unique: true }) token!: string;
@Prop({ type: [String] }) blocks!: string[];
@Prop({ type: String }) createdAt!: string;
}
export const DashboardShareSchema = SchemaFactory.createForClass(
DashboardShareDocument,
);
/** The devices where each person turned push on. */
@Schema({ ...LOOSE, collection: 'push_subscriptions' })
class PushSubscriptionDocument {
@@ -404,6 +425,7 @@ export const READ_MODEL_SCHEMAS = [
{ name: ReadModels.SavedFilter, schema: SavedFilterSchema },
{ name: ReadModels.MyView, schema: MyViewSchema },
{ name: ReadModels.CalendarFeed, schema: CalendarFeedSchema },
{ name: ReadModels.DashboardShare, schema: DashboardShareSchema },
{ name: ReadModels.PushSubscription, schema: PushSubscriptionSchema },
];