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

@@ -465,6 +465,16 @@ input CustomFieldValueInput {
value: String
}
"""The project's link for clients (owners and admins)"""
type DashboardShare {
blocks: [String!]!
createdAt: String!
projectId: ID!
"""Secret of the link /share/{token}"""
token: String!
}
"""A commit, branch or pull request that mentions the ticket"""
type DevLink {
at: String!
@@ -1010,6 +1020,9 @@ type Mutation {
"""Put a ticket in a sprint (null: back to the backlog)"""
setIssueSprint(issueId: ID!, sprintId: ID): CommandResult!
"""Turn the link on or change its blocks; renew for a new secret"""
shareDashboard(input: ShareDashboardInput!): DashboardShare!
"""Start a planned sprint (only one under way per project)"""
startSprint(sprintId: ID!): CommandResult!
@@ -1033,6 +1046,9 @@ type Mutation {
unarchiveProject(projectId: ID!): CommandResult!
unlinkIssues(input: LinkIssuesInput!): CommandResult!
"""The link stops working"""
unshareDashboard(projectId: ID!): Boolean!
"""Push off on this device"""
unsubscribePush(endpoint: String!): Boolean!
@@ -1456,6 +1472,9 @@ type Query {
): [Activity!]!
automations(projectId: ID!): [Automation!]!
"""The project's link for clients, if any (owners and admins)"""
dashboardShare(projectId: ID!): DashboardShare
"""
A project's epics: top-level tickets that group others, with the progress of those
"""
@@ -1532,6 +1551,9 @@ type Query {
"""My views of the project's board, then the shared ones"""
savedFilters(projectId: ID!): [SavedFilter!]!
"""What a link for clients shows, for anyone with it"""
sharedDashboard(token: String!): SharedDashboard
sprint(id: ID!): Sprint!
"""
@@ -1707,6 +1729,50 @@ input SavedFilterCriteriaInput {
unassigned: Boolean
}
"""The project's link for clients"""
input ShareDashboardInput {
"""Blocks to show: flow, sprint, releases, epics, activity"""
blocks: [String!]!
projectId: ID!
"""A new secret: the old link stops working"""
renew: Boolean
}
"""
What a link for clients shows: only the chosen blocks (the others are null)
"""
type SharedDashboard {
activity: [ActivityDay!]
blocks: [String!]!
epics: [SharedEpic!]
flow: [SharedFlow!]
generatedAt: String!
projectKey: String!
projectName: String!
releases: [ReleaseForecast!]
sprint: SprintPulse
}
"""An open epic and how far it got"""
type SharedEpic {
done: Int!
dueDate: String
key: String!
percent: Int!
title: String!
total: Int!
}
"""Tickets in one status"""
type SharedFlow {
"""A colour name of the project settings"""
color: String!
count: Int!
name: String!
status: String!
}
"""A time-boxed iteration: the tickets the team commits to"""
type Sprint {
burndown: SprintBurndown!