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

@@ -1636,6 +1636,45 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/projects/{project}/share": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** The project's link for clients, if any */
get: operations["SharingController_share"];
/** Turn the link on or change its blocks (renew: a new secret) */
put: operations["SharingController_save"];
post?: never;
/** The link stops working */
delete: operations["SharingController_remove"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/shared/dashboards/{token}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* What a link for clients shows (no session)
* @description Only the chosen blocks: tickets per status, the sprint under way, release forecasts, open epics and activity per day. The others are null.
*/
get: operations["SharingController_shared"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/projects/{project}/export.csv": {
parameters: {
query?: never;
@@ -3424,6 +3463,59 @@ export interface components {
/** @description Paused webhooks get nothing */
active?: boolean;
};
DashboardShareDto: {
projectId: string;
/** @description Secret of the link /share/{token} */
token: string;
/**
* @example [
* "flow",
* "sprint",
* "releases"
* ]
*/
blocks: string[];
createdAt: string;
};
ShareDashboardDto: {
/**
* @description Blocks to show: flow, sprint, releases, epics, activity
* @example [
* "flow",
* "sprint",
* "releases"
* ]
*/
blocks: string[];
/** @description A new secret: the old link stops working */
renew?: boolean;
};
SharedFlowDto: {
status: string;
name: string;
/** @description A colour name of the project settings */
color: string;
count: number;
};
SharedEpicDto: {
key: string;
title: string;
dueDate: string | null;
done: number;
total: number;
percent: number;
};
SharedDashboardDto: {
projectKey: string;
projectName: string;
generatedAt: string;
blocks: string[];
flow: components["schemas"]["SharedFlowDto"][] | null;
sprint: components["schemas"]["SprintPulseDto"] | null;
releases: components["schemas"]["ReleaseForecastDto"][] | null;
epics: components["schemas"]["SharedEpicDto"][] | null;
activity: components["schemas"]["ActivityDayDto"][] | null;
};
ImportLineDto: {
/** @description Line of the file (the header is 1) */
line: number;
@@ -10881,6 +10973,246 @@ export interface operations {
};
};
};
SharingController_share: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project id or key */
project: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DashboardShareDto"];
};
};
/** @description It has none */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
SharingController_save: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project id or key */
project: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ShareDashboardDto"];
};
};
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DashboardShareDto"];
};
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
SharingController_remove: {
parameters: {
query?: never;
header?: never;
path: {
/** @description Project id or key */
project: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Invalid input */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Missing or rejected Pireon token */
401: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not a member of the project (or not allowed) */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Not found */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Someone changed it at the same time: retry */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
/** @description Business rule (e.g. a transition the workflow forbids) */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ApiErrorDto"];
};
};
};
};
SharingController_shared: {
parameters: {
query?: never;
header?: never;
path: {
token: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SharedDashboardDto"];
};
};
};
};
TransferController_exportCsv: {
parameters: {
query?: never;