# Public activity pages

A public activity page shows human contributions to public GitHub and GitLab repositories. It includes pull requests, reviews, and issues from workspace members and outside contributors. A person’s detail shows counts by repository, a public work timeline, and weekly pull request, review, or issue counts. Work titles link to the provider. It excludes private repositories, account details, automation, practices, feedback, Slack, Outline, and AI content.

Keep course workspaces private. Before publication, verify the activity repair described in the [activity history runbook](/next/admin/activity-history-repair.md).

## Settings in the UI

Three screens control public activity pages. Each change takes effect at once. Visitors see a change when they load the page again. A page that is already open in a browser keeps its contents until it loads again.

| Who                     | Where                                            | Control                                                                                  |
| ----------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------- |
| Instance administrator  | **Instance settings**, **Public activity pages** | **Allow public activity pages**                                                          |
| Workspace administrator | **Workspace settings**, **Public activity page** | **Publish the page**, **Allow search engines**, **Show contributors without an account** |
| Workspace administrator | **Workspace activity**, a person's level         | **Hide from activity**                                                                   |

### Instance administrators

**Allow public activity pages** is the instance switch. While it is off, no workspace can publish a page, and every public page stops. A change asks for a recent sign-in. The switch shows the stored choice, not the configured default.

### Workspace administrators

**Publish the page** asks first. The confirmation states what becomes public:

* the people who chose to show, by name, picture and profile link
* the pull requests (merge requests on GitLab) each person opened and reviewed, and the issues they opened
* their public work timeline and weekly activity counts

It also states that the page shows nothing from private repositories, practices, feedback, AI content, Slack, Outline or automation accounts. It states that members and people who signed in are hidden until they choose to show. It also states that people without an account are counted, not named, until you show them. It also states that anyone on the page can hide in User settings. Turning the page off needs no confirmation.

**Publish the page** shows the saved choice of the workspace. The saved choice alone does not make the page public. For the other conditions, see [Workspace settings](#workspace-settings). When the saved choice is on and public availability is confirmed, the section shows the address of the workspace.

Signed-in members open their workspace home at that address. To preview the page, open the address in a private window. If the page is on but public availability is not confirmed, the section shows no address and says that availability is not confirmed.

**Allow search engines** is off by default, and it exists only while the page is on. Turning the page off sets it back to off, so publishing again starts with search engines off. Off, the page sets `<meta name="robots" content="noindex">`, and the API response carries `X-Robots-Tag: noindex`. The webapp sets the meta tag with JavaScript, so a crawler that does not run JavaScript does not see it. This is a request to search engines, not access control.

**Show contributors without an account** is off by default, and it exists only while the page is on. Off, the page does not name a contributor who has no account that chose to show. It shows only their number, for example "and 3 more contributors", so the page does not hide that they contributed. Their work does not count in the totals. Their person pages are not available.

On, the page names them until they object. To turn it on, review the confirmation and select **Show contributors**. The confirmation asks you to check your organization's legal basis before publication. Turn it off to hide them again. Turning the page off sets it back to off.

This default follows data protection by default (GDPR Art. 25(2)). The page publishes a person only after that person or an administrator chooses it.

**People left out of the page** shows how many people the page leaves out and does not count. It includes people who hid themselves, people who have not answered yet, and people an administrator hid. It never shows who hid themselves.

To leave out a person without a membership, open the person on **Workspace activity**. Select **Hide from activity**. The action is not offered for a member. Hide a member under **Members**. The person then leaves the public page and Workspace activity. Right after the action, the message **Undo** restores the person.

Later, **Workspace settings** lists the people without a membership whom an administrator hid, under **Hidden people**. Select **Show again** beside a person to restore them. Show a member again under **Members**, and a team or repository under **Team setup**.

### Members

A member is on public pages only after they choose to show, in any workspace or in User settings. Until then, the member is not on any public page. This includes members from the provider roster who have not signed in yet. Their work does not count in the totals of the page. An answer to the question of a workspace that a member gave before this rule stays as it is. A choice to show that a member saved only in User settings before this rule counts as no answer.

In a workspace that publishes a page, each page of a member starts with a notice until the member answers. The notice says that the page is public and what it shows of them. It is part of the page and does not take focus.

**Show me** and **Hide me** have the same weight. Either answer records that the person answered for this workspace. **Decide later** hides the notice for that visit without an answer, so a member who has not answered sees it again on the next visit. A save that fails or takes more than 15 seconds shows an error, and the member can answer again or decide later. The AI setup of the workspace comes first, and the notice waits until it ends. A person who is not a member sees no notice on the public page, because only a member can answer.

**Members** in the workspace administration has a **Public activity** column. It marks each member who has not answered the question of this workspace with **Not answered yet**. Workspace admins see the column, and so do instance admins who administer the workspace. The column is there only while the workspace publishes a page. The Members page does not show which way anyone chose. The public page itself shows who chose to show.

The column shows the answer to the question of this workspace, not the account-wide choice. A member who chose in User settings or in another workspace is still marked until they answer here. Public pages follow the account-wide choice: such a member can already be on the page.

**User settings** has the same choice as **Show me on public activity pages**. It applies to every public page, in every workspace, at once. After either choice, other people see the change when they load the page again.

## Instance setting

**Allow public activity pages** defaults to off. Set `HEPHAESTUS_PUBLIC_ACTIVITY_ENABLED=true` to supply an instance default. An instance administrator can read and replace this default with `GET` and `PUT /admin/settings/public-activity`. The PUT body is `{"allowed": true}` or `{"allowed": false}`. The stored choice takes precedence over the configured default and is audited. Turning the setting off stops all public activity pages on the next request.

## Workspace settings

A workspace administrator can read `GET /workspaces/{slug}/public-activity` and change `PATCH /workspaces/{slug}/public-activity`. The PATCH body is `{"publicActivityEnabled": true, "allowSearchEngines": false, "contributorsWithoutAccount": false}`. Send only the settings that you change. Omitted or null fields stay unchanged. Turning the page off resets the other two settings. Changes are audited.

The page is available only when the instance allows it and the active workspace enables it.

**Allow search engines** defaults to off. The JSON response then carries `X-Robots-Tag: noindex`. The DTO also returns `allowSearchEngines: false` for a page client to set `<meta name="robots" content="noindex">` on its HTML page. The header protects the JSON response only. It does not protect the webapp HTML.

The API does not set a robots instruction on HTML pages. The page client must supply it.

Neither instruction is access control. See [Google's robots header contract](https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag).

## Personal choice and objections

Each account is hidden on public activity pages until its person chooses to show. A signed-in person can read and change `GET` and `PUT /user/public-activity` with `{"visible": false}` or `{"visible": true}`. The choice applies to all linked provider identities in every workspace. A minimal provider-id and subject marker retains an objection after unlinking or account deletion. Showing activity again clears markers for identities currently linked to the account, not identities it no longer owns.

This marker affects publication only. It does not suppress internal processing.

People who hid themselves, and people an administrator hid, do not contribute to public totals. An administrator can read only the count through `GET /workspaces/{slug}/activity/public-hidden-count`. This endpoint does not identify them.

A workspace administrator can list the people without a membership whom an administrator hid with `GET /workspaces/{slug}/activity/hidden-contributors`. It lists administrator decisions only, not account objections.

For an objection from a person without an account, the workspace administrator can use `PATCH /workspaces/{slug}/activity/people/{userId}/public-visibility?hidden=true`. Use `hidden=false` to restore visibility. The action reuses workspace activity hiding and therefore also hides that contributor from member activity. It does not erase source work, block provider synchronization, or grant workspace access. Restoring workspace visibility never overrides an account-wide opt-out.

For an outside contributor, the audit entry records the workspace and change, not the contributor's identity. For a member, this action uses the existing membership visibility operation and its workspace-role audit entry. An objection with broader scope belongs in the existing person-data request process.

Workspace administrators handle this action because their workspace publishes the activity. Instance administrators can use the existing audited workspace elevation when they handle the privacy contact. Do not use the permanent erasure suppression mechanism for a reversible publication objection.

## Onboarding contract

A signed-in visitor can read `GET /user/public-activity/workspaces/{slug}/onboarding`. The response gives `seen` and the account-wide `visible` choice. `PUT` with `{"visible": true}` or `{"visible": false}` applies the choice and marks this workspace as seen in one transaction. The public-workspace visitor needs no workspace membership. The account always comes from the authenticated session. A page client can use this state for a one-time Show me / Hide me step.

## Anonymous responses

Three routes publish workspace activity without sign-in, through `GET` and `HEAD`:

| Route                                                    | Published data                                                            |
| -------------------------------------------------------- | ------------------------------------------------------------------------- |
| `/public/workspaces/{slug}/activity`                     | People and repository facets                                              |
| `/public/workspaces/{slug}/activity/people/{login}`      | One person's profile identity, counts, weekly breakdown, and repositories |
| `/public/workspaces/{slug}/activity/people/{login}/work` | That person's public work timeline, without authors or participants       |

Each accepts activity range presets, custom dates, and `repo`, which names public repositories to count and repeats for more than one. The work route also accepts repeated `kinds`, `size` from 1 to 100 (default 30), and the preceding page's `nextCursor` as `cursor`. The supported kinds are pull requests opened or merged, reviews approved, requesting changes or commented, and issues opened. The person response lists these as `supportedKinds`. Comments on code or in conversations, and close events, are not published. An unknown repository is a not-found error.

The response names the provider (`providerType`) so a client can use its words and icons. Each query requires stored public visibility and a recent successful repository metadata read for this workspace. A missing confirmation, an expired confirmation, or a known unavailable repository excludes the repository, its contributions, and its coverage from the public response. A sync that reports repository access loss (404 or authorization failure) marks that workspace's monitor unavailable immediately.

Installation repository removal and GitLab project removal or transfer out of scope stop publication by removing the workspace monitor. A GitLab project event whose metadata lookup reports access loss also marks the monitor unavailable. These actions do not change another workspace's access to the same repository. A successful sync can restore publication when it confirms that the repository is public and accessible again.

`HEPHAESTUS_PUBLIC_ACTIVITY_REPOSITORY_VISIBILITY_MAX_AGE` supplies `hephaestus.public-activity.repository-visibility-max-age` and defaults to `48h`. The value must be a positive duration. Use twice your configured metadata sync interval when you change the cadence. The limit is enforced during each public query, without a cleanup job.

If metadata reads stop, publication stops no later than this limit after the last successful repository metadata read. This includes disabled sync, credentials that no longer work, and failed metadata reads. A failed metadata fetch does not renew the confirmation. The separate visibility confirmation does not advance when only issues, pull requests, or other resources sync. A repository without a successful metadata confirmation is not published. Run a repository sync before enabling a page.

GitLab project create, rename, transfer and update events refresh stored visibility. A system hook supplies `project_update`. Normal project and group webhooks do not supply visibility-change events. Push events and metadata sync paths also refresh stored visibility. Without a visibility event, the next successful metadata sync detects a private change. If the confirmation expires first, the repository is excluded.

See [GitHub installation repository events](https://docs.github.com/en/webhooks/webhook-events-and-payloads#installation_repositories), [GitLab system hooks](https://docs.gitlab.com/administration/system_hooks/) and [GitLab webhook events](https://docs.gitlab.com/user/project/integrations/webhook_events/#project-events). Unknown, inactive, and disabled pages return the same not-found problem. Other workspace endpoints require sign-in and workspace access.

Every response carries `Cache-Control: no-store`, so no browser or shared cache keeps a page. The existing shared rate limiter defaults to 60 anonymous requests per minute per client IP across workspace slugs. The Helm chart exposes no value to change this rate limit. Signed-in requests do not consume this anonymous IP budget. The rate limiter refuses requests if its backend is unavailable. Proxy trust comes from the servlet container, not a second parser for forwarded headers.

Publication changes apply to new server requests. The server cannot recall a response already delivered. See [Spring Security's cache header contract](https://docs.spring.io/spring-security/reference/servlet/exploits/headers.html).
