tenseleyflow/shithub / f1b97dc

Browse files

Add private contribution setting

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
f1b97dcc7a553684770db85885ad1bbaeeec3ca0
Parents
778c821
Tree
c240f19

27 changed files

StatusFile+-
M docs/internal/profile.md 10 4
M internal/actions/sqlc/models.go 24 23
M internal/admin/sqlc/models.go 24 23
M internal/auth/policy/sqlc/models.go 24 23
M internal/checks/sqlc/models.go 24 23
M internal/issues/sqlc/models.go 24 23
M internal/meta/sqlc/models.go 24 23
A internal/migrationsfs/migrations/0055_user_private_contributions.sql 15 0
M internal/notif/sqlc/models.go 24 23
M internal/orgs/sqlc/models.go 24 23
M internal/pulls/sqlc/models.go 24 23
M internal/ratelimit/sqlc/models.go 24 23
M internal/repos/sqlc/models.go 24 23
M internal/social/sqlc/models.go 24 23
M internal/users/queries/users.sql 3 0
M internal/users/sqlc/models.go 24 23
M internal/users/sqlc/querier.go 1 0
M internal/users/sqlc/users.sql.go 24 5
A internal/web/handlers/profile/contribution_settings.go 79 0
M internal/web/handlers/profile/overview.go 37 28
M internal/web/handlers/profile/profile.go 25 22
M internal/web/handlers/profile/profile_test.go 93 1
M internal/web/render/octicons.go 2 0
M internal/web/static/css/shithub.css 43 10
M internal/web/templates/profile/view.html 23 6
M internal/webhook/sqlc/models.go 24 23
M internal/worker/sqlc/models.go 24 23
docs/internal/profile.mdmodified
@@ -7,6 +7,7 @@ S09 shipped the public `/{username}` page and the `/avatars/{username}` route. L
77
 | Route | Source | Notes |
88
 |---|---|---|
99
 | `GET /{username}` | profile.serveProfile | Public profile. citext lookup; canonical-case 301; reserved short-circuit. |
10
+| `POST /{username}/contribution-settings` | profile.contributionSettingsUpdate | Auth required. Profile owner toggles private contribution counts. |
1011
 | `POST /{username}/pins` | profile.pinsUpdate | Auth required. Profile owner saves up to six public owned repositories. |
1112
 | `GET /avatars/{username}` | profile.serveAvatar | Streams uploaded avatar OR falls back to deterministic SVG identicon. |
1213
 
@@ -96,13 +97,14 @@ The overview looks for a visible repository owned by the user whose name matches
9697
 
9798
 ## Contribution calendar
9899
 
99
-The overview contribution calendar is computed from local Git history for repositories visible to the viewer:
100
+The overview contribution calendar is computed from local Git history:
100101
 
101102
 - The window is the last 365 days, rendered as a 53-week GitHub-style grid.
102
-- Only visible repositories are scanned, capped at 80 repos and 2,000 commits per repo for request-time safety.
103
+- Public repositories are scanned when visible to the viewer, capped at 500 repos and 5,000 commits per repo for request-time safety.
103104
 - When the user has verified email addresses, commits are counted only if the author email matches one of them.
104
-- If no verified email exists, shithub falls back to visible user-owned repository commits as a best-effort local signal.
105
-- Private repository contributions appear only when the viewer can already see those repositories.
105
+- On affiliated repositories (user-owned repos and repos owned by organizations the user belongs to), shithub also accepts username, display-name, and GitHub noreply-address matches as a best-effort imported-history signal.
106
+- Arbitrary public repositories outside the user's affiliation remain verified-email-only to avoid spoofed username/display-name commits.
107
+- Private repositories are excluded by default. When the profile owner enables "Private contributions", private user-owned and member-org repositories contribute to the aggregate graph counts, but repository names and commit metadata are not exposed.
106108
 
107109
 ## Self-view enrichment
108110
 
@@ -113,6 +115,10 @@ When the viewer's session matches the profile's user (`viewer.ID == user.ID`):
113115
 - A "Customize pins" modal lists the user's public repositories with a
114116
   live client-side filter and persists up to six selected repos through
115117
   `profile_pin_sets` / `profile_pins` (migration 0040).
118
+- The "Contribution settings" menu toggles the owner's
119
+  `users.include_private_contributions` preference. The checked state
120
+  mirrors the persisted setting, and the graph is recomputed after the
121
+  POST redirect.
116122
 - Cache-Control flips from `max-age=300` (anonymous) to `no-cache, private` so admin- or settings-driven changes appear immediately.
117123
 
118124
 Pinned repositories are intentionally public-only. Private repos are
internal/actions/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/admin/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/auth/policy/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/checks/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/issues/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/meta/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/migrationsfs/migrations/0055_user_private_contributions.sqladded
@@ -0,0 +1,15 @@
1
+-- SPDX-License-Identifier: AGPL-3.0-or-later
2
+--
3
+-- Profile contribution privacy:
4
+--
5
+-- - include_private_contributions mirrors GitHub's profile setting for
6
+--   displaying private contribution counts on the public contribution graph.
7
+--   The graph never exposes private repository names or commit metadata.
8
+
9
+-- +goose Up
10
+ALTER TABLE users
11
+    ADD COLUMN include_private_contributions boolean NOT NULL DEFAULT false;
12
+
13
+-- +goose Down
14
+ALTER TABLE users
15
+    DROP COLUMN IF EXISTS include_private_contributions;
internal/notif/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/orgs/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/pulls/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/ratelimit/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/repos/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/social/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/users/queries/users.sqlmodified
@@ -94,6 +94,9 @@ WHERE user_id = $1 AND changed_at > $2;
9494
 -- name: UpdateUserTheme :exec
9595
 UPDATE users SET theme = $2 WHERE id = $1;
9696
 
97
+-- name: UpdateUserPrivateContributions :exec
98
+UPDATE users SET include_private_contributions = $2 WHERE id = $1;
99
+
97100
 -- name: BumpUserSessionEpoch :exec
98101
 UPDATE users SET session_epoch = session_epoch + 1 WHERE id = $1;
99102
 
internal/users/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/users/sqlc/querier.gomodified
@@ -134,6 +134,7 @@ type Querier interface {
134134
 	UnsuspendUser(ctx context.Context, db DBTX, id int64) error
135135
 	UpdateUserAvatarKey(ctx context.Context, db DBTX, arg UpdateUserAvatarKeyParams) error
136136
 	UpdateUserPassword(ctx context.Context, db DBTX, arg UpdateUserPasswordParams) error
137
+	UpdateUserPrivateContributions(ctx context.Context, db DBTX, arg UpdateUserPrivateContributionsParams) error
137138
 	UpdateUserProfile(ctx context.Context, db DBTX, arg UpdateUserProfileParams) error
138139
 	UpdateUserTheme(ctx context.Context, db DBTX, arg UpdateUserThemeParams) error
139140
 	UpsertUserNotificationPref(ctx context.Context, db DBTX, arg UpsertUserNotificationPrefParams) error
internal/users/sqlc/users.sql.gomodified
@@ -53,7 +53,7 @@ const createUser = `-- name: CreateUser :one
5353
 
5454
 INSERT INTO users (username, display_name, password_hash)
5555
 VALUES ($1, $2, $3)
56
-RETURNING id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch, is_site_admin
56
+RETURNING id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch, is_site_admin, include_private_contributions
5757
 `
5858
 
5959
 type CreateUserParams struct {
@@ -90,12 +90,13 @@ func (q *Queries) CreateUser(ctx context.Context, db DBTX, arg CreateUserParams)
9090
 		&i.Theme,
9191
 		&i.SessionEpoch,
9292
 		&i.IsSiteAdmin,
93
+		&i.IncludePrivateContributions,
9394
 	)
9495
 	return i, err
9596
 }
9697
 
9798
 const getUserByID = `-- name: GetUserByID :one
98
-SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch, is_site_admin
99
+SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch, is_site_admin, include_private_contributions
99100
 FROM users
100101
 WHERE id = $1 AND deleted_at IS NULL
101102
 `
@@ -127,12 +128,13 @@ func (q *Queries) GetUserByID(ctx context.Context, db DBTX, id int64) (User, err
127128
 		&i.Theme,
128129
 		&i.SessionEpoch,
129130
 		&i.IsSiteAdmin,
131
+		&i.IncludePrivateContributions,
130132
 	)
131133
 	return i, err
132134
 }
133135
 
134136
 const getUserByUsername = `-- name: GetUserByUsername :one
135
-SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch, is_site_admin
137
+SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch, is_site_admin, include_private_contributions
136138
 FROM users
137139
 WHERE username = $1 AND deleted_at IS NULL
138140
 `
@@ -164,12 +166,13 @@ func (q *Queries) GetUserByUsername(ctx context.Context, db DBTX, username strin
164166
 		&i.Theme,
165167
 		&i.SessionEpoch,
166168
 		&i.IsSiteAdmin,
169
+		&i.IncludePrivateContributions,
167170
 	)
168171
 	return i, err
169172
 }
170173
 
171174
 const getUserByUsernameIncludingDeleted = `-- name: GetUserByUsernameIncludingDeleted :one
172
-SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch, is_site_admin FROM users WHERE username = $1
175
+SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch, is_site_admin, include_private_contributions FROM users WHERE username = $1
173176
 `
174177
 
175178
 func (q *Queries) GetUserByUsernameIncludingDeleted(ctx context.Context, db DBTX, username string) (User, error) {
@@ -199,12 +202,13 @@ func (q *Queries) GetUserByUsernameIncludingDeleted(ctx context.Context, db DBTX
199202
 		&i.Theme,
200203
 		&i.SessionEpoch,
201204
 		&i.IsSiteAdmin,
205
+		&i.IncludePrivateContributions,
202206
 	)
203207
 	return i, err
204208
 }
205209
 
206210
 const getUserIncludingDeleted = `-- name: GetUserIncludingDeleted :one
207
-SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch, is_site_admin FROM users WHERE id = $1
211
+SELECT id, username, display_name, primary_email_id, password_hash, password_algo, password_updated_at, email_verified, last_login_at, suspended_at, suspended_reason, deleted_at, created_at, updated_at, bio, location, website, company, pronouns, avatar_object_key, theme, session_epoch, is_site_admin, include_private_contributions FROM users WHERE id = $1
208212
 `
209213
 
210214
 // Like GetUserByID but returns the row even when deleted_at IS NOT NULL.
@@ -235,6 +239,7 @@ func (q *Queries) GetUserIncludingDeleted(ctx context.Context, db DBTX, id int64
235239
 		&i.Theme,
236240
 		&i.SessionEpoch,
237241
 		&i.IsSiteAdmin,
242
+		&i.IncludePrivateContributions,
238243
 	)
239244
 	return i, err
240245
 }
@@ -399,6 +404,20 @@ func (q *Queries) UpdateUserPassword(ctx context.Context, db DBTX, arg UpdateUse
399404
 	return err
400405
 }
401406
 
407
+const updateUserPrivateContributions = `-- name: UpdateUserPrivateContributions :exec
408
+UPDATE users SET include_private_contributions = $2 WHERE id = $1
409
+`
410
+
411
+type UpdateUserPrivateContributionsParams struct {
412
+	ID                          int64
413
+	IncludePrivateContributions bool
414
+}
415
+
416
+func (q *Queries) UpdateUserPrivateContributions(ctx context.Context, db DBTX, arg UpdateUserPrivateContributionsParams) error {
417
+	_, err := db.Exec(ctx, updateUserPrivateContributions, arg.ID, arg.IncludePrivateContributions)
418
+	return err
419
+}
420
+
402421
 const updateUserProfile = `-- name: UpdateUserProfile :exec
403422
 UPDATE users
404423
 SET display_name = $2,
internal/web/handlers/profile/contribution_settings.goadded
@@ -0,0 +1,79 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package profile
4
+
5
+import (
6
+	"errors"
7
+	"net/http"
8
+	"net/url"
9
+	"strings"
10
+
11
+	"github.com/go-chi/chi/v5"
12
+	"github.com/jackc/pgx/v5"
13
+
14
+	authpkg "github.com/tenseleyFlow/shithub/internal/auth"
15
+	usersdb "github.com/tenseleyFlow/shithub/internal/users/sqlc"
16
+	"github.com/tenseleyFlow/shithub/internal/web/middleware"
17
+)
18
+
19
+func (h *Handlers) contributionSettingsUpdate(w http.ResponseWriter, r *http.Request) {
20
+	ctx := r.Context()
21
+	rawName := chi.URLParam(r, "username")
22
+	lower := strings.ToLower(rawName)
23
+	if authpkg.IsReserved(lower) {
24
+		h.d.Render.HTTPError(w, r, http.StatusNotFound, r.URL.Path)
25
+		return
26
+	}
27
+	viewer := middleware.CurrentUserFromContext(ctx)
28
+	if viewer.IsAnonymous() {
29
+		h.d.Render.HTTPError(w, r, http.StatusUnauthorized, "")
30
+		return
31
+	}
32
+	if err := r.ParseForm(); err != nil {
33
+		h.d.Render.HTTPError(w, r, http.StatusBadRequest, "")
34
+		return
35
+	}
36
+
37
+	user, err := h.q.GetUserByUsername(ctx, h.d.Pool, rawName)
38
+	if err != nil {
39
+		if errors.Is(err, pgx.ErrNoRows) {
40
+			h.d.Render.HTTPError(w, r, http.StatusNotFound, r.URL.Path)
41
+			return
42
+		}
43
+		h.d.Logger.ErrorContext(ctx, "profile contributions: user lookup", "error", err)
44
+		h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
45
+		return
46
+	}
47
+	if user.SuspendedAt.Valid || user.DeletedAt.Valid {
48
+		h.d.Render.HTTPError(w, r, http.StatusGone, "")
49
+		return
50
+	}
51
+	if viewer.ID != user.ID {
52
+		h.d.Render.HTTPError(w, r, http.StatusForbidden, "")
53
+		return
54
+	}
55
+
56
+	includePrivate := r.PostFormValue("include_private_contributions") == "1"
57
+	if err := h.q.UpdateUserPrivateContributions(ctx, h.d.Pool, usersdb.UpdateUserPrivateContributionsParams{
58
+		ID:                          user.ID,
59
+		IncludePrivateContributions: includePrivate,
60
+	}); err != nil {
61
+		h.d.Logger.ErrorContext(ctx, "profile contributions: update settings", "user_id", user.ID, "error", err)
62
+		h.d.Render.HTTPError(w, r, http.StatusInternalServerError, "")
63
+		return
64
+	}
65
+	http.Redirect(w, r, profileContributionSettingsReturnPath(r.PostFormValue("return_to"), user.Username), http.StatusSeeOther)
66
+}
67
+
68
+func profileContributionSettingsReturnPath(raw, username string) string {
69
+	fallback := "/" + url.PathEscape(username)
70
+	u, err := url.Parse(raw)
71
+	if err != nil || u.IsAbs() || u.Host != "" || u.Path != fallback {
72
+		return fallback
73
+	}
74
+	u.Scheme = ""
75
+	u.Host = ""
76
+	u.User = nil
77
+	u.Fragment = ""
78
+	return u.RequestURI()
79
+}
internal/web/handlers/profile/overview.gomodified
@@ -49,16 +49,17 @@ type profileReadme struct {
4949
 }
5050
 
5151
 type contributionCalendar struct {
52
-	Total             int
53
-	Period            string
54
-	Weeks             []contributionWeek
55
-	Years             []contributionYear
56
-	CurrentYear       int
57
-	SelectedYear      int
58
-	MonthLabel        string
59
-	MonthCommitCount  int
60
-	MonthRepoCount    int
61
-	HasRepositoryData bool
52
+	Total                       int
53
+	Period                      string
54
+	Weeks                       []contributionWeek
55
+	Years                       []contributionYear
56
+	CurrentYear                 int
57
+	SelectedYear                int
58
+	MonthLabel                  string
59
+	MonthCommitCount            int
60
+	MonthRepoCount              int
61
+	HasRepositoryData           bool
62
+	IncludePrivateContributions bool
6263
 }
6364
 
6465
 type contributionYear struct {
@@ -213,7 +214,7 @@ func (h *Handlers) contributionCalendar(ctx context.Context, user usersdb.User,
213214
 	gridStart := windowStart.AddDate(0, 0, -int(windowStart.Weekday()))
214215
 	windowEnd := windowEndDay.Add(24 * time.Hour)
215216
 	activityMonth := time.Date(windowEndDay.Year(), windowEndDay.Month(), 1, 0, 0, 0, 0, time.UTC)
216
-	repos := h.profileContributionRepos(ctx, user, viewer)
217
+	repos := h.profileContributionRepos(ctx, user, viewer, user.IncludePrivateContributions)
217218
 
218219
 	counts := map[string]int{}
219220
 	reposWithMonthActivity := map[int64]struct{}{}
@@ -284,20 +285,21 @@ func (h *Handlers) contributionCalendar(ctx context.Context, user usersdb.User,
284285
 		weeks = append(weeks, week)
285286
 	}
286287
 	return contributionCalendar{
287
-		Total:             total,
288
-		Period:            period,
289
-		Weeks:             weeks,
290
-		Years:             contributionYears(user.Username, currentYear, selectedYear),
291
-		CurrentYear:       currentYear,
292
-		SelectedYear:      selectedYear,
293
-		MonthLabel:        activityMonth.Format("January 2006"),
294
-		MonthCommitCount:  monthCommitCount,
295
-		MonthRepoCount:    len(reposWithMonthActivity),
296
-		HasRepositoryData: h.d.RepoFS != nil && len(repos) > 0,
288
+		Total:                       total,
289
+		Period:                      period,
290
+		Weeks:                       weeks,
291
+		Years:                       contributionYears(user.Username, currentYear, selectedYear),
292
+		CurrentYear:                 currentYear,
293
+		SelectedYear:                selectedYear,
294
+		MonthLabel:                  activityMonth.Format("January 2006"),
295
+		MonthCommitCount:            monthCommitCount,
296
+		MonthRepoCount:              len(reposWithMonthActivity),
297
+		HasRepositoryData:           h.d.RepoFS != nil && len(repos) > 0,
298
+		IncludePrivateContributions: user.IncludePrivateContributions,
297299
 	}
298300
 }
299301
 
300
-func (h *Handlers) profileContributionRepos(ctx context.Context, user usersdb.User, viewer middleware.CurrentUser) []profileContributionRepo {
302
+func (h *Handlers) profileContributionRepos(ctx context.Context, user usersdb.User, viewer middleware.CurrentUser, includePrivate bool) []profileContributionRepo {
301303
 	actor := policy.AnonymousActor()
302304
 	if !viewer.IsAnonymous() {
303305
 		actor = viewer.PolicyActor()
@@ -306,15 +308,22 @@ func (h *Handlers) profileContributionRepos(ctx context.Context, user usersdb.Us
306308
 	queries := reposdb.New()
307309
 	seen := map[int64]struct{}{}
308310
 	out := make([]profileContributionRepo, 0, 64)
309
-	add := func(ownerSlug string, repo reposdb.Repo, allowIdentityFallback bool) {
311
+	add := func(ownerSlug string, repo reposdb.Repo, allowIdentityFallback, affiliated bool) {
310312
 		if ownerSlug == "" {
311313
 			return
312314
 		}
313315
 		if _, ok := seen[repo.ID]; ok {
314316
 			return
315317
 		}
316
-		if !policy.IsVisibleTo(ctx, deps, actor, policy.NewRepoRefFromRepo(repo)) {
317
-			return
318
+		repoRef := policy.NewRepoRefFromRepo(repo)
319
+		if repoRef.IsPrivate() {
320
+			if !includePrivate || !affiliated {
321
+				return
322
+			}
323
+		} else {
324
+			if !policy.IsVisibleTo(ctx, deps, actor, repoRef) {
325
+				return
326
+			}
318327
 		}
319328
 		seen[repo.ID] = struct{}{}
320329
 		out = append(out, profileContributionRepo{
@@ -329,7 +338,7 @@ func (h *Handlers) profileContributionRepos(ctx context.Context, user usersdb.Us
329338
 		h.d.Logger.WarnContext(ctx, "profile overview: contribution user repos", "user_id", user.ID, "error", err)
330339
 	} else {
331340
 		for _, repo := range userRepos {
332
-			add(user.Username, repo, true)
341
+			add(user.Username, repo, true, true)
333342
 		}
334343
 	}
335344
 
@@ -344,7 +353,7 @@ func (h *Handlers) profileContributionRepos(ctx context.Context, user usersdb.Us
344353
 				continue
345354
 			}
346355
 			for _, repo := range orgRepos {
347
-				add(org.Slug, repo, true)
356
+				add(org.Slug, repo, true, true)
348357
 			}
349358
 		}
350359
 	}
@@ -355,7 +364,7 @@ func (h *Handlers) profileContributionRepos(ctx context.Context, user usersdb.Us
355364
 		return out
356365
 	}
357366
 	for _, row := range publicRepos {
358
-		add(row.OwnerSlug, row.Repo, false)
367
+		add(row.OwnerSlug, row.Repo, false, false)
359368
 	}
360369
 	return out
361370
 }
internal/web/handlers/profile/profile.gomodified
@@ -77,6 +77,7 @@ func (h *Handlers) MountAvatars(r chi.Router) {
7777
 func (h *Handlers) MountProfile(r chi.Router) {
7878
 	r.Group(func(r chi.Router) {
7979
 		r.Use(middleware.RequireUser)
80
+		r.Post("/{username}/contribution-settings", h.contributionSettingsUpdate)
8081
 		r.Post("/{username}/pins", h.pinsUpdate)
8182
 	})
8283
 	r.Get("/{username}", h.serveProfile)
@@ -167,28 +168,30 @@ func (h *Handlers) serveProfile(w http.ResponseWriter, r *http.Request) {
167168
 		displayName = user.Username
168169
 	}
169170
 	data := map[string]any{
170
-		"Title":            displayName,
171
-		"User":             user,
172
-		"DisplayName":      displayName,
173
-		"IsSelf":           isSelf,
174
-		"AvatarURL":        avatarURL,
175
-		"OGTitle":          displayName + " (@" + user.Username + ")",
176
-		"OGDescription":    ogDescription(user),
177
-		"OGImage":          avatarURL,
178
-		"JoinedFormatted":  user.CreatedAt.Time.Format("January 2, 2006"),
179
-		"WebsiteSafe":      safeWebsite(user.Website),
180
-		"Tabs":             tabs,
181
-		"ActiveTab":        "overview",
182
-		"VisibleRepoCount": len(visibleRepos),
183
-		"Orgs":             h.profileOrganizations(r.Context(), user.ID),
184
-		"ProfileReadme":    readme,
185
-		"HasProfileReadme": hasReadme,
186
-		"Contributions":    h.contributionCalendar(r.Context(), user, viewer, r.URL.Query()),
187
-		"PinnedRepos":      pinnedRepos,
188
-		"PinCandidates":    pinCandidates,
189
-		"PinsRemaining":    profilePinsRemaining(pinCandidates),
190
-		"CanCustomizePins": isSelf,
191
-		"PinsAction":       "/" + url.PathEscape(user.Username) + "/pins",
171
+		"Title":                      displayName,
172
+		"User":                       user,
173
+		"DisplayName":                displayName,
174
+		"IsSelf":                     isSelf,
175
+		"AvatarURL":                  avatarURL,
176
+		"OGTitle":                    displayName + " (@" + user.Username + ")",
177
+		"OGDescription":              ogDescription(user),
178
+		"OGImage":                    avatarURL,
179
+		"JoinedFormatted":            user.CreatedAt.Time.Format("January 2, 2006"),
180
+		"WebsiteSafe":                safeWebsite(user.Website),
181
+		"Tabs":                       tabs,
182
+		"ActiveTab":                  "overview",
183
+		"VisibleRepoCount":           len(visibleRepos),
184
+		"Orgs":                       h.profileOrganizations(r.Context(), user.ID),
185
+		"ProfileReadme":              readme,
186
+		"HasProfileReadme":           hasReadme,
187
+		"Contributions":              h.contributionCalendar(r.Context(), user, viewer, r.URL.Query()),
188
+		"PinnedRepos":                pinnedRepos,
189
+		"PinCandidates":              pinCandidates,
190
+		"PinsRemaining":              profilePinsRemaining(pinCandidates),
191
+		"CanCustomizePins":           isSelf,
192
+		"PinsAction":                 "/" + url.PathEscape(user.Username) + "/pins",
193
+		"ContributionSettingsAction": "/" + url.PathEscape(user.Username) + "/contribution-settings",
194
+		"ContributionSettingsReturn": r.URL.RequestURI(),
192195
 	}
193196
 	w.Header().Set("Content-Type", "text/html; charset=utf-8")
194197
 	if err := h.d.Render.RenderPage(w, r, "profile/view", data); err != nil {
internal/web/handlers/profile/profile_test.gomodified
@@ -61,7 +61,7 @@ func setupProfileEnvWithDeps(t *testing.T, objectStore storage.ObjectStore, repo
6161
 	tmplFS := fstest.MapFS{
6262
 		"_layout.html":           {Data: []byte(`{{ define "layout" }}<html><head><title>{{ .Title }}</title></head><body>{{ template "page" . }}</body></html>{{ end }}`)},
6363
 		"hello.html":             {Data: []byte(`{{ define "page" }}home{{ end }}`)},
64
-		"profile/view.html":      {Data: []byte(`{{ define "page" }}USER={{.User.Username}} DISPLAY={{.User.DisplayName}}{{ if .IsSelf }} SELF=1{{ end }} BIO={{.User.Bio}} VISIBLE={{.VisibleRepoCount}} ORGS={{len .Orgs}} README={{.HasProfileReadme}} CONTRIB={{.Contributions.Total}} PERIOD={{.Contributions.Period}} WEEKS={{len .Contributions.Weeks}} YEARS={{len .Contributions.Years}} YEARLINKS={{range .Contributions.Years}}{{.Year}}:{{.Active}}:{{.Href}};{{end}} PINS={{len .PinnedRepos}} PINNAMES={{range .PinnedRepos}}{{.Name}};{{end}} CANDIDATES={{len .PinCandidates}} SELECTED={{range .PinCandidates}}{{if .IsPinned}}{{.Name}};{{end}}{{end}}{{ if .CanCustomizePins }} CUSTOMIZE=1{{ end }}{{ end }}`)},
64
+		"profile/view.html":      {Data: []byte(`{{ define "page" }}USER={{.User.Username}} DISPLAY={{.User.DisplayName}}{{ if .IsSelf }} SELF=1{{ end }} BIO={{.User.Bio}} VISIBLE={{.VisibleRepoCount}} ORGS={{len .Orgs}} README={{.HasProfileReadme}} CONTRIB={{.Contributions.Total}} PERIOD={{.Contributions.Period}} PRIVATE={{.Contributions.IncludePrivateContributions}} WEEKS={{len .Contributions.Weeks}} YEARS={{len .Contributions.Years}} YEARLINKS={{range .Contributions.Years}}{{.Year}}:{{.Active}}:{{.Href}};{{end}} PINS={{len .PinnedRepos}} PINNAMES={{range .PinnedRepos}}{{.Name}};{{end}} CANDIDATES={{len .PinCandidates}} SELECTED={{range .PinCandidates}}{{if .IsPinned}}{{.Name}};{{end}}{{end}}{{ if .CanCustomizePins }} CUSTOMIZE=1 ACTION={{.ContributionSettingsAction}} RETURN={{.ContributionSettingsReturn}}{{ end }}{{ end }}`)},
6565
 		"profile/suspended.html": {Data: []byte(`{{ define "page" }}SUSPENDED={{.Username}}{{ end }}`)},
6666
 		"orgs/profile.html":      {Data: []byte(`{{ define "page" }}ORG={{.Org.Slug}} REPOS={{len .Repos}} PINS={{len .PinnedRepos}} PINNAMES={{range .PinnedRepos}}{{.Name}};{{end}} CANDIDATES={{len .PinCandidates}} SELECTED={{range .PinCandidates}}{{if .IsPinned}}{{.Name}};{{end}}{{end}} MEMBERS={{.MemberCount}} PEOPLE={{len .People}} NAMES={{range .Repos}}{{.Name}};{{end}} LANGS={{range .TopLanguages}}{{.Name}}={{.Count}};{{end}} TOPICS={{range .TopTopics}}{{.Name}}={{.Count}};{{end}} VIEWAS={{.ViewAs}}{{ if .CanCustomizePins }} CUSTOMIZE=1{{ end }}{{ end }}`)},
6767
 		"orgs/repositories.html": {Data: []byte(`{{ define "page" }}ORGREPOS={{.Org.Slug}} ACTIVE={{.ActiveOrgNav}} TOTAL={{.RepoCount}} FILTERED={{.FilteredCount}} PAGE={{.Page}}/{{.PageCount}} TYPE={{.SelectedType}} LANG={{.SelectedLanguage}} SORT={{.SelectedSort}} PREV={{.PrevHref}} NEXT={{.NextHref}} NAMES={{range .Repos}}{{.Name}};{{end}}{{range .PaginationPages}} P{{.Number}}={{.Current}}{{end}}{{ end }}`)},
@@ -315,6 +315,31 @@ func (e *profileEnv) postPins(t *testing.T, path string, user usersdb.User, repo
315315
 	return resp
316316
 }
317317
 
318
+func (e *profileEnv) postContributionSettings(t *testing.T, path string, user usersdb.User, includePrivate bool, returnTo string) *http.Response {
319
+	t.Helper()
320
+	include := "0"
321
+	if includePrivate {
322
+		include = "1"
323
+	}
324
+	form := url.Values{
325
+		"include_private_contributions": {include},
326
+		"return_to":                     {returnTo},
327
+	}
328
+	req, err := http.NewRequest(http.MethodPost, e.srv.URL+path, strings.NewReader(form.Encode()))
329
+	if err != nil {
330
+		t.Fatalf("request: %v", err)
331
+	}
332
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
333
+	req.Header.Set("X-Test-User-ID", strconv.FormatInt(user.ID, 10))
334
+	req.Header.Set("X-Test-Username", user.Username)
335
+	resp, err := newNonRedirClient(t).Do(req)
336
+	if err != nil {
337
+		t.Fatalf("POST: %v", err)
338
+	}
339
+	t.Cleanup(func() { _ = resp.Body.Close() })
340
+	return resp
341
+}
342
+
318343
 // =============================== tests ==================================
319344
 
320345
 func TestProfile_RendersForExistingUser(t *testing.T) {
@@ -419,6 +444,73 @@ func TestProfile_ContributionsSelectedYearHasStableLinks(t *testing.T) {
419444
 	}
420445
 }
421446
 
447
+func TestProfile_PrivateContributionsRequireOwnerOptIn(t *testing.T) {
448
+	t.Parallel()
449
+	env := setupProfileEnvWithRepoFS(t)
450
+	alice := env.insertUser(t, "alice", "Alice Anderson", "")
451
+	env.insertVerifiedEmail(t, alice.ID, "alice@example.com")
452
+	env.insertUserRepo(t, alice.ID, "public-work", "visible work", "public", "Go", 0, 0)
453
+	env.insertUserRepo(t, alice.ID, "private-work", "private work", "private", "Go", 0, 0)
454
+
455
+	now := time.Now().UTC()
456
+	env.writeInitialCommit(t, "alice", "public-work", "Alice Anderson", "alice@example.com", now.AddDate(0, 0, -2))
457
+	env.writeInitialCommit(t, "alice", "private-work", "Alice Anderson", "alice@example.com", now.AddDate(0, 0, -1))
458
+
459
+	body := env.getAs(t, "/alice", alice)
460
+	for _, want := range []string{
461
+		"CONTRIB=1",
462
+		"PRIVATE=false",
463
+		"CUSTOMIZE=1 ACTION=/alice/contribution-settings RETURN=/alice",
464
+	} {
465
+		if !strings.Contains(body, want) {
466
+			t.Errorf("missing %q in body: %s", want, body)
467
+		}
468
+	}
469
+	if strings.Contains(body, "private-work") {
470
+		t.Fatalf("self profile leaked private repo name through contribution settings: %s", body)
471
+	}
472
+
473
+	currentYear := time.Now().UTC().Year()
474
+	returnTo := fmt.Sprintf("/alice?year=%d", currentYear)
475
+	resp := env.postContributionSettings(t, "/alice/contribution-settings", alice, true, returnTo)
476
+	if resp.StatusCode != http.StatusSeeOther {
477
+		t.Fatalf("status %d, want 303", resp.StatusCode)
478
+	}
479
+	if loc := resp.Header.Get("Location"); loc != returnTo {
480
+		t.Fatalf("Location = %q, want %q", loc, returnTo)
481
+	}
482
+
483
+	body = env.getAs(t, "/alice", usersdb.User{})
484
+	for _, want := range []string{
485
+		"CONTRIB=2",
486
+		"PRIVATE=true",
487
+	} {
488
+		if !strings.Contains(body, want) {
489
+			t.Errorf("missing %q in body: %s", want, body)
490
+		}
491
+	}
492
+	if strings.Contains(body, "private-work") {
493
+		t.Fatalf("anonymous profile leaked private repo name after private contribution opt-in: %s", body)
494
+	}
495
+}
496
+
497
+func TestProfile_ContributionSettingsRequireProfileOwner(t *testing.T) {
498
+	t.Parallel()
499
+	env := setupProfileEnv(t)
500
+	alice := env.insertUser(t, "alice", "Alice", "")
501
+	bob := env.insertUser(t, "bob", "Bob", "")
502
+
503
+	resp := env.postContributionSettings(t, "/alice/contribution-settings", bob, true, "/alice")
504
+	if resp.StatusCode != http.StatusForbidden {
505
+		t.Fatalf("status %d, want 403", resp.StatusCode)
506
+	}
507
+
508
+	body := env.getAs(t, "/alice", alice)
509
+	if !strings.Contains(body, "PRIVATE=false") {
510
+		t.Fatalf("unexpected settings change by non-owner: %s", body)
511
+	}
512
+}
513
+
422514
 func TestProfile_UnknownUser404(t *testing.T) {
423515
 	t.Parallel()
424516
 	env := setupProfileEnv(t)
internal/web/render/octicons.gomodified
@@ -59,6 +59,8 @@ func BuiltinOcticons() OcticonResolver {
5959
 			`><path d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.75.75 0 0 1 1.06-1.06l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"/></svg>`),
6060
 		"checklist": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
6161
 			`><path d="M2.5 1.75v11.5c0 .138.112.25.25.25h3.17a.75.75 0 0 1 0 1.5H2.75A1.75 1.75 0 0 1 1 13.25V1.75C1 .784 1.784 0 2.75 0h8.5C12.216 0 13 .784 13 1.75v7.736a.75.75 0 0 1-1.5 0V1.75a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13.274 9.537-4.557 4.45a.75.75 0 0 1-1.055-.008l-1.943-1.95a.75.75 0 0 1 1.062-1.058l1.419 1.425 4.026-3.932a.75.75 0 1 1 1.048 1.074ZM4.75 4h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM4 7.75A.75.75 0 0 1 4.75 7h2a.75.75 0 0 1 0 1.5h-2A.75.75 0 0 1 4 7.75Z"/></svg>`),
62
+		"check": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
63
+			`><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/></svg>`),
6264
 		"check-circle": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
6365
 			`><path d="M8 1.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm11.03-1.78a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.97 8.78a.75.75 0 0 1 1.06-1.06l1.22 1.22 2.72-2.72a.75.75 0 0 1 1.06 0Z"/></svg>`),
6466
 		"check-circle-fill": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
internal/web/static/css/shithub.cssmodified
@@ -1218,25 +1218,58 @@ code {
12181218
   right: 0;
12191219
   top: calc(100% + 0.45rem);
12201220
   width: min(360px, calc(100vw - 2rem));
1221
-  padding: 1rem;
1221
+  padding: 0.75rem 0;
12221222
   border: 1px solid var(--border-default);
12231223
   border-radius: 12px;
12241224
   background: var(--canvas-overlay, var(--canvas-default));
12251225
   color: var(--fg-default);
12261226
   box-shadow: 0 16px 32px rgba(1,4,9,0.45);
12271227
 }
1228
-.shithub-profile-contrib-settings strong {
1228
+.shithub-contrib-settings-menu form {
1229
+  margin: 0;
1230
+}
1231
+.shithub-contrib-setting-item {
1232
+  display: grid;
1233
+  grid-template-columns: 24px minmax(0, 1fr);
1234
+  gap: 0.5rem;
1235
+  width: 100%;
1236
+  padding: 0.35rem 1rem;
1237
+  border: 0;
1238
+  background: transparent;
1239
+  color: var(--fg-default);
1240
+  font: inherit;
1241
+  text-align: left;
1242
+}
1243
+button.shithub-contrib-setting-item {
1244
+  cursor: pointer;
1245
+}
1246
+button.shithub-contrib-setting-item:hover {
1247
+  background: var(--canvas-subtle);
1248
+}
1249
+.shithub-contrib-setting-item.is-static {
1250
+  cursor: default;
1251
+}
1252
+.shithub-contrib-setting-check {
1253
+  display: flex;
1254
+  align-items: flex-start;
1255
+  justify-content: center;
1256
+  padding-top: 0.1rem;
1257
+  color: var(--fg-default);
1258
+}
1259
+.shithub-contrib-setting-check svg {
1260
+  width: 16px;
1261
+  height: 16px;
1262
+}
1263
+.shithub-contrib-setting-item strong {
12291264
   display: block;
1230
-  margin-bottom: 0.35rem;
1265
+  margin: 0 0 0.45rem;
1266
+  color: var(--fg-default);
12311267
 }
1232
-.shithub-profile-contrib-settings p {
1233
-  margin: 0 0 1rem;
1268
+.shithub-contrib-setting-item span span {
1269
+  display: block;
12341270
   color: var(--fg-muted);
12351271
   line-height: 1.45;
12361272
 }
1237
-.shithub-profile-contrib-settings p:last-child {
1238
-  margin-bottom: 0;
1239
-}
12401273
 .shithub-profile-contrib-layout {
12411274
   display: grid;
12421275
   grid-template-columns: minmax(0, 1fr) 112px;
@@ -1317,7 +1350,7 @@ code {
13171350
   width: 0;
13181351
   height: 0;
13191352
   border: 5px solid transparent;
1320
-  border-top-color: var(--fg-muted);
1353
+  border-top-color: #6e7681;
13211354
   transform: translateX(-50%);
13221355
   pointer-events: none;
13231356
 }
@@ -1331,7 +1364,7 @@ code {
13311364
   max-width: min(260px, 80vw);
13321365
   padding: 0.35rem 0.55rem;
13331366
   border-radius: 6px;
1334
-  background: var(--fg-muted);
1367
+  background: #6e7681;
13351368
   color: #fff;
13361369
   box-shadow: 0 8px 18px rgba(1,4,9,0.35);
13371370
   font-size: 0.75rem;
internal/web/templates/profile/view.htmlmodified
@@ -101,15 +101,32 @@
101101
       <section class="shithub-profile-contributions" aria-labelledby="contrib-h">
102102
         <div class="shithub-profile-contrib-head">
103103
           <h2 id="contrib-h">{{ .Contributions.Total }} contribution{{ pluralize .Contributions.Total "" "s" }} {{ .Contributions.Period }}</h2>
104
+          {{ if .IsSelf }}
104105
           <details class="shithub-profile-contrib-settings">
105106
             <summary>Contribution settings {{ octicon "triangle-down" }}</summary>
106
-            <div>
107
-              <strong>Private contributions</strong>
108
-              <p>Private contribution counts are shown only when those repositories are visible to you.</p>
109
-              <strong>Activity overview</strong>
110
-              <p>Activity is summarized from visible repositories on this shithub instance.</p>
107
+            <div class="shithub-contrib-settings-menu">
108
+              <form method="post" action="{{ .ContributionSettingsAction }}">
109
+                <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
110
+                <input type="hidden" name="return_to" value="{{ .ContributionSettingsReturn }}">
111
+                <input type="hidden" name="include_private_contributions" value="{{ if .Contributions.IncludePrivateContributions }}0{{ else }}1{{ end }}">
112
+                <button type="submit" class="shithub-contrib-setting-item" aria-pressed="{{ .Contributions.IncludePrivateContributions }}">
113
+                  <span class="shithub-contrib-setting-check" aria-hidden="true">{{ if .Contributions.IncludePrivateContributions }}{{ octicon "check" }}{{ end }}</span>
114
+                  <span>
115
+                    <strong>Private contributions</strong>
116
+                    <span>{{ if .Contributions.IncludePrivateContributions }}Turning off private contributions will show only public activity on your profile.{{ else }}Turning on private contributions will show private activity counts on your profile.{{ end }}</span>
117
+                  </span>
118
+                </button>
119
+              </form>
120
+              <div class="shithub-contrib-setting-item is-static">
121
+                <span class="shithub-contrib-setting-check" aria-hidden="true"></span>
122
+                <span>
123
+                  <strong>Activity overview</strong>
124
+                  <span>Turning on the activity overview will show an overview of your activity across organizations and repositories.</span>
125
+                </span>
126
+              </div>
111127
             </div>
112128
           </details>
129
+          {{ end }}
113130
         </div>
114131
 
115132
         <div class="shithub-profile-contrib-layout">
@@ -125,7 +142,7 @@
125142
                 {{ range .Contributions.Weeks }}
126143
                 <div class="shithub-contrib-week">
127144
                   {{ range .Days }}
128
-                  <span class="shithub-contrib-day level-{{ .Level }}{{ if .IsFuture }} is-future{{ end }}{{ if not .IsInWindow }} is-outside{{ end }}" title="{{ .Title }}" data-title="{{ .Title }}" data-date="{{ .Date }}" data-count="{{ .Count }}" aria-label="{{ .Title }}" tabindex="0"></span>
145
+                  <span class="shithub-contrib-day level-{{ .Level }}{{ if .IsFuture }} is-future{{ end }}{{ if not .IsInWindow }} is-outside{{ end }}" data-title="{{ .Title }}" data-date="{{ .Date }}" data-count="{{ .Count }}" aria-label="{{ .Title }}" tabindex="0"></span>
129146
                   {{ end }}
130147
                 </div>
131148
                 {{ end }}
internal/webhook/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {
internal/worker/sqlc/models.gomodified
@@ -2099,29 +2099,30 @@ type TransactionalEmailLog struct {
20992099
 }
21002100
 
21012101
 type User struct {
2102
-	ID                int64
2103
-	Username          string
2104
-	DisplayName       string
2105
-	PrimaryEmailID    pgtype.Int8
2106
-	PasswordHash      string
2107
-	PasswordAlgo      string
2108
-	PasswordUpdatedAt pgtype.Timestamptz
2109
-	EmailVerified     bool
2110
-	LastLoginAt       pgtype.Timestamptz
2111
-	SuspendedAt       pgtype.Timestamptz
2112
-	SuspendedReason   pgtype.Text
2113
-	DeletedAt         pgtype.Timestamptz
2114
-	CreatedAt         pgtype.Timestamptz
2115
-	UpdatedAt         pgtype.Timestamptz
2116
-	Bio               string
2117
-	Location          string
2118
-	Website           string
2119
-	Company           string
2120
-	Pronouns          string
2121
-	AvatarObjectKey   pgtype.Text
2122
-	Theme             string
2123
-	SessionEpoch      int32
2124
-	IsSiteAdmin       bool
2102
+	ID                          int64
2103
+	Username                    string
2104
+	DisplayName                 string
2105
+	PrimaryEmailID              pgtype.Int8
2106
+	PasswordHash                string
2107
+	PasswordAlgo                string
2108
+	PasswordUpdatedAt           pgtype.Timestamptz
2109
+	EmailVerified               bool
2110
+	LastLoginAt                 pgtype.Timestamptz
2111
+	SuspendedAt                 pgtype.Timestamptz
2112
+	SuspendedReason             pgtype.Text
2113
+	DeletedAt                   pgtype.Timestamptz
2114
+	CreatedAt                   pgtype.Timestamptz
2115
+	UpdatedAt                   pgtype.Timestamptz
2116
+	Bio                         string
2117
+	Location                    string
2118
+	Website                     string
2119
+	Company                     string
2120
+	Pronouns                    string
2121
+	AvatarObjectKey             pgtype.Text
2122
+	Theme                       string
2123
+	SessionEpoch                int32
2124
+	IsSiteAdmin                 bool
2125
+	IncludePrivateContributions bool
21252126
 }
21262127
 
21272128
 type UserEmail struct {