Compare commits

...

37 Commits

Author SHA1 Message Date
GitCaddy
15bd1d61c4 feat(api): Add v2 runner status API with AJAX polling
Some checks failed
Build and Release / Lint (push) Failing after 2m13s
Build and Release / Unit Tests (push) Successful in 2m48s
Build and Release / Create Release (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m0s
- Add /api/v2/repos/{owner}/{repo}/actions/runners/status endpoint
- Add /api/v2/repos/{owner}/{repo}/actions/runners/{id}/status endpoint
- Add internal status JSON endpoint for admin panel polling
- Add JavaScript polling (10s interval) to runner edit page
- Status tiles now auto-update: online/offline, disk, bandwidth

🤖 Generated with Claude Code
2026-01-11 17:36:44 +00:00
GitCaddy
b569c3f8a8 fix(runners): Show Connected when online, Last seen only when offline
Some checks failed
Build and Release / Lint (push) Failing after 2m4s
Build and Release / Unit Tests (push) Successful in 2m55s
Build and Release / Create Release (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m1s
2026-01-11 17:29:34 +00:00
GitCaddy
ded40c34c5 feat(runners): Add suggested labels and label management
Some checks failed
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Create Release (push) Has been skipped
Build and Release / Lint (push) Failing after 3s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 51s
Build and Release / Unit Tests (push) Has been cancelled
- Add DistroInfo struct to parse Linux distribution from capabilities
- Add runner label management endpoints (add/remove/use-suggested)
- Update runner edit UI with:
  - Clickable labels with X to remove
  - Suggested labels with + to add individually
  - Use All Suggested Labels button
  - Buttons moved to full-width row below columns
- Suggested labels derived from OS and distro (linux, linux-latest, debian, debian-latest, etc)

🤖 Generated with Claude Code
2026-01-11 17:25:01 +00:00
GitCaddy
e53c8fd040 feat: add Check Now button for on-demand bandwidth testing
Some checks failed
Build and Release / Lint (push) Failing after 2m5s
Build and Release / Unit Tests (push) Successful in 2m45s
Build and Release / Create Release (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m0s
- Add BandwidthTestRequestedAt field to ActionRunner model
- Update to actions-proto-go v0.5.7 with RequestBandwidthTest field
- Add RunnerRequestBandwidthTest handler and route
- Update FetchTask to check and return bandwidth test request flag
- Add Check Now button to runner capabilities panel
- Add locale strings for bandwidth test feature

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-11 15:38:01 +00:00
GitCaddy
a3c1aa3011 feat: add bandwidth display to runner capabilities panel
Some checks failed
Build and Release / Lint (push) Failing after 2m0s
Build and Release / Unit Tests (push) Successful in 2m52s
Build and Release / Create Release (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m2s
- Add BandwidthInfo struct for bandwidth test results
- Display download speed and latency in runner edit page
- Show when the bandwidth test was last performed
- Add locale string for bandwidth label

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-11 07:45:39 +00:00
GitCaddy
469551095b UI: Split runner edit page with capabilities panel on right
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m22s
Build and Release / Lint (push) Failing after 2m48s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
- Two-column layout: basic info left, capabilities right
- Task list remains full-width below
- Vertical layout for capabilities (better for future expansion)
- Shows Docker availability status
2026-01-11 07:22:13 +00:00
GitCaddy
5ca3661c33 Store runner capabilities from FetchTask poll
Some checks failed
Build and Release / Lint (push) Failing after 28s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m7s
Build and Release / Create Release (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m32s
- Update FetchTask handler to store capabilities_json from request
- Use GitCaddy actions-proto-go v0.5.6 with CapabilitiesJson field
- Capabilities include disk space, tools, docker, and limitations
2026-01-11 07:04:44 +00:00
GitCaddy
a68d691750 Add disk space display to runner capabilities page
Some checks failed
Build and Release / Lint (push) Failing after 2m2s
Build and Release / Unit Tests (push) Successful in 2m56s
Build and Release / Create Release (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m3s
- Add DiskInfo struct to RunnerCapability for disk space data
- Add disk space progress bar with color coding (green/yellow/red)
- Add Int64ToFloat64 and DivideFloat64 template helper functions
- Add locale strings for disk space labels and warnings
- Show disk usage percentage, free space, and total space
- Display warning icons when disk usage is high (85%+) or critical (95%+)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-11 05:26:03 +00:00
3a1075d6a0 fix: release page improvements and dark mode dropzone
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m43s
Build and Release / Unit Tests (push) Successful in 2m3s
Build and Release / Lint (push) Failing after 2m3s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
- Latest release downloads always expanded (no collapsible)
- Show archived checkbox moved to right of Release/Tag toggle
- 8px top padding for OS group sections
- Improved OS detection patterns for zip files
- Fixed dark mode dropzone styling
- Renamed slice to newSlice to avoid conflict with Go builtin

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-11 04:43:36 +00:00
6795122e00 feat: group release downloads by OS, load primary language for pinned repos
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m32s
Build and Release / Lint (push) Failing after 1m54s
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 1m59s
- Add ToLower to StringUtils template helper
- Add slice and Append template functions for grouping
- Group release attachments by OS (Windows, macOS, Linux, Other)
- Load primary language for org pinned repos to show in cards

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-11 04:27:55 +00:00
2fc3e5a1c7 fix: stats 2x2 grid with inline style, user profile pinned repos vertical layout
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m33s
Build and Release / Lint (push) Failing after 1m52s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 1m59s
2026-01-11 04:00:23 +00:00
1af82412c0 feat: auto-create .profile repo with README and redirect to edit
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Lint (push) Failing after 3s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 48s
Build and Release / Unit Tests (push) Successful in 2m4s
2026-01-11 03:57:14 +00:00
5832d93f0a fix: hide Actions/Activity for non-contributors, hide PRs/Projects for non-logged-in users
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m34s
Build and Release / Lint (push) Failing after 1m53s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m5s
2026-01-11 03:49:30 +00:00
44f04a7866 fix: stats section with bordered blocks in 2x2 grid
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m44s
Build and Release / Unit Tests (push) Successful in 1m57s
Build and Release / Lint (push) Failing after 2m2s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
2026-01-11 03:43:49 +00:00
2ba34c0abb fix: pinned repos vertical layout with icon on top, small icons for recent activity
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m34s
Build and Release / Lint (push) Failing after 1m53s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m4s
2026-01-11 03:40:18 +00:00
1717a0c45c fix: small icons for recent activity, hide private repos from non-members
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m42s
Build and Release / Lint (push) Failing after 1m59s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m1s
2026-01-11 03:32:29 +00:00
e871e65342 fix: restore .Name in Teams section
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m39s
Build and Release / Lint (push) Failing after 2m2s
Build and Release / Unit Tests (push) Successful in 2m3s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
2026-01-11 03:28:36 +00:00
8b8812f81c fix: remove double .Repo reference in pinned repos template
Some checks failed
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m35s
Build and Release / Lint (push) Has been cancelled
Build and Release / Unit Tests (push) Has been cancelled
2026-01-11 03:26:49 +00:00
67ff066157 feat: show last commit message in org recent activity section
Some checks failed
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Has been cancelled
Build and Release / Lint (push) Has been cancelled
Build and Release / Integration Tests (PostgreSQL) (push) Has been cancelled
2026-01-11 03:25:30 +00:00
3fb751bc24 fix: RelAvatarLink ctx arg, GitCaddy branding in error message
Some checks failed
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m3s
Build and Release / Create Release (push) Has been skipped
Build and Release / Lint (push) Failing after 1m31s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m9s
2026-01-11 03:15:33 +00:00
6cfd51e4c7 fix: use DateUtils functions in org home template
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m39s
Build and Release / Lint (push) Failing after 2m0s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m0s
2026-01-11 03:10:47 +00:00
659e08da6c fix: user profile nil pointer and error page URL
- Fix nil pointer on user profile when Repo.Owner is not loaded
- Change 500 error page GitHub URL to git.marketally.com/gitcaddy/gitea

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 02:56:14 +00:00
d664ce29d8 feat: improve org overview - stats layout, repo icons, recent activity
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m33s
Build and Release / Lint (push) Failing after 1m54s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m5s
- Rename Activity to Stats in sidebar with better flex layout
- Add repo avatars/icons to pinned repos (Featured Projects)
- Add Recent Activity section showing 10 most recently updated repos
- Show repo description, language, and time since update

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 02:51:34 +00:00
4580e5c87f fix: org overview layout - move public members to sidebar, fix stats
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m33s
Build and Release / Lint (push) Failing after 1m54s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m5s
- Move public members section to right sidebar for non-members
- Members see internal members + teams in sidebar
- Non-members see public members in sidebar
- Fix Activity stats formatting with 2x2 grid in sidebar
- Remove duplicate public members section from main content

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 02:44:00 +00:00
11b2ee48e9 fix: add IsOrganizationMember check for pin to org dropdown
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m33s
Build and Release / Lint (push) Failing after 1m53s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m0s
The pin dropdown now correctly shows the Pin to Organization option
when viewing a repo owned by an org where the user is a member.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 02:34:34 +00:00
85d73a2d85 fix: org overview stats field names and add star count
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m41s
Build and Release / Lint (push) Failing after 2m0s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m1s
- Rename OrgOverviewStats fields to match template expectations
- Add TotalStars field to show aggregate star count
- Add CountOrgRepoStars function to repo model
- Fix API struct and handler to use new field names

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 02:30:54 +00:00
54510ce582 feat: add activity heatmap on profile overview
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m36s
Build and Release / Lint (push) Failing after 1m55s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m4s
- Add ShowHeatmapOnProfile field to user model
- Add checkbox in user settings under privacy section
- Display heatmap on profile overview page when enabled
- Users can now show their contribution activity on their profile

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 02:22:40 +00:00
1986d90df0 feat: Pin repos to user profile or organization
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m33s
Build and Release / Lint (push) Failing after 1m54s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m4s
- Add UserPinnedRepo model for pinning repos to user profiles
- Add Pin dropdown in repo header with options for profile/org
- Add pin/unpin routes and handlers
- Update user profile to show pinned repos with nice cards
- User overview tab always visible (like org overview)
- Shows empty state with instructions when no pinned repos
- Limit of 6 pinned repos per user
- Org members can pin repos to organization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 02:09:07 +00:00
5b0442d357 feat(org): Always show Overview tab with empty states
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Lint (push) Failing after 3s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 48s
Build and Release / Unit Tests (push) Successful in 2m4s
- Overview tab now always visible for organizations
- Added nice empty state for pinned repos with setup instructions
- Added empty state for public members section
- Added empty state for profile README with create button
- Added organization activity stats section
- Overview is now the default tab (not repositories)
- Added 10 new locale strings for empty states

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 01:56:20 +00:00
d44fea18d5 feat(pages): Route custom domains to landing page
Some checks failed
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Create Release (push) Has been skipped
Build and Release / Lint (push) Failing after 3s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 48s
Build and Release / Unit Tests (push) Successful in 2m4s
Custom domains configured in Pages settings now route directly
to the repository landing page, in addition to subdomain routing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 00:55:11 +00:00
e57b4f1654 feat(pages): Standalone templates without Gitea navbar
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m40s
Build and Release / Unit Tests (push) Successful in 1m53s
Build and Release / Lint (push) Failing after 1m59s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
- Add base_head.tmpl with standalone HTML/CSS (no Gitea navbar)
- Add base_footer.tmpl for clean HTML closing
- Update simple.tmpl: clean minimal design with hero and stats
- Update documentation.tmpl: dark header, sidebar navigation
- Update product.tmpl: gradient hero, features grid, marketing style
- Update portfolio.tmpl: dark theme, gallery grid with hover effects

Pages now render as standalone landing pages without Gitea UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 00:50:58 +00:00
69d7c72ba8 feat(pages): Add subdomain routing and default config support
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m32s
Build and Release / Lint (push) Failing after 1m53s
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 1m58s
- Update subdomain parser to use {repo}-{owner}.{domain} format
- Add middleware to intercept Pages subdomain requests
- Generate default config when Pages enabled but no .gitea/landing.yaml
- Pages are public landing pages (accessible even for private repos)

🤖 Generated with Claude Code
2026-01-11 00:15:21 +00:00
Claude Code
919746c756 fix: Use repo-owner format for Pages subdomain URL
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m34s
Build and Release / Lint (push) Failing after 1m52s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m3s
- Format: {repo}-{owner}.{domain} (e.g., gitcaddy-gitcaddy.git-test.marketally.com)
- Simpler and unique URL structure
2026-01-11 00:51:31 +01:00
Claude Code
853ff29ae2 fix: Make Pages subdomain URL clickable and display proper domain
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m31s
Build and Release / Lint (push) Failing after 1m54s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 1m59s
- Show full URL instead of just subdomain
- Make it a clickable hyperlink with target=_blank
- Use setting.Domain for proper URL construction
2026-01-11 00:45:29 +01:00
Claude Code
7292421334 feat: Add SSL external option for Pages custom domains
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m37s
Build and Release / Unit Tests (push) Successful in 1m54s
Build and Release / Lint (push) Failing after 1m58s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
- Add checkbox to mark SSL as handled externally (e.g., Cloudflare)
- Add Activate SSL button for verified domains with pending SSL
- Add SSLExternal option to API
- Useful when using CDN/reverse proxy that handles SSL certificates
2026-01-11 00:40:44 +01:00
Claude Code
84adad19bf fix: Use StringUtils.HasPrefix instead of hasPrefix in product template
All checks were successful
Build and Release / Lint (push) Successful in 2m45s
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 58s
Build and Release / Build Binaries (amd64, linux) (push) Successful in 1m8s
Build and Release / Build Binaries (amd64, darwin) (push) Successful in 1m16s
Build and Release / Unit Tests (push) Successful in 2m27s
Build and Release / Build Binaries (amd64, windows) (push) Successful in 1m6s
Build and Release / Build Binaries (arm64, darwin) (push) Successful in 1m2s
Build and Release / Build Binaries (arm64, linux) (push) Successful in 55s
2026-01-11 00:01:42 +01:00
Claude Code
01c9563d1d fix: Add missing translation for cleanup_expired_upload_sessions cron task
All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 51s
Build and Release / Lint (push) Successful in 2m34s
Build and Release / Build Binaries (amd64, linux) (push) Successful in 1m6s
Build and Release / Build Binaries (amd64, windows) (push) Successful in 1m5s
Build and Release / Build Binaries (amd64, darwin) (push) Successful in 1m11s
Build and Release / Unit Tests (push) Successful in 2m17s
Build and Release / Build Binaries (arm64, darwin) (push) Successful in 58s
Build and Release / Build Binaries (arm64, linux) (push) Successful in 1m1s
2026-01-10 23:49:26 +01:00
52 changed files with 6310 additions and 4100 deletions

4
go.mod
View File

@ -25,6 +25,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
github.com/Masterminds/semver/v3 v3.4.0
github.com/ProtonMail/go-crypto v1.3.0
github.com/PuerkitoBio/goquery v1.10.3
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
@ -145,7 +146,6 @@ require (
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/DataDog/zstd v1.5.7 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/RoaringBitmap/roaring/v2 v2.10.0 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
@ -314,7 +314,7 @@ replace github.com/nektos/act => gitea.com/gitea/act v0.261.7-0.20251003180512-a
replace git.sr.ht/~mariusor/go-xsd-duration => gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078
// Use GitCaddy fork with capability support
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.3
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.7
exclude github.com/gofrs/uuid v3.2.0+incompatible

6
go.sum
View File

@ -29,8 +29,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.marketally.com/gitcaddy/actions-proto-go v0.5.3 h1:tf625YKv1Bykxr9CIcoqilC2MWiO/yBN3srlJYnFQqM=
git.marketally.com/gitcaddy/actions-proto-go v0.5.3/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
git.marketally.com/gitcaddy/actions-proto-go v0.5.7 h1:RUbafr3Vkw2l4WfSwa+oF+Ihakbm05W0FlAmXuQrDJc=
git.marketally.com/gitcaddy/actions-proto-go v0.5.7/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763 h1:ohdxegvslDEllZmRNDqpKun6L4Oq81jNdEDtGgHEV2c=
gitea.com/gitea/act v0.261.7-0.20251003180512-ac6e4b751763/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
gitea.com/gitea/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:BAFmdZpRW7zMQZQDClaCWobRj9uL1MR3MzpCVJvc5s4=
@ -78,8 +78,6 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=

View File

@ -64,6 +64,8 @@ type ActionRunner struct {
Ephemeral bool `xorm:"ephemeral NOT NULL DEFAULT false"`
// CapabilitiesJSON stores structured capability information for AI consumption
CapabilitiesJSON string `xorm:"TEXT"`
// BandwidthTestRequestedAt tracks when a bandwidth test was requested by admin
BandwidthTestRequestedAt timeutil.TimeStamp `xorm:"index"`
Created timeutil.TimeStamp `xorm:"created"`
Updated timeutil.TimeStamp `xorm:"updated"`

View File

@ -144,10 +144,10 @@ func GetMemberPublicVisibility(ctx context.Context, orgID, userID int64) (bool,
// OrgOverviewStats represents statistics for the organization overview
type OrgOverviewStats struct {
MemberCount int64
RepoCount int64
PublicRepoCount int64
TeamCount int64
TotalRepos int64
TotalMembers int64
TotalTeams int64
TotalStars int64
}
// GetOrgMemberAndTeamCounts returns member and team counts for an organization

View File

@ -142,6 +142,12 @@ func UpdatePagesDomain(ctx context.Context, domain *PagesDomain) error {
return err
}
// ActivatePagesDomainSSL sets SSL status to active for a domain
func ActivatePagesDomainSSL(ctx context.Context, id int64) error {
_, err := db.GetEngine(ctx).ID(id).Cols("ssl_status").Update(&PagesDomain{SSLStatus: SSLStatusActive})
return err
}
// DeletePagesDomain deletes a pages domain
func DeletePagesDomain(ctx context.Context, id int64) error {
_, err := db.GetEngine(ctx).ID(id).Delete(new(PagesDomain))

View File

@ -968,6 +968,17 @@ func CountNullArchivedRepository(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Count(new(Repository))
}
// CountOrgRepoStars returns the total number of stars across all repos owned by an organization
func CountOrgRepoStars(ctx context.Context, orgID int64) (int64, error) {
var total int64
_, err := db.GetEngine(ctx).
Table("repository").
Where("owner_id = ?", orgID).
Select("COALESCE(SUM(num_stars), 0)").
Get(&total)
return total, err
}
// FixNullArchivedRepository sets is_archived to false where it is null
func FixNullArchivedRepository(ctx context.Context) (int64, error) {
return db.GetEngine(ctx).Where(builder.IsNull{"is_archived"}).Cols("is_archived").NoAutoTime().Update(&Repository{

View File

@ -150,6 +150,7 @@ type User struct {
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
Theme string `xorm:"NOT NULL DEFAULT ''"`
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`
ShowHeatmapOnProfile bool `xorm:"NOT NULL DEFAULT false"`
}
// Meta defines the meta information of a user, to be stored in the K/V table

137
models/user/user_pinned.go Normal file
View File

@ -0,0 +1,137 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
)
// UserPinnedRepo represents a pinned repository for a user's profile
type UserPinnedRepo struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX NOT NULL"`
RepoID int64 `xorm:"INDEX NOT NULL"`
DisplayOrder int `xorm:"DEFAULT 0"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
Repo any `xorm:"-"` // Will be loaded by caller to avoid import cycle
}
// TableName returns the table name for UserPinnedRepo
func (p *UserPinnedRepo) TableName() string {
return "user_pinned_repo"
}
func init() {
db.RegisterModel(new(UserPinnedRepo))
}
// MaxUserPinnedRepos is the maximum number of repos a user can pin
const MaxUserPinnedRepos = 6
// GetUserPinnedRepos returns all pinned repos for a user
func GetUserPinnedRepos(ctx context.Context, userID int64) ([]*UserPinnedRepo, error) {
pinnedRepos := make([]*UserPinnedRepo, 0, MaxUserPinnedRepos)
err := db.GetEngine(ctx).
Where("user_id = ?", userID).
OrderBy("display_order ASC, id ASC").
Find(&pinnedRepos)
return pinnedRepos, err
}
// CountUserPinnedRepos returns the count of pinned repos for a user
func CountUserPinnedRepos(ctx context.Context, userID int64) (int64, error) {
return db.GetEngine(ctx).Where("user_id = ?", userID).Count(new(UserPinnedRepo))
}
// IsRepoPinnedByUser checks if a repo is pinned by a user
func IsRepoPinnedByUser(ctx context.Context, userID, repoID int64) (bool, error) {
return db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", userID, repoID).Exist(new(UserPinnedRepo))
}
// PinRepoToUser pins a repo to a user's profile
func PinRepoToUser(ctx context.Context, userID, repoID int64) error {
// Check if already pinned
exists, err := IsRepoPinnedByUser(ctx, userID, repoID)
if err != nil {
return err
}
if exists {
return nil // Already pinned
}
// Check max limit
count, err := CountUserPinnedRepos(ctx, userID)
if err != nil {
return err
}
if count >= MaxUserPinnedRepos {
return ErrUserPinnedRepoLimit{UserID: userID, Limit: MaxUserPinnedRepos}
}
// Get next display order
var maxOrder int
_, err = db.GetEngine(ctx).
Table("user_pinned_repo").
Where("user_id = ?", userID).
Select("COALESCE(MAX(display_order), 0)").
Get(&maxOrder)
if err != nil {
return err
}
pinnedRepo := &UserPinnedRepo{
UserID: userID,
RepoID: repoID,
DisplayOrder: maxOrder + 1,
}
_, err = db.GetEngine(ctx).Insert(pinnedRepo)
return err
}
// UnpinRepoFromUser unpins a repo from a user's profile
func UnpinRepoFromUser(ctx context.Context, userID, repoID int64) error {
_, err := db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", userID, repoID).Delete(new(UserPinnedRepo))
return err
}
// UpdateUserPinnedRepoOrder updates the display order of pinned repos
func UpdateUserPinnedRepoOrder(ctx context.Context, userID int64, repoIDs []int64) error {
for i, repoID := range repoIDs {
_, err := db.GetEngine(ctx).
Where("user_id = ? AND repo_id = ?", userID, repoID).
Cols("display_order").
Update(&UserPinnedRepo{DisplayOrder: i})
if err != nil {
return err
}
}
return nil
}
// DeleteUserPinnedReposByRepoID deletes all pins for a repo (when repo is deleted)
func DeleteUserPinnedReposByRepoID(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(UserPinnedRepo))
return err
}
// ErrUserPinnedRepoLimit represents an error when user has reached pin limit
type ErrUserPinnedRepoLimit struct {
UserID int64
Limit int
}
func (err ErrUserPinnedRepoLimit) Error() string {
return "user has reached the maximum number of pinned repositories"
}
// IsErrUserPinnedRepoLimit checks if error is ErrUserPinnedRepoLimit
func IsErrUserPinnedRepoLimit(err error) bool {
_, ok := err.(ErrUserPinnedRepoLimit)
return ok
}

View File

@ -3,10 +3,36 @@
package structs
import "time"
// BandwidthInfo holds network bandwidth test results
type BandwidthInfo struct {
DownloadMbps float64 `json:"download_mbps"`
UploadMbps float64 `json:"upload_mbps,omitempty"`
Latency float64 `json:"latency_ms,omitempty"`
TestedAt time.Time `json:"tested_at"`
}
// DiskInfo holds disk space information for a runner
type DiskInfo struct {
Total uint64 `json:"total_bytes"`
Free uint64 `json:"free_bytes"`
Used uint64 `json:"used_bytes"`
UsedPercent float64 `json:"used_percent"`
}
// DistroInfo holds Linux distribution information
type DistroInfo struct {
ID string `json:"id,omitempty"` // e.g., "ubuntu", "debian", "fedora"
VersionID string `json:"version_id,omitempty"` // e.g., "24.04", "12"
PrettyName string `json:"pretty_name,omitempty"` // e.g., "Ubuntu 24.04 LTS"
}
// RunnerCapability represents the detailed capabilities of a runner
type RunnerCapability struct {
OS string `json:"os"`
Arch string `json:"arch"`
Distro *DistroInfo `json:"distro,omitempty"`
Docker bool `json:"docker"`
DockerCompose bool `json:"docker_compose"`
ContainerRuntime string `json:"container_runtime,omitempty"`
@ -14,6 +40,9 @@ type RunnerCapability struct {
Tools map[string][]string `json:"tools,omitempty"`
Features *CapabilityFeatures `json:"features,omitempty"`
Limitations []string `json:"limitations,omitempty"`
Disk *DiskInfo `json:"disk,omitempty"`
Bandwidth *BandwidthInfo `json:"bandwidth,omitempty"`
SuggestedLabels []string `json:"suggested_labels,omitempty"`
}
// CapabilityFeatures represents feature support flags

View File

@ -179,10 +179,10 @@ type OrgOverview struct {
// OrgOverviewStats represents organization statistics
type OrgOverviewStats struct {
MemberCount int64 `json:"member_count"`
RepoCount int64 `json:"repo_count"`
PublicRepoCount int64 `json:"public_repo_count"`
TeamCount int64 `json:"team_count"`
TotalRepos int64 `json:"total_repos"`
TotalMembers int64 `json:"total_members"`
TotalTeams int64 `json:"total_teams"`
TotalStars int64 `json:"total_stars"`
}
// OrgProfileContent represents the organization profile content

View File

@ -38,6 +38,8 @@ type AddPagesDomainOption struct {
// The custom domain to add
// required: true
Domain string `json:"domain" binding:"Required"`
// Mark SSL as handled externally (e.g., by Cloudflare)
SSLExternal bool `json:"ssl_external"`
}
// PagesInfo represents the full pages information for a repository

View File

@ -48,6 +48,10 @@ func NewFuncMap() template.FuncMap {
// utils
"StringUtils": NewStringUtils,
"SliceUtils": NewSliceUtils,
"newSlice": func() []any { return []any{} },
"Append": func(s []any, v any) []any { return append(s, v) },
"Int64ToFloat64": func(i uint64) float64 { return float64(i) },
"DivideFloat64": func(a, b float64) float64 { if b == 0 { return 0 }; return a / b },
"JsonUtils": NewJsonUtils,
"DateUtils": NewDateUtils,

View File

@ -33,3 +33,17 @@ func (su *SliceUtils) Contains(s, v any) bool {
}
return false
}
// Append appends an element to a slice and returns the new slice
func (su *SliceUtils) Append(s any, v any) any {
if s == nil {
return []any{v}
}
sv := reflect.ValueOf(s)
if sv.Kind() != reflect.Slice {
panic(fmt.Sprintf("invalid type, expected slice, but got: %T", s))
}
// Create a new slice with the appended element
newSlice := reflect.Append(sv, reflect.ValueOf(v))
return newSlice.Interface()
}

View File

@ -61,6 +61,10 @@ func (su *StringUtils) ToUpper(s string) string {
return strings.ToUpper(s)
}
func (su *StringUtils) ToLower(s string) string {
return strings.ToLower(s)
}
func (su *StringUtils) TrimPrefix(s, prefix string) string {
return strings.TrimPrefix(s, prefix)
}

View File

@ -206,7 +206,7 @@
"filter.string.asc": "AZ",
"filter.string.desc": "ZA",
"error.occurred": "An error occurred",
"error.report_message": "If you believe that this is a Gitea bug, please search for issues on <a href=\"%s\" target=\"_blank\">GitHub</a> or open a new issue if necessary.",
"error.report_message": "If you believe that this is a GitCaddy bug, please search for issues on <a href=\"%s\" target=\"_blank\">GitCaddy Gitea</a> or open a new issue if necessary.",
"error.not_found": "The target couldn't be found.",
"error.network_error": "Network error",
"startpage.app_desc": "A painless, self-hosted Git service",
@ -2536,6 +2536,10 @@
"repo.settings.pages.pending": "Pending",
"repo.settings.pages.ssl_active": "Active",
"repo.settings.pages.ssl_pending": "Pending",
"repo.settings.pages.ssl_external": "SSL handled externally (e.g., Cloudflare)",
"repo.settings.pages.ssl_external_desc": "Check this if SSL is managed by a CDN or reverse proxy like Cloudflare",
"repo.settings.pages.activate_ssl": "Activate SSL",
"repo.settings.pages.ssl_activated": "SSL has been activated for this domain",
"repo.settings.pages.ssl_none": "None",
"repo.settings.pages.verify": "Verify",
"repo.settings.pages.verify_dns_hint": "Add the following TXT record to your DNS to verify domain ownership:",
@ -2937,6 +2941,7 @@
"admin.dashboard.cleanup_hook_task_table": "Clean up hook_task table",
"admin.dashboard.cleanup_packages": "Clean up expired packages",
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
"admin.dashboard.server_uptime": "Server Uptime",
"admin.dashboard.current_goroutine": "Current Goroutines",
"admin.dashboard.current_memory_usage": "Current Memory Usage",
@ -3703,6 +3708,15 @@
"actions.runners.capabilities.tools": "Tools",
"actions.runners.capabilities.limitations": "Limitations",
"actions.runners.capabilities.available": "Available",
"actions.runners.capabilities.disk": "Disk Space",
"actions.runners.capabilities.disk_free": "free",
"actions.runners.capabilities.disk_total": "total",
"actions.runners.capabilities.disk_warning": "Low disk space",
"actions.runners.capabilities.disk_critical": "Critical: disk almost full",
"actions.runners.capabilities.bandwidth": "Network Bandwidth",
"actions.runners.bandwidth_test_requested": "Bandwidth test requested. Results will appear on next poll.",
"actions.runners.bandwidth_test_request_failed": "Failed to request bandwidth test.",
"actions.runners.check_bandwidth_now": "Check Bandwidth",
"actions.runs.all_workflows": "All Workflows",
"actions.runs.commit": "Commit",
"actions.runs.scheduled": "Scheduled",
@ -3776,5 +3790,43 @@
"git.filemode.normal_file": "Normal file",
"git.filemode.executable_file": "Executable file",
"git.filemode.symbolic_link": "Symbolic link",
"git.filemode.submodule": "Submodule"
"git.filemode.submodule": "Submodule",
"org.pinned_repos_empty_title": "Showcase your best work",
"org.pinned_repos_empty_desc": "Pin up to 6 repositories to highlight your organization's most important projects.",
"org.settings.pinned.manage": "Manage Pins",
"org.settings.pinned.setup": "Set Up Pinned Repos",
"org.no_public_members": "No public members yet",
"org.profile_readme_empty_title": "Add a profile README",
"org.profile_readme_empty_desc": "Create a .profile repository with a README.md to introduce your organization.",
"org.create_profile_repo": "Create Profile Repository",
"org.activity": "Activity",
"org.repositories": "Repositories",
"repo.pin": "Pin",
"repo.pin.tooltip": "Pin this repository",
"repo.pin.pin_to_profile": "Pin to your profile",
"repo.pin.unpin_from_profile": "Unpin from profile",
"repo.pin.pin_to_org": "Pin to organization",
"repo.pin.unpin_from_org": "Unpin from organization",
"repo.pin.success_profile": "Repository pinned to your profile",
"repo.pin.success_org": "Repository pinned to organization",
"repo.pin.unpin_success_profile": "Repository unpinned from your profile",
"repo.pin.unpin_success_org": "Repository unpinned from organization",
"repo.pin.already_pinned_org": "Repository is already pinned",
"repo.pin.error_limit": "You have reached the maximum number of pinned repositories (6)",
"repo.pin.error_org_limit": "Organization has reached the maximum number of pinned repositories",
"repo.pin.error_not_org": "This repository does not belong to an organization",
"repo.pin.error_not_member": "You must be a member of the organization to pin repositories",
"repo.pin.error_generic": "Failed to update pin status",
"repo.pin.error_invalid_type": "Invalid pin type",
"user.pinned_repos": "Pinned Repositories",
"user.pinned_repos_hint": "Pin repos from the repo page",
"user.pinned_repos_empty_title": "No pinned repositories",
"user.pinned_repos_empty_desc": "Pin repositories to showcase your best work. Visit a repository and use the Pin dropdown.",
"settings.show_heatmap_on_profile": "Show activity heatmap on profile",
"settings.show_heatmap_on_profile_popup": "Display your contribution heatmap on your profile overview page",
"user.activity_heatmap": "Activity Heatmap",
"org.stats": "Stats",
"org.recent_activity": "Recent Activity",
"org.profile_repo_no_permission": "You do not have permission to create repositories in this organization.",
"org.profile_repo_create_failed": "Failed to create the profile repository."
}

View File

@ -119,6 +119,7 @@ func (s *Service) Declare(
runner.Version = req.Msg.Version
runner.CapabilitiesJSON = req.Msg.CapabilitiesJson
if err := actions_model.UpdateRunner(ctx, runner, "agent_labels", "version", "capabilities_json"); err != nil {
log.Error("Declare: failed to update runner %d: %v", runner.ID, err)
return nil, status.Errorf(codes.Internal, "update runner: %v", err)
}
@ -141,6 +142,15 @@ func (s *Service) FetchTask(
) (*connect.Response[runnerv1.FetchTaskResponse], error) {
runner := GetRunner(ctx)
// Update runner capabilities if provided
if capsJson := req.Msg.GetCapabilitiesJson(); capsJson != "" && capsJson != runner.CapabilitiesJSON {
runner.CapabilitiesJSON = capsJson
if err := actions_model.UpdateRunner(ctx, runner, "capabilities_json"); err != nil {
log.Warn("failed to update runner capabilities: %v", err)
// Don't return error, just log warning - capabilities update is not critical
}
}
var task *runnerv1.Task
tasksVersion := req.Msg.TasksVersion // task version from runner
latestVersion, err := actions_model.GetTasksVersionByScope(ctx, runner.OwnerID, runner.RepoID)
@ -167,9 +177,22 @@ func (s *Service) FetchTask(
task = t
}
}
// Check if admin requested a bandwidth test
requestBandwidthTest := false
if runner.BandwidthTestRequestedAt > 0 {
requestBandwidthTest = true
// Clear the request after sending
runner.BandwidthTestRequestedAt = 0
if err := actions_model.UpdateRunner(ctx, runner, "bandwidth_test_requested_at"); err != nil {
log.Warn("failed to clear bandwidth test request: %v", err)
}
}
res := connect.NewResponse(&runnerv1.FetchTaskResponse{
Task: task,
TasksVersion: latestVersion,
RequestBandwidthTest: requestBandwidthTest,
})
return res, nil
}

View File

@ -92,10 +92,10 @@ func GetOverview(ctx *context.APIContext) {
PublicMembers: apiPublicMembers,
TotalMembers: totalMembers,
Stats: &api.OrgOverviewStats{
MemberCount: stats.MemberCount,
RepoCount: stats.RepoCount,
PublicRepoCount: stats.PublicRepoCount,
TeamCount: stats.TeamCount,
TotalRepos: stats.TotalRepos,
TotalMembers: stats.TotalMembers,
TotalTeams: stats.TotalTeams,
TotalStars: stats.TotalStars,
},
}

View File

@ -219,7 +219,7 @@ func AddPagesDomain(ctx *context.APIContext) {
form := web.GetForm(ctx).(*api.AddPagesDomainOption)
domain, err := pages_service.AddPagesDomain(ctx, ctx.Repo.Repository.ID, form.Domain)
domain, err := pages_service.AddPagesDomain(ctx, ctx.Repo.Repository.ID, form.Domain, form.SSLExternal)
if err != nil {
if repo_model.IsErrPagesDomainAlreadyExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, "Domain already exists")

View File

@ -140,6 +140,8 @@ func Routes() *web.Router {
// Actions v2 API - AI-friendly runner capability discovery
m.Group("/repos/{owner}/{repo}/actions", func() {
m.Get("/runners/capabilities", repoAssignment(), GetActionsCapabilities)
m.Get("/runners/status", repoAssignment(), ListRunnersStatus)
m.Get("/runners/{runner_id}/status", repoAssignment(), GetRunnerStatus)
m.Post("/workflows/validate", repoAssignment(), reqToken(), web.Bind(api.WorkflowValidationRequest{}), ValidateWorkflow)
})

134
routers/api/v2/runners.go Normal file
View File

@ -0,0 +1,134 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v2
import (
"net/http"
"time"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/json"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/services/context"
)
// RunnerStatusResponse represents the runner status for API/polling
type RunnerStatusResponse struct {
ID int64 `json:"id"`
Name string `json:"name"`
IsOnline bool `json:"is_online"`
Status string `json:"status"`
Version string `json:"version"`
Labels []string `json:"labels"`
LastOnline *time.Time `json:"last_online,omitempty"`
Capabilities *api.RunnerCapability `json:"capabilities,omitempty"`
}
// GetRunnerStatus returns the current status of a runner
// @Summary Get runner status
// @Description Returns current runner status including online state, capabilities, disk, and bandwidth
// @Tags actions
// @Accept json
// @Produce json
// @Param owner path string true "owner of the repo"
// @Param repo path string true "name of the repo"
// @Param runner_id path int64 true "runner ID"
// @Success 200 {object} RunnerStatusResponse
// @Router /repos/{owner}/{repo}/actions/runners/{runner_id}/status [get]
func GetRunnerStatus(ctx *context.APIContext) {
runnerID := ctx.PathParamInt64("runner_id")
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
if err != nil {
ctx.APIErrorNotFound(err)
return
}
// Check access - runner must belong to this repo or be global
repo := ctx.Repo.Repository
if runner.RepoID != 0 && runner.RepoID != repo.ID {
ctx.APIErrorNotFound(nil)
return
}
response := buildRunnerStatusResponse(runner)
ctx.JSON(http.StatusOK, response)
}
// GetAdminRunnerStatus returns the current status of a runner (admin endpoint)
// @Summary Get runner status (admin)
// @Description Returns current runner status for admin panel AJAX polling
// @Tags admin
// @Accept json
// @Produce json
// @Param runner_id path int64 true "runner ID"
// @Success 200 {object} RunnerStatusResponse
// @Router /admin/actions/runners/{runner_id}/status [get]
func GetAdminRunnerStatus(ctx *context.APIContext) {
runnerID := ctx.PathParamInt64("runner_id")
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
if err != nil {
ctx.APIErrorNotFound(err)
return
}
response := buildRunnerStatusResponse(runner)
ctx.JSON(http.StatusOK, response)
}
// buildRunnerStatusResponse creates a status response from a runner
func buildRunnerStatusResponse(runner *actions_model.ActionRunner) *RunnerStatusResponse {
response := &RunnerStatusResponse{
ID: runner.ID,
Name: runner.Name,
IsOnline: runner.IsOnline(),
Status: runner.Status().String(),
Version: runner.Version,
Labels: runner.AgentLabels,
}
// Add last online time if available
if runner.LastOnline.AsTime().Unix() > 0 {
lastOnline := runner.LastOnline.AsTime()
response.LastOnline = &lastOnline
}
// Parse capabilities JSON if available
if runner.CapabilitiesJSON != "" {
var caps api.RunnerCapability
if err := json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps); err == nil {
response.Capabilities = &caps
}
}
return response
}
// ListRunnersStatus returns status for all runners accessible to the repo
// @Summary List runner statuses
// @Description Returns status for all runners available to the repository
// @Tags actions
// @Accept json
// @Produce json
// @Param owner path string true "owner of the repo"
// @Param repo path string true "name of the repo"
// @Success 200 {array} RunnerStatusResponse
// @Router /repos/{owner}/{repo}/actions/runners/status [get]
func ListRunnersStatus(ctx *context.APIContext) {
repo := ctx.Repo.Repository
runners, err := actions_model.GetRunnersOfRepo(ctx, repo.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
responses := make([]*RunnerStatusResponse, 0, len(runners))
for _, runner := range runners {
responses = append(responses, buildRunnerStatusResponse(runner))
}
ctx.JSON(http.StatusOK, responses)
}

View File

@ -17,11 +17,20 @@ import (
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
org_service "code.gitea.io/gitea/services/org"
repo_service "code.gitea.io/gitea/services/repository"
)
// RecentRepoActivity holds repo and its latest commit info
type RecentRepoActivity struct {
Repo *repo_model.Repository
CommitMessage string
CommitTime timeutil.TimeStamp
}
const tplOrgHome templates.TplName = "org/home"
@ -103,6 +112,44 @@ func home(ctx *context.Context, viewRepositories bool) {
ctx.Data["Teams"] = ctx.Org.Teams
ctx.Data["DisableNewPullMirrors"] = setting.Mirror.DisableNewPull
ctx.Data["ShowMemberAndTeamTab"] = ctx.Org.IsMember || len(members) > 0
ctx.Data["IsOrganizationMember"] = ctx.Org.IsMember
// Load recently updated repositories for activity section
// Only show private repos if user is signed in and is org member
showPrivate := ctx.IsSigned && ctx.Org.IsMember
recentRepos, _, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: 10,
Page: 1,
},
OwnerID: org.ID,
OrderBy: db.SearchOrderByRecentUpdated,
Private: showPrivate,
Actor: ctx.Doer,
})
if err != nil {
log.Error("SearchRepository for recent repos: %v", err)
} else {
// Load commit info for each repo
var recentActivity []*RecentRepoActivity
for _, repo := range recentRepos {
activity := &RecentRepoActivity{Repo: repo}
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
if err == nil {
commit, err := gitRepo.GetBranchCommit(repo.DefaultBranch)
if err == nil {
activity.CommitMessage = commit.Summary()
activity.CommitTime = timeutil.TimeStamp(commit.Author.When.Unix())
}
gitRepo.Close()
}
recentActivity = append(recentActivity, activity)
}
ctx.Data["RecentActivity"] = recentActivity
}
prepareResult, err := shared_user.RenderUserOrgHeader(ctx)
if err != nil {
@ -157,12 +204,10 @@ func home(ctx *context.Context, viewRepositories bool) {
}
ctx.Data["OrgStats"] = orgStats
// if no profile readme, it still means "view repositories"
isViewOverview := !viewRepositories && prepareOrgProfileReadme(ctx, prepareResult)
// Also show overview if there are pinned repos even without profile readme
if !viewRepositories && len(pinnedRepos) > 0 {
isViewOverview = true
}
// Always show overview by default for organizations
isViewOverview := !viewRepositories
// Load profile readme if available
prepareOrgProfileReadme(ctx, prepareResult)
ctx.Data["PageIsViewRepositories"] = !isViewOverview
ctx.Data["PageIsViewOverview"] = isViewOverview
ctx.Data["ShowOrgProfileReadmeSelector"] = isViewOverview && prepareResult.ProfilePublicReadmeBlob != nil && prepareResult.ProfilePrivateReadmeBlob != nil
@ -242,3 +287,45 @@ func prepareOrgProfileReadme(ctx *context.Context, prepareResult *shared_user.Pr
ctx.Data["IsViewingOrgAsMember"] = viewAsMember
return true
}
// CreateProfileRepo creates a .profile repository with README for the organization
func CreateProfileRepo(ctx *context.Context) {
org := ctx.Org.Organization
// Check if user can create repos in this org
if !ctx.Org.CanCreateOrgRepo {
ctx.Flash.Error(ctx.Tr("org.profile_repo_no_permission"))
ctx.Redirect(org.AsUser().HomeLink())
return
}
// Check if .profile repo already exists
exists, err := repo_model.IsRepositoryModelExist(ctx, org.AsUser(), ".profile")
if err != nil {
ctx.ServerError("IsRepositoryExist", err)
return
}
if exists {
ctx.Redirect(org.AsUser().HomeLink() + "/.profile")
return
}
// Create the .profile repository
repo, err := repo_service.CreateRepository(ctx, ctx.Doer, org.AsUser(), repo_service.CreateRepoOptions{
Name: ".profile",
Description: "Organization profile",
AutoInit: true,
Readme: "Default",
DefaultBranch: "main",
IsPrivate: false,
})
if err != nil {
log.Error("CreateProfileRepo: %v", err)
ctx.Flash.Error(ctx.Tr("org.profile_repo_create_failed"))
ctx.Redirect(org.AsUser().HomeLink())
return
}
// Redirect to edit the README
ctx.Redirect(repo.Link() + "/_edit/main/README.md")
}

View File

@ -70,15 +70,19 @@ func getRepoFromRequest(ctx *context.Context) (*repo_model.Repository, *pages_mo
return repo, config, nil
}
// Parse subdomain: {repo}.{owner}.pages.{domain}
// This is a simplified implementation
// Parse subdomain: {repo}-{owner}.{domain}
parts := strings.Split(host, ".")
if len(parts) < 4 {
if len(parts) < 2 {
return nil, nil, errors.New("invalid pages subdomain")
}
repoName := parts[0]
ownerName := parts[1]
// First part is {repo}-{owner}
repoOwner := strings.SplitN(parts[0], "-", 2)
if len(repoOwner) != 2 {
return nil, nil, errors.New("invalid pages subdomain format")
}
repoName := repoOwner[0]
ownerName := repoOwner[1]
repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
if err != nil {

128
routers/web/repo/pin.go Normal file
View File

@ -0,0 +1,128 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
)
// Pin handles pinning a repo to user profile or organization
func Pin(ctx *context.Context) {
pinType := ctx.FormString("type")
redirectTo := ctx.FormString("redirect_to")
if redirectTo == "" {
redirectTo = ctx.Repo.RepoLink
}
switch pinType {
case "user":
if err := user_model.PinRepoToUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID); err != nil {
if user_model.IsErrUserPinnedRepoLimit(err) {
ctx.Flash.Error(ctx.Tr("repo.pin.error_limit"))
} else {
log.Error("PinRepoToUser failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
}
} else {
ctx.Flash.Success(ctx.Tr("repo.pin.success_profile"))
}
case "org":
if !ctx.Repo.Repository.Owner.IsOrganization() {
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_org"))
ctx.Redirect(redirectTo)
return
}
// Check if user is a member of the org
isMember, err := organization.IsOrganizationMember(ctx, ctx.Repo.Repository.OwnerID, ctx.Doer.ID)
if err != nil {
log.Error("IsOrganizationMember failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
ctx.Redirect(redirectTo)
return
}
if !isMember {
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_member"))
ctx.Redirect(redirectTo)
return
}
// Use CreateOrgPinnedRepo
pinnedRepo := &organization.OrgPinnedRepo{
OrgID: ctx.Repo.Repository.OwnerID,
RepoID: ctx.Repo.Repository.ID,
}
if err := organization.CreateOrgPinnedRepo(ctx, pinnedRepo); err != nil {
if _, ok := err.(organization.ErrOrgPinnedRepoAlreadyExist); ok {
ctx.Flash.Info(ctx.Tr("repo.pin.already_pinned_org"))
} else {
log.Error("CreateOrgPinnedRepo failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
}
} else {
ctx.Flash.Success(ctx.Tr("repo.pin.success_org"))
}
default:
ctx.Flash.Error(ctx.Tr("repo.pin.error_invalid_type"))
}
ctx.Redirect(redirectTo)
}
// Unpin handles unpinning a repo from user profile or organization
func Unpin(ctx *context.Context) {
pinType := ctx.FormString("type")
redirectTo := ctx.FormString("redirect_to")
if redirectTo == "" {
redirectTo = ctx.Repo.RepoLink
}
switch pinType {
case "user":
if err := user_model.UnpinRepoFromUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID); err != nil {
log.Error("UnpinRepoFromUser failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
} else {
ctx.Flash.Success(ctx.Tr("repo.pin.unpin_success_profile"))
}
case "org":
if !ctx.Repo.Repository.Owner.IsOrganization() {
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_org"))
ctx.Redirect(redirectTo)
return
}
// Check if user is a member of the org
isMember, err := organization.IsOrganizationMember(ctx, ctx.Repo.Repository.OwnerID, ctx.Doer.ID)
if err != nil {
log.Error("IsOrganizationMember failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
ctx.Redirect(redirectTo)
return
}
if !isMember {
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_member"))
ctx.Redirect(redirectTo)
return
}
if err := organization.DeleteOrgPinnedRepo(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID); err != nil {
log.Error("DeleteOrgPinnedRepo failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
} else {
ctx.Flash.Success(ctx.Tr("repo.pin.unpin_success_org"))
}
default:
ctx.Flash.Error(ctx.Tr("repo.pin.error_invalid_type"))
}
ctx.Redirect(redirectTo)
}

View File

@ -41,6 +41,7 @@ func Pages(ctx *context.Context) {
// Generate subdomain
ctx.Data["PagesSubdomain"] = pages_service.GetPagesSubdomain(ctx.Repo.Repository)
ctx.Data["PagesURL"] = pages_service.GetPagesURL(ctx.Repo.Repository)
// Available templates
ctx.Data["PagesTemplates"] = []string{"simple", "documentation", "product", "portfolio"}
@ -92,7 +93,8 @@ func PagesPost(ctx *context.Context) {
ctx.Redirect(ctx.Repo.Repository.Link() + "/settings/pages")
return
}
_, err := pages_service.AddPagesDomain(ctx, ctx.Repo.Repository.ID, domain)
sslExternal := ctx.FormBool("ssl_external")
_, err := pages_service.AddPagesDomain(ctx, ctx.Repo.Repository.ID, domain, sslExternal)
if err != nil {
if repo_model.IsErrPagesDomainAlreadyExist(err) {
ctx.Flash.Error(ctx.Tr("repo.settings.pages.domain_exists"))
@ -112,6 +114,14 @@ func PagesPost(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.domain_deleted"))
case "activate_ssl":
domainID := ctx.FormInt64("domain_id")
if err := repo_model.ActivatePagesDomainSSL(ctx, domainID); err != nil {
ctx.ServerError("ActivatePagesDomainSSL", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.pages.ssl_activated"))
case "verify_domain":
domainID := ctx.FormInt64("domain_id")
if err := pages_service.VerifyDomain(ctx, domainID); err != nil {

View File

@ -5,6 +5,7 @@ package actions
import (
"errors"
"strings"
"net/http"
"net/url"
@ -15,6 +16,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
@ -295,6 +297,45 @@ func ResetRunnerRegistrationToken(ctx *context.Context) {
ctx.JSONRedirect(redirectTo)
}
// RunnerRequestBandwidthTest handles admin request to trigger a bandwidth test
func RunnerRequestBandwidthTest(ctx *context.Context) {
rCtx, err := getRunnersCtx(ctx)
if err != nil {
ctx.ServerError("getRunnersCtx", err)
return
}
runnerID := ctx.PathParamInt64("runnerid")
ownerID := rCtx.OwnerID
repoID := rCtx.RepoID
redirectTo := rCtx.RedirectLink + url.PathEscape(ctx.PathParam("runnerid"))
runner, err := actions_model.GetRunnerByID(ctx, runnerID)
if err != nil {
log.Warn("RunnerRequestBandwidthTest.GetRunnerByID failed: %v, url: %s", err, ctx.Req.URL)
ctx.ServerError("RunnerRequestBandwidthTest.GetRunnerByID", err)
return
}
if !runner.EditableInContext(ownerID, repoID) {
ctx.NotFound(util.NewPermissionDeniedErrorf("no permission to edit this runner"))
return
}
// Set the bandwidth test request timestamp
runner.BandwidthTestRequestedAt = timeutil.TimeStampNow()
err = actions_model.UpdateRunner(ctx, runner, "bandwidth_test_requested_at")
if err != nil {
log.Warn("RunnerRequestBandwidthTest.UpdateRunner failed: %v, url: %s", err, ctx.Req.URL)
ctx.Flash.Warning(ctx.Tr("actions.runners.bandwidth_test_request_failed"))
ctx.Redirect(redirectTo)
return
}
log.Debug("RunnerRequestBandwidthTest success: %s", ctx.Req.URL)
ctx.Flash.Success(ctx.Tr("actions.runners.bandwidth_test_requested"))
ctx.Redirect(redirectTo)
}
// RunnerDeletePost response for deleting runner
func RunnerDeletePost(ctx *context.Context) {
rCtx, err := getRunnersCtx(ctx)
@ -368,3 +409,234 @@ func findActionsRunner(ctx *context.Context, rCtx *runnersCtx) *actions_model.Ac
return got[0]
}
// RunnerAddLabel adds a single label to a runner
func RunnerAddLabel(ctx *context.Context) {
rCtx, err := getRunnersCtx(ctx)
if err != nil {
ctx.ServerError("getRunnersCtx", err)
return
}
runner := findActionsRunner(ctx, rCtx)
if runner == nil {
return
}
label := ctx.FormString("label")
if label == "" {
ctx.Flash.Warning("No label specified")
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
return
}
// Check if label already exists
for _, existing := range runner.AgentLabels {
if existing == label {
ctx.Flash.Info("Label already exists")
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
return
}
}
// Add the label
runner.AgentLabels = append(runner.AgentLabels, label)
err = actions_model.UpdateRunner(ctx, runner, "agent_labels")
if err != nil {
log.Warn("RunnerAddLabel.UpdateRunner failed: %v", err)
ctx.Flash.Warning("Failed to add label")
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
return
}
ctx.Flash.Success("Label added: " + label)
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
}
// RunnerRemoveLabel removes a single label from a runner
func RunnerRemoveLabel(ctx *context.Context) {
rCtx, err := getRunnersCtx(ctx)
if err != nil {
ctx.ServerError("getRunnersCtx", err)
return
}
runner := findActionsRunner(ctx, rCtx)
if runner == nil {
return
}
label := ctx.FormString("label")
if label == "" {
ctx.Flash.Warning("No label specified")
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
return
}
// Remove the label
newLabels := make([]string, 0, len(runner.AgentLabels))
found := false
for _, existing := range runner.AgentLabels {
if existing == label {
found = true
} else {
newLabels = append(newLabels, existing)
}
}
if !found {
ctx.Flash.Info("Label not found")
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
return
}
runner.AgentLabels = newLabels
err = actions_model.UpdateRunner(ctx, runner, "agent_labels")
if err != nil {
log.Warn("RunnerRemoveLabel.UpdateRunner failed: %v", err)
ctx.Flash.Warning("Failed to remove label")
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
return
}
ctx.Flash.Success("Label removed: " + label)
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
}
// RunnerUseSuggestedLabels adds all suggested labels based on capabilities
func RunnerUseSuggestedLabels(ctx *context.Context) {
rCtx, err := getRunnersCtx(ctx)
if err != nil {
ctx.ServerError("getRunnersCtx", err)
return
}
runner := findActionsRunner(ctx, rCtx)
if runner == nil {
return
}
// Parse capabilities to get suggested labels
if runner.CapabilitiesJSON == "" {
ctx.Flash.Warning("No capabilities data available")
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
return
}
var caps structs.RunnerCapability
if err := json.Unmarshal([]byte(runner.CapabilitiesJSON), &caps); err != nil {
ctx.Flash.Warning("Failed to parse capabilities")
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
return
}
// Build suggested labels
suggestedLabels := []string{}
existingSet := make(map[string]bool)
for _, label := range runner.AgentLabels {
existingSet[label] = true
}
// OS-based labels
switch caps.OS {
case "linux":
suggestedLabels = append(suggestedLabels, "linux", "linux-latest")
case "windows":
suggestedLabels = append(suggestedLabels, "windows", "windows-latest")
case "darwin":
suggestedLabels = append(suggestedLabels, "macos", "macos-latest")
}
// Distro-based labels
if caps.Distro != nil && caps.Distro.ID != "" {
suggestedLabels = append(suggestedLabels, caps.Distro.ID, caps.Distro.ID+"-latest")
}
// Add only new labels
added := []string{}
for _, label := range suggestedLabels {
if !existingSet[label] {
runner.AgentLabels = append(runner.AgentLabels, label)
added = append(added, label)
existingSet[label] = true
}
}
if len(added) == 0 {
ctx.Flash.Info("All suggested labels already exist")
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
return
}
err = actions_model.UpdateRunner(ctx, runner, "agent_labels")
if err != nil {
log.Warn("RunnerUseSuggestedLabels.UpdateRunner failed: %v", err)
ctx.Flash.Warning("Failed to add labels")
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
return
}
ctx.Flash.Success("Added labels: " + strings.Join(added, ", "))
ctx.Redirect(rCtx.RedirectLink + ctx.PathParam("runnerid"))
}
// RunnerStatusJSON returns runner status as JSON for AJAX polling
func RunnerStatusJSON(ctx *context.Context) {
rCtx, err := getRunnersCtx(ctx)
if err != nil {
ctx.ServerError("getRunnersCtx", err)
return
}
runner := findActionsRunner(ctx, rCtx)
if runner == nil {
return
}
// Parse capabilities
var caps *structs.RunnerCapability
if runner.CapabilitiesJSON != "" {
caps = &structs.RunnerCapability{}
if err := json.Unmarshal([]byte(runner.CapabilitiesJSON), caps); err != nil {
caps = nil
}
}
// Build response matching the tile structure
response := map[string]any{
"id": runner.ID,
"name": runner.Name,
"is_online": runner.IsOnline(),
"status": runner.StatusLocaleName(ctx.Locale),
"version": runner.Version,
"labels": runner.AgentLabels,
}
if runner.LastOnline.AsTime().Unix() > 0 {
response["last_online"] = runner.LastOnline.AsTime().Format("2006-01-02T15:04:05Z")
}
if caps != nil {
if caps.Disk != nil {
response["disk"] = map[string]any{
"total_bytes": caps.Disk.Total,
"free_bytes": caps.Disk.Free,
"used_bytes": caps.Disk.Used,
"used_percent": caps.Disk.UsedPercent,
}
}
if caps.Bandwidth != nil {
bw := map[string]any{
"download_mbps": caps.Bandwidth.DownloadMbps,
"latency_ms": caps.Bandwidth.Latency,
}
if !caps.Bandwidth.TestedAt.IsZero() {
bw["tested_at"] = caps.Bandwidth.TestedAt.Format("2006-01-02T15:04:05Z")
}
response["bandwidth"] = bw
}
}
ctx.JSON(http.StatusOK, response)
}

View File

@ -79,15 +79,10 @@ func userProfile(ctx *context.Context) {
}
func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.Repository, profileReadme *git.Blob) {
// if there is a profile readme, default to "overview" page, otherwise, default to "repositories" page
// if there is not a profile readme, the overview tab should be treated as the repositories tab
// Default to overview page for users
tab := ctx.FormString("tab")
if tab == "" || tab == "overview" {
if profileReadme != nil {
if tab == "" {
tab = "overview"
} else {
tab = "repositories"
}
}
ctx.Data["TabName"] = tab
ctx.Data["HasUserProfileReadme"] = profileReadme != nil
@ -252,6 +247,35 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
total = int(count)
case "overview":
// Load heatmap if user has it enabled
if ctx.ContextUser.ShowHeatmapOnProfile && setting.Service.EnableUserHeatmap {
data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
if err != nil {
log.Error("GetUserHeatmapDataByUser: %v", err)
} else {
ctx.Data["HeatmapData"] = data
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
}
}
// Load pinned repositories
pinnedRepos, err := user_model.GetUserPinnedRepos(ctx, ctx.ContextUser.ID)
if err != nil {
log.Error("GetUserPinnedRepos: %v", err)
} else {
// Load repo details for each pinned repo
for _, p := range pinnedRepos {
repo, err := repo_model.GetRepositoryByID(ctx, p.RepoID)
if err == nil {
p.Repo = repo
}
}
}
ctx.Data["UserPinnedRepos"] = pinnedRepos
ctx.Data["IsContextUserProfile"] = ctx.Doer != nil && ctx.Doer.ID == ctx.ContextUser.ID
// Load profile README
if profileReadme != nil {
if bytes, err := profileReadme.GetBlobContent(setting.UI.MaxDisplayFileSize); err != nil {
log.Error("failed to GetBlobContent: %v", err)
} else {
@ -264,6 +288,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
ctx.Data["ProfileReadmeContent"] = profileContent
}
}
}
case "organizations":
orgs, count, err := db.FindAndCount[organization.Organization](ctx, organization.FindOrgOptions{
UserID: ctx.ContextUser.ID,

View File

@ -103,6 +103,7 @@ func ProfilePost(ctx *context.Context) {
Location: optional.Some(form.Location),
Visibility: optional.Some(form.Visibility),
KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
ShowHeatmapOnProfile: optional.Some(form.ShowHeatmapOnProfile),
}
if form.FullName != "" {

View File

@ -8,6 +8,7 @@ import (
"strings"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/git"
@ -302,6 +303,45 @@ var optSignInFromAnyOrigin = verifyAuthWithOptions(&common.VerifyOptions{Disable
// registerWebRoutes register routes
func registerWebRoutes(m *web.Router) {
// Check for Pages subdomain and custom domain requests first
m.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
host := req.Host
// Remove port if present
if idx := strings.Index(host, ":"); idx > 0 {
host = host[:idx]
}
// Check if this is a subdomain of our main domain
mainDomain := setting.Domain
if strings.HasSuffix(host, "."+mainDomain) {
subdomain := strings.TrimSuffix(host, "."+mainDomain)
// Skip known subdomains
if subdomain != "" && subdomain != "www" && subdomain != "api" && subdomain != "git" && strings.Contains(subdomain, "-") {
// This looks like a Pages subdomain ({repo}-{owner})
ctx := context.GetWebContext(req.Context())
if ctx != nil {
log.Trace("Pages subdomain request: %s", host)
pages.ServeLandingPage(ctx)
return
}
}
} else if host != mainDomain && host != "www."+mainDomain {
// Check if this is a custom domain for Pages
domain, err := repo_model.GetPagesDomainByDomain(req.Context(), host)
if err == nil && domain != nil && domain.Verified {
ctx := context.GetWebContext(req.Context())
if ctx != nil {
log.Trace("Pages custom domain request: %s -> repo %d", host, domain.RepoID)
pages.ServeLandingPage(ctx)
return
}
}
}
next.ServeHTTP(w, req)
})
})
// required to be signed in or signed out
reqSignIn := verifyAuthWithOptions(&common.VerifyOptions{SignInRequired: true})
reqSignOut := verifyAuthWithOptions(&common.VerifyOptions{SignOutRequired: true})
@ -470,6 +510,11 @@ func registerWebRoutes(m *web.Router) {
m.Combo("/{runnerid}").Get(shared_actions.RunnersEdit).
Post(web.Bind(forms.EditRunnerForm{}), shared_actions.RunnersEditPost)
m.Post("/{runnerid}/delete", shared_actions.RunnerDeletePost)
m.Post("/{runnerid}/bandwidth-test", shared_actions.RunnerRequestBandwidthTest)
m.Post("/{runnerid}/add-label", shared_actions.RunnerAddLabel)
m.Post("/{runnerid}/remove-label", shared_actions.RunnerRemoveLabel)
m.Post("/{runnerid}/use-suggested-labels", shared_actions.RunnerUseSuggestedLabels)
m.Get("/{runnerid}/status", shared_actions.RunnerStatusJSON)
m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken)
})
}
@ -908,6 +953,7 @@ func registerWebRoutes(m *web.Router) {
m.Get("/milestones", reqMilestonesDashboardPageEnabled, user.Milestones)
m.Get("/milestones/{team}", reqMilestonesDashboardPageEnabled, user.Milestones)
m.Post("/members/action/{action}", org.MembersAction)
m.Post("/create-profile-repo", org.CreateProfileRepo)
m.Get("/teams", org.Teams)
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireMember: true, RequireTeamMember: true}))
@ -1670,6 +1716,8 @@ func registerWebRoutes(m *web.Router) {
m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch)
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
m.Get("/action/pin", reqSignIn, repo.Pin)
m.Get("/action/unpin", reqSignIn, repo.Unpin)
}, optSignIn, context.RepoAssignment)
common.AddOwnerRepoGitLFSRoutes(m, lfsServerEnabled, repo.CorsHandler(), optSignInFromAnyOrigin) // "/{username}/{reponame}/{lfs-paths}": git-lfs support, see also addOwnerRepoGitHTTPRouters

View File

@ -19,6 +19,7 @@ import (
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
@ -415,6 +416,21 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
ctx.Repo.Repository = repo
ctx.Data["RepoName"] = ctx.Repo.Repository.Name
// Check if repo is pinned (for pin dropdown)
if ctx.Doer != nil {
isPinnedToUser, _ := user_model.IsRepoPinnedByUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
ctx.Data["IsRepoPinnedToUser"] = isPinnedToUser
}
if ctx.Repo.Repository.Owner.IsOrganization() {
isPinnedToOrg, _ := organization.IsRepoPinned(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID)
ctx.Data["IsRepoPinnedToOrg"] = isPinnedToOrg
// Check if user is a member of the org for pin dropdown
if ctx.Doer != nil {
isMember, _ := organization.IsOrganizationMember(ctx, ctx.Repo.Repository.OwnerID, ctx.Doer.ID)
ctx.Data["IsOrganizationMember"] = isMember
}
}
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
}

View File

@ -219,6 +219,7 @@ type UpdateProfileForm struct {
Description string `binding:"MaxSize(255)"`
Visibility structs.VisibleType
KeepActivityPrivate bool
ShowHeatmapOnProfile bool
}
// Validate validates the fields

View File

@ -8,7 +8,6 @@ import (
"code.gitea.io/gitea/models/organization"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/optional"
)
// GetOrgPinnedReposWithDetails returns all pinned repos with repo and group details loaded
@ -38,9 +37,13 @@ func GetOrgPinnedReposWithDetails(ctx context.Context, orgID int64) ([]*organiza
return nil, err
}
// Attach repos
// Attach repos and load attributes (including primary language)
for _, p := range pinnedRepos {
p.Repo = repos[p.RepoID]
repo := repos[p.RepoID]
if repo != nil {
_ = repo.LoadAttributes(ctx)
}
p.Repo = repo
}
return pinnedRepos, nil
@ -54,23 +57,22 @@ func GetOrgOverviewStats(ctx context.Context, orgID int64) (*organization.OrgOve
if err != nil {
return nil, err
}
stats.MemberCount = memberCount
stats.TeamCount = teamCount
stats.TotalMembers = memberCount
stats.TotalTeams = teamCount
// Repo counts
stats.RepoCount, err = repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{
// Repo count
stats.TotalRepos, err = repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{
OwnerID: orgID,
})
if err != nil {
return nil, err
}
stats.PublicRepoCount, err = repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{
OwnerID: orgID,
Private: optional.Some(false),
})
// Total stars across all repos
stats.TotalStars, err = repo_model.CountOrgRepoStars(ctx, orgID)
if err != nil {
return nil, err
// Non-fatal, just log and continue
stats.TotalStars = 0
}
return stats, nil

View File

@ -44,6 +44,10 @@ func GetPagesConfig(ctx context.Context, repo *repo_model.Repository) (*pages_mo
}
return &config, nil
}
// If Pages is enabled but no config file, return a default config
if dbConfig != nil && dbConfig.Enabled {
return getDefaultConfig(repo, string(dbConfig.Template)), nil
}
return nil, err
}
@ -93,6 +97,26 @@ func GetPagesConfig(ctx context.Context, repo *repo_model.Repository) (*pages_mo
}
// loadConfigFromRepo loads the landing.yaml configuration from the repository
// getDefaultConfig returns a default landing page configuration
func getDefaultConfig(repo *repo_model.Repository, template string) *pages_module.LandingConfig {
if template == "" {
template = "simple"
}
return &pages_module.LandingConfig{
Enabled: true,
Template: template,
Hero: pages_module.HeroConfig{
Title: repo.Name,
Tagline: repo.Description,
},
Branding: pages_module.BrandingConfig{
PrimaryColor: "#4183c4",
},
}
}
func loadConfigFromRepo(ctx context.Context, repo *repo_model.Repository) (*pages_module.LandingConfig, string, error) {
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
if err != nil {
@ -172,16 +196,17 @@ func DisablePages(ctx context.Context, repo *repo_model.Repository) error {
// GetPagesSubdomain returns the subdomain for a repository's pages
func GetPagesSubdomain(repo *repo_model.Repository) string {
// Format: {repo}.{owner}.pages.{domain}
return fmt.Sprintf("%s.%s", strings.ToLower(repo.Name), strings.ToLower(repo.OwnerName))
// Format: {repo}-{owner}.{domain}
return fmt.Sprintf("%s-%s", strings.ToLower(repo.Name), strings.ToLower(repo.OwnerName))
}
// GetPagesURL returns the full URL for a repository's pages
func GetPagesURL(repo *repo_model.Repository) string {
subdomain := GetPagesSubdomain(repo)
// This should be configurable
pagesDomain := setting.AppURL // TODO: Add proper pages domain setting
return fmt.Sprintf("https://%s.pages.%s", subdomain, pagesDomain)
// Extract domain from settings
domain := setting.Domain
return fmt.Sprintf("https://%s.%s", subdomain, domain)
}
// GetPagesDomains returns all custom domains for a repository's pages
@ -190,7 +215,7 @@ func GetPagesDomains(ctx context.Context, repoID int64) ([]*repo_model.PagesDoma
}
// AddPagesDomain adds a custom domain for pages
func AddPagesDomain(ctx context.Context, repoID int64, domain string) (*repo_model.PagesDomain, error) {
func AddPagesDomain(ctx context.Context, repoID int64, domain string, sslExternal bool) (*repo_model.PagesDomain, error) {
// Normalize domain
domain = strings.ToLower(strings.TrimSpace(domain))
@ -200,9 +225,15 @@ func AddPagesDomain(ctx context.Context, repoID int64, domain string) (*repo_mod
return nil, repo_model.ErrPagesDomainAlreadyExist{Domain: domain}
}
sslStatus := repo_model.SSLStatusPending
if sslExternal {
sslStatus = repo_model.SSLStatusActive
}
pagesDomain := &repo_model.PagesDomain{
RepoID: repoID,
Domain: domain,
SSLStatus: sslStatus,
}
if err := repo_model.CreatePagesDomain(ctx, pagesDomain); err != nil {

View File

@ -47,6 +47,7 @@ type UpdateOptions struct {
IsRestricted optional.Option[bool]
Visibility optional.Option[structs.VisibleType]
KeepActivityPrivate optional.Option[bool]
ShowHeatmapOnProfile optional.Option[bool]
Language optional.Option[string]
Theme optional.Option[string]
DiffViewStyle optional.Option[string]
@ -158,6 +159,11 @@ func UpdateUser(ctx context.Context, u *user_model.User, opts *UpdateOptions) er
cols = append(cols, "keep_activity_private")
}
if opts.ShowHeatmapOnProfile.Has() {
u.ShowHeatmapOnProfile = opts.ShowHeatmapOnProfile.Value()
cols = append(cols, "show_heatmap_on_profile")
}
if opts.AllowCreateOrganization.Has() {
u.AllowCreateOrganization = opts.AllowCreateOrganization.Value()

View File

@ -4,31 +4,43 @@
<div class="ui container">
<div class="ui mobile reversed stackable grid">
<div class="ui {{if .ShowMemberAndTeamTab}}eleven wide{{end}} column">
<div class="ui eleven wide column">
{{/* Profile README Section */}}
{{if .ProfileReadmeContent}}
<div id="readme_profile" class="render-content markup" data-profile-view-as-member="{{.IsViewingOrgAsMember}}">{{.ProfileReadmeContent}}</div>
{{end}}
{{/* Overview Tab Content */}}
{{if .PageIsViewOverview}}
{{/* Pinned Repositories Section */}}
{{if and .PageIsViewOverview .HasPinnedRepos}}
<div class="ui segment pinned-repos-section">
<h4 class="ui header">
<h4 class="ui header tw-flex tw-items-center">
{{svg "octicon-pin" 16}} {{ctx.Locale.Tr "org.pinned_repos"}}
{{if .IsOrganizationOwner}}
<a class="tw-ml-auto ui mini button" href="{{.OrgLink}}/settings">
{{svg "octicon-gear" 14}} {{ctx.Locale.Tr "org.settings.pinned.manage"}}
</a>
{{end}}
</h4>
{{if .HasPinnedRepos}}
{{/* Ungrouped pinned repos */}}
{{if .UngroupedPinned}}
<div class="ui three stackable cards pinned-repos">
{{range .UngroupedPinned}}
{{if .Repo}}
<a class="ui card" href="{{.Repo.Link}}">
<div class="content">
<div class="header text truncate">
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 16}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 16}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 16}}{{else}}{{svg "octicon-repo" 16}}{{end}}
{{.Repo.Name}}
<div class="content tw-text-center">
{{if .Repo.Avatar}}
<img class="tw-inline-block tw-rounded" style="max-width: 80px; max-height: 80px; object-fit: contain;" src="{{.Repo.RelAvatarLink ctx}}" alt="">
{{else}}
<div class="tw-inline-block tw-p-4">
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 48}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 48}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 48}}{{else}}{{svg "octicon-repo" 48}}{{end}}
</div>
{{end}}
<div class="header tw-mt-2">{{.Repo.Name}}</div>
{{if .Repo.Description}}
<div class="description text truncate">{{.Repo.Description}}</div>
<div class="description text grey tw-text-sm tw-mt-1">{{.Repo.Description}}</div>
{{end}}
</div>
<div class="extra content">
@ -63,13 +75,17 @@
{{range $groupRepos}}
{{if .Repo}}
<a class="ui card" href="{{.Repo.Link}}">
<div class="content">
<div class="header text truncate">
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 16}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 16}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 16}}{{else}}{{svg "octicon-repo" 16}}{{end}}
{{.Repo.Name}}
<div class="content tw-text-center">
{{if .Repo.Avatar}}
<img class="tw-inline-block tw-rounded" style="max-width: 80px; max-height: 80px; object-fit: contain;" src="{{.Repo.RelAvatarLink ctx}}" alt="">
{{else}}
<div class="tw-inline-block tw-p-4">
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 48}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 48}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 48}}{{else}}{{svg "octicon-repo" 48}}{{end}}
</div>
{{end}}
<div class="header tw-mt-2">{{.Repo.Name}}</div>
{{if .Repo.Description}}
<div class="description text truncate">{{.Repo.Description}}</div>
<div class="description text grey tw-text-sm tw-mt-1">{{.Repo.Description}}</div>
{{end}}
</div>
<div class="extra content">
@ -93,30 +109,86 @@
</div>
{{end}}
{{end}}
{{else}}
{{/* Empty state for pinned repos */}}
<div class="ui placeholder segment tw-text-center">
<div class="ui icon header">
{{svg "octicon-pin" 48}}
<div class="content">
{{ctx.Locale.Tr "org.pinned_repos_empty_title"}}
<div class="sub header">
{{ctx.Locale.Tr "org.pinned_repos_empty_desc"}}
</div>
{{end}}
{{/* Public Members Section (on overview) */}}
{{if and .PageIsViewOverview .PublicMembers}}
<div class="ui segment public-members-section tw-mt-4">
<h4 class="ui header tw-flex tw-items-center">
{{svg "octicon-people" 16}} {{ctx.Locale.Tr "org.public_members"}}
{{if .HasMorePublicMembers}}
<a class="tw-ml-auto text grey tw-text-sm" href="{{.OrgLink}}/members">{{ctx.Locale.Tr "org.view_all_members" .TotalPublicMembers}}</a>
{{end}}
</h4>
<div class="tw-flex tw-flex-wrap tw-gap-2">
{{range .PublicMembers}}
<a href="{{.User.HomeLink}}" title="{{.User.Name}} ({{.Role}})" class="tw-flex tw-flex-col tw-items-center tw-p-2">
{{ctx.AvatarUtils.Avatar .User 48}}
<span class="tw-text-sm tw-mt-1">{{.User.Name}}</span>
<span class="tw-text-xs text grey">{{.Role}}</span>
</div>
</div>
{{if .IsOrganizationOwner}}
<div class="tw-mt-4">
<a class="ui primary button" href="{{.OrgLink}}/settings">
{{svg "octicon-gear" 16}} {{ctx.Locale.Tr "org.settings.pinned.setup"}}
</a>
</div>
{{end}}
</div>
{{end}}
</div>
{{/* Profile README Empty State */}}
{{if and (not .ProfileReadmeContent) .IsOrganizationOwner}}
<div class="ui segment tw-mt-4">
<div class="ui placeholder segment tw-text-center">
<div class="ui icon header">
{{svg "octicon-book" 32}}
<div class="content">
{{ctx.Locale.Tr "org.profile_readme_empty_title"}}
<div class="sub header">
{{ctx.Locale.Tr "org.profile_readme_empty_desc"}}
</div>
</div>
</div>
<div class="tw-mt-4">
<form action="{{.OrgLink}}/create-profile-repo" method="post">{{.CsrfTokenHtml}}<button class="ui primary button" type="submit">
{{svg "octicon-plus" 16}} {{ctx.Locale.Tr "org.create_profile_repo"}}
</button></form>
</div>
</div>
</div>
{{end}}
{{/* Recent Activity Section */}}
{{if .RecentActivity}}
<div class="ui segment tw-mt-4">
<h4 class="ui header tw-flex tw-items-center">
{{svg "octicon-pulse" 16}} {{ctx.Locale.Tr "org.recent_activity"}}
</h4>
<div class="ui relaxed divided list">
{{range .RecentActivity}}
<div class="item">
<div class="tw-flex tw-items-center tw-gap-3">
{{if .Repo.Avatar}}
<img style="width: 32px; height: 32px; border-radius: 4px; object-fit: cover;" src="{{.Repo.RelAvatarLink ctx}}" alt="">
{{else}}
<div class="tw-w-8 tw-h-8 tw-flex tw-items-center tw-justify-center">
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 20}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 20}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 20}}{{else}}{{svg "octicon-repo" 20}}{{end}}
</div>
{{end}}
<div class="tw-flex-1 tw-min-w-0">
<a href="{{.Repo.Link}}" class="tw-font-semibold">{{.Repo.Name}}</a>
{{if .CommitMessage}}
<p class="text grey tw-text-sm tw-truncate tw-mb-0">{{.CommitMessage}}</p>
{{end}}
</div>
<div class="tw-text-right tw-text-sm text grey tw-flex-shrink-0">
<span title="{{DateUtils.FullTime .CommitTime}}">{{DateUtils.TimeSince .CommitTime}}</span>
</div>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
{{end}}
{{/* Repositories Tab Content */}}
{{if .PageIsViewRepositories}}
{{template "shared/repo/search" .}}
{{template "shared/repo/list" .}}
@ -124,7 +196,6 @@
{{end}}
</div>
{{if .ShowMemberAndTeamTab}}
<div class="ui five wide column">
{{if .CanCreateOrgRepo}}
<div class="tw-flex tw-flex-wrap tw-justify-center tw-gap-x-1 tw-gap-y-2 tw-mb-4">
@ -136,7 +207,7 @@
<div class="divider"></div>
{{end}}
{{if and .ShowMemberAndTeamTab .ShowOrgProfileReadmeSelector}}
{{if .ShowOrgProfileReadmeSelector}}
<div class="tw-my-4">
<div id="org-home-view-as-dropdown" class="ui dropdown jump">
{{- $viewAsRole := Iif (.IsViewingOrgAsMember) (ctx.Locale.Tr "org.members.member") (ctx.Locale.Tr "settings.visibility.public") -}}
@ -151,28 +222,56 @@
</a>
</div>
</div>
<div class="tw-my-2">
<div class="tw-my-2 text grey">
{{if .IsViewingOrgAsMember}}{{ctx.Locale.Tr "org.view_as_member_hint"}}{{else}}{{ctx.Locale.Tr "org.view_as_public_hint"}}{{end}}
</div>
</div>
{{end}}
{{/* Organization Stats - Sidebar Card */}}
{{if .OrgStats}}
<div class="ui top attached header tw-flex">
<strong class="tw-flex-1">{{svg "octicon-graph" 16}} {{ctx.Locale.Tr "org.stats"}}</strong>
</div>
<div class="ui attached segment">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<div class="tw-text-center tw-p-3" style="border: 1px solid var(--color-secondary); border-radius: 6px;">
<div class="tw-text-2xl tw-font-bold">{{.OrgStats.TotalRepos}}</div>
<div class="text grey tw-text-xs">{{ctx.Locale.Tr "org.repositories"}}</div>
</div>
<div class="tw-text-center tw-p-3" style="border: 1px solid var(--color-secondary); border-radius: 6px;">
<div class="tw-text-2xl tw-font-bold">{{.OrgStats.TotalMembers}}</div>
<div class="text grey tw-text-xs">{{ctx.Locale.Tr "org.members"}}</div>
</div>
<div class="tw-text-center tw-p-3" style="border: 1px solid var(--color-secondary); border-radius: 6px;">
<div class="tw-text-2xl tw-font-bold">{{.OrgStats.TotalTeams}}</div>
<div class="text grey tw-text-xs">{{ctx.Locale.Tr "org.teams"}}</div>
</div>
<div class="tw-text-center tw-p-3" style="border: 1px solid var(--color-secondary); border-radius: 6px;">
<div class="tw-text-2xl tw-font-bold">{{.OrgStats.TotalStars}}</div>
<div class="text grey tw-text-xs">{{ctx.Locale.Tr "repo.stars"}}</div>
</div>
</div>
</div>
{{end}}
{{/* Members/Public Members Section */}}
{{if .IsOrganizationMember}}
{{/* Internal view - show all members */}}
{{if .NumMembers}}
<h4 class="ui top attached header tw-flex">
<h4 class="ui top attached header tw-flex tw-mt-4">
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.members"}}</strong>
<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/members"><span>{{.NumMembers}}</span> {{svg "octicon-chevron-right"}}</a>
</h4>
<div class="ui attached segment members">
{{$isMember := .IsOrganizationMember}}
{{range .Members}}
{{if or $isMember (call $.IsPublicMember .ID)}}
<a href="{{.HomeLink}}" title="{{.Name}}{{if .FullName}} ({{.FullName}}){{end}}">{{ctx.AvatarUtils.Avatar . 48}}</a>
{{end}}
{{end}}
</div>
{{end}}
{{if .IsOrganizationMember}}
<div class="ui top attached header tw-flex">
{{/* Teams - only for members */}}
<div class="ui top attached header tw-flex tw-mt-4">
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.teams"}}</strong>
<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/teams"><span>{{.Org.NumTeams}}</span> {{svg "octicon-chevron-right"}}</a>
</div>
@ -192,9 +291,30 @@
<a class="ui primary small button" href="{{.OrgLink}}/teams/new">{{ctx.Locale.Tr "org.create_new_team"}}</a>
</div>
{{end}}
{{else}}
{{/* Public view - show public members only */}}
{{if .PublicMembers}}
<h4 class="ui top attached header tw-flex tw-mt-4">
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.public_members"}}</strong>
{{if .HasMorePublicMembers}}
<a class="text grey tw-flex tw-items-center" href="{{.OrgLink}}/members"><span>{{.TotalPublicMembers}}</span> {{svg "octicon-chevron-right"}}</a>
{{end}}
</h4>
<div class="ui attached segment members">
{{range .PublicMembers}}
<a href="{{.User.HomeLink}}" title="{{.User.Name}}{{if .User.FullName}} ({{.User.FullName}}){{end}}">{{ctx.AvatarUtils.Avatar .User 48}}</a>
{{end}}
</div>
{{else}}
<h4 class="ui top attached header tw-flex tw-mt-4">
<strong class="tw-flex-1">{{ctx.Locale.Tr "org.public_members"}}</strong>
</h4>
<div class="ui attached segment">
<p class="text grey tw-text-center">{{ctx.Locale.Tr "org.no_public_members"}}</p>
</div>
{{end}}
{{end}}
</div>
</div>
</div>
</div>

View File

@ -1,12 +1,10 @@
<div class="ui container">
<overflow-menu class="ui secondary pointing tabular borderless menu tw-mb-4">
<div class="overflow-menu-items">
{{if .HasOrgProfileReadme}}
<a class="{{if .PageIsViewOverview}}active {{end}}item" href="{{$.Org.HomeLink}}">
{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
</a>
{{end}}
<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}{{if .HasOrgProfileReadme}}/-/repositories{{end}}">
<a class="{{if .PageIsViewRepositories}}active {{end}}item" href="{{$.Org.HomeLink}}/-/repositories">
{{svg "octicon-repo"}} {{ctx.Locale.Tr "user.repositories"}}
{{if .RepoCount}}
<div class="ui small label">{{.RepoCount}}</div>

View File

@ -0,0 +1,2 @@
</body>
</html>

View File

@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="{{ctx.Locale.Lang}}" data-theme="{{ctx.CurrentWebTheme.InternalName}}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{if .Config.Hero.Title}}{{.Config.Hero.Title}}{{else}}{{.Repository.Name}}{{end}} - {{.Repository.Owner.Name}}</title>
<meta name="description" content="{{if .Config.Hero.Tagline}}{{.Config.Hero.Tagline}}{{else}}{{.Repository.Description}}{{end}}">
<link rel="icon" href="{{AssetUrlPrefix}}/img/favicon.svg" type="image/svg+xml">
{{template "base/head_style" .}}
<style>
/* Pages standalone styles - no Gitea navbar */
:root {
--pages-primary: {{if .Config.Branding.PrimaryColor}}{{.Config.Branding.PrimaryColor}}{{else}}#4183c4{{end}};
--pages-secondary: {{if .Config.Branding.SecondaryColor}}{{.Config.Branding.SecondaryColor}}{{else}}#6c757d{{end}};
}
* { box-sizing: border-box; }
body.pages-body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #fff;
}
.pages-landing {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.pages-main { flex: 1; }
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
/* Header styles */
.pages-header {
background: #fff;
border-bottom: 1px solid #e1e4e8;
padding: 16px 0;
position: sticky;
top: 0;
z-index: 100;
}
.pages-nav {
display: flex;
align-items: center;
justify-content: space-between;
}
.pages-nav-brand {
text-decoration: none;
font-weight: 600;
font-size: 1.25rem;
color: #24292e;
}
.pages-nav-logo { height: 32px; }
.pages-nav-links {
display: flex;
align-items: center;
gap: 24px;
}
.pages-nav-link {
color: #586069;
text-decoration: none;
font-size: 0.875rem;
}
.pages-nav-link:hover { color: var(--pages-primary); }
/* Hero styles */
.pages-hero {
padding: 80px 0;
text-align: center;
background: linear-gradient(135deg, #f6f8fa 0%, #fff 100%);
}
.pages-logo {
max-height: 80px;
margin-bottom: 24px;
}
.pages-title {
font-size: 3rem;
font-weight: 700;
margin: 0 0 16px;
color: #24292e;
}
.pages-tagline {
font-size: 1.25rem;
color: #586069;
margin: 0 0 32px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.pages-cta { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; }
/* Stats */
.pages-stats {
padding: 48px 0;
background: #f6f8fa;
}
.pages-stats-grid {
display: flex;
justify-content: center;
gap: 48px;
flex-wrap: wrap;
}
.pages-stat {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.pages-stat-value {
font-size: 2rem;
font-weight: 700;
color: #24292e;
}
.pages-stat-label {
color: #586069;
font-size: 0.875rem;
}
/* README */
.pages-readme {
padding: 64px 0;
}
.pages-readme .markup {
max-width: 800px;
margin: 0 auto;
}
/* Footer */
.pages-footer {
background: #24292e;
color: #fff;
padding: 48px 0 24px;
margin-top: auto;
}
.pages-footer-links {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 32px;
margin-bottom: 32px;
}
.pages-footer-title {
font-size: 0.875rem;
font-weight: 600;
margin: 0 0 16px;
color: #fff;
}
.pages-footer-list {
list-style: none;
padding: 0;
margin: 0;
}
.pages-footer-list li { margin-bottom: 8px; }
.pages-footer-list a {
color: #959da5;
text-decoration: none;
font-size: 0.875rem;
}
.pages-footer-list a:hover { color: #fff; }
.pages-footer-bottom {
text-align: center;
padding-top: 24px;
border-top: 1px solid #444d56;
}
.pages-footer-copyright, .pages-footer-powered {
color: #959da5;
font-size: 0.75rem;
margin: 4px 0;
}
.pages-footer-powered a { color: #79b8ff; }
</style>
</head>
<body class="pages-body">

View File

@ -1,5 +1,118 @@
{{template "base/head" .}}
<div class="page-content pages-landing pages-documentation">
{{template "pages/base_head" .}}
<style>
/* Documentation-specific styles */
.pages-documentation {
background: #fff;
}
.pages-documentation .pages-header {
background: #24292e;
border-bottom: none;
}
.pages-documentation .pages-nav-brand,
.pages-documentation .pages-nav-link {
color: #fff;
}
.pages-documentation .pages-nav-link:hover {
color: #79b8ff;
}
.pages-docs-layout {
display: flex;
min-height: calc(100vh - 65px);
}
.pages-docs-sidebar {
width: 280px;
background: #f6f8fa;
border-right: 1px solid #e1e4e8;
padding: 24px;
position: sticky;
top: 65px;
height: calc(100vh - 65px);
overflow-y: auto;
}
.pages-docs-search {
margin-bottom: 24px;
}
.pages-search-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #e1e4e8;
border-radius: 6px;
font-size: 0.875rem;
}
.pages-docs-section {
margin-bottom: 24px;
}
.pages-docs-section-title {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #586069;
margin: 0 0 12px;
}
.pages-docs-list {
list-style: none;
padding: 0;
margin: 0;
}
.pages-docs-list li {
margin-bottom: 8px;
}
.pages-docs-list a {
color: #24292e;
text-decoration: none;
font-size: 0.875rem;
display: block;
padding: 4px 8px;
border-radius: 4px;
}
.pages-docs-list a:hover {
background: #e1e4e8;
color: var(--pages-primary);
}
.pages-docs-content {
flex: 1;
padding: 48px;
max-width: 900px;
}
.pages-docs-article {
line-height: 1.7;
}
.pages-docs-article h1,
.pages-docs-article h2,
.pages-docs-article h3 {
margin-top: 32px;
padding-bottom: 8px;
border-bottom: 1px solid #e1e4e8;
}
.pages-docs-article code {
background: #f6f8fa;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.875em;
}
.pages-docs-article pre {
background: #24292e;
color: #e1e4e8;
padding: 16px;
border-radius: 6px;
overflow-x: auto;
}
.pages-docs-article pre code {
background: none;
padding: 0;
color: inherit;
}
@media (max-width: 768px) {
.pages-docs-sidebar {
display: none;
}
.pages-docs-content {
padding: 24px;
}
}
</style>
<div class="pages-landing pages-documentation">
{{template "pages/header" .}}
<div class="pages-docs-layout">
@ -21,6 +134,15 @@
</ul>
</div>
{{end}}
{{else}}
<div class="pages-docs-section">
<h4 class="pages-docs-section-title">Documentation</h4>
<ul class="pages-docs-list">
<li><a href="#">Getting Started</a></li>
<li><a href="#">Installation</a></li>
<li><a href="#">Configuration</a></li>
</ul>
</div>
{{end}}
</nav>
</aside>
@ -32,12 +154,11 @@
{{.ReadmeContent}}
</div>
{{else}}
<p>{{ctx.Locale.Tr "repo.no_desc"}}</p>
<h1>{{.Repository.Name}}</h1>
<p>{{if .Repository.Description}}{{.Repository.Description}}{{else}}Welcome to the documentation.{{end}}</p>
{{end}}
</article>
</main>
</div>
{{template "pages/footer" .}}
</div>
{{template "base/footer" .}}
{{template "pages/base_footer" .}}

View File

@ -1,5 +1,131 @@
{{template "base/head" .}}
<div class="page-content pages-landing pages-portfolio">
{{template "pages/base_head" .}}
<style>
/* Portfolio-specific styles - Gallery/Creative design */
.pages-portfolio {
background: #1a1a1a;
color: #fff;
}
.pages-portfolio .pages-header {
background: transparent;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.pages-portfolio .pages-nav-brand,
.pages-portfolio .pages-nav-link {
color: #fff;
}
.pages-portfolio .pages-nav-link:hover {
color: var(--pages-primary);
}
.pages-portfolio .pages-hero {
background: linear-gradient(180deg, #1a1a1a 0%, #2d2d2d 100%);
padding: 100px 0 60px;
}
.pages-portfolio .pages-title {
color: #fff;
font-size: 2.5rem;
letter-spacing: -0.02em;
}
.pages-portfolio .pages-tagline {
color: rgba(255,255,255,0.7);
}
.pages-portfolio .pages-logo {
border-radius: 50%;
border: 3px solid rgba(255,255,255,0.2);
}
.pages-gallery {
padding: 60px 0;
background: #1a1a1a;
}
.pages-gallery-grid {
display: grid;
grid-template-columns: repeat(var(--columns, 3), 1fr);
gap: 16px;
}
.pages-gallery-item {
position: relative;
overflow: hidden;
border-radius: 8px;
aspect-ratio: 1;
background: #2d2d2d;
}
.pages-gallery-item a {
display: block;
width: 100%;
height: 100%;
}
.pages-gallery-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease, filter 0.3s ease;
}
.pages-gallery-item:hover .pages-gallery-image {
transform: scale(1.05);
filter: brightness(0.8);
}
.pages-gallery-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background: linear-gradient(transparent, rgba(0,0,0,0.8));
color: #fff;
font-size: 0.875rem;
font-weight: 500;
opacity: 0;
transform: translateY(10px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.pages-gallery-item:hover .pages-gallery-caption {
opacity: 1;
transform: translateY(0);
}
.pages-portfolio .pages-readme {
background: #2d2d2d;
padding: 60px 0;
}
.pages-portfolio .pages-readme .markup {
color: rgba(255,255,255,0.9);
}
.pages-portfolio .pages-readme .markup h1,
.pages-portfolio .pages-readme .markup h2,
.pages-portfolio .pages-readme .markup h3 {
color: #fff;
}
.pages-portfolio .pages-readme .markup a {
color: var(--pages-primary);
}
.pages-portfolio .pages-readme .markup code {
background: rgba(255,255,255,0.1);
color: #fff;
}
.pages-portfolio .pages-footer {
background: #111;
}
/* Empty gallery placeholder */
.pages-gallery-empty {
grid-column: 1 / -1;
text-align: center;
padding: 60px;
color: rgba(255,255,255,0.5);
}
.pages-gallery-empty-icon {
font-size: 3rem;
margin-bottom: 16px;
}
@media (max-width: 768px) {
.pages-gallery-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.pages-gallery-grid {
grid-template-columns: 1fr;
}
}
</style>
<div class="pages-landing pages-portfolio">
{{template "pages/header" .}}
<main class="pages-main">
@ -20,7 +146,7 @@
</a>
{{end}}
{{if .Config.Hero.CTASecondary.Text}}
<a href="{{.Config.Hero.CTASecondary.Link}}" class="ui button">
<a href="{{.Config.Hero.CTASecondary.Link}}" class="ui button basic inverted">
{{.Config.Hero.CTASecondary.Text}}
</a>
{{end}}
@ -29,28 +155,31 @@
</section>
<!-- Gallery Section -->
{{if .Config.Gallery.Items}}
<section class="pages-gallery">
<div class="container">
<div class="pages-gallery-grid" style="--columns: {{if .Config.Gallery.Columns}}{{.Config.Gallery.Columns}}{{else}}4{{end}}">
{{if .Config.Gallery.Items}}
<div class="pages-gallery-grid" style="--columns: {{if .Config.Gallery.Columns}}{{.Config.Gallery.Columns}}{{else}}3{{end}}">
{{range .Config.Gallery.Items}}
<div class="pages-gallery-item">
{{if .Link}}
<a href="{{.Link}}">
{{end}}
{{if .Link}}<a href="{{.Link}}">{{end}}
<img src="{{.Image}}" alt="{{.Title}}" class="pages-gallery-image">
{{if .Title}}
<div class="pages-gallery-caption">{{.Title}}</div>
{{end}}
{{if .Link}}
</a>
{{end}}
{{if .Link}}</a>{{end}}
</div>
{{end}}
</div>
{{else}}
<div class="pages-gallery-grid" style="--columns: 3">
<div class="pages-gallery-empty">
{{svg "octicon-image" 48}}
<p>Add gallery items in your landing.yaml configuration</p>
</div>
</div>
{{end}}
</div>
</section>
{{end}}
<!-- README Section -->
{{if .ReadmeContent}}
@ -66,4 +195,4 @@
{{template "pages/footer" .}}
</div>
{{template "base/footer" .}}
{{template "pages/base_footer" .}}

View File

@ -1,11 +1,121 @@
{{template "base/head" .}}
<div class="page-content pages-landing pages-product">
{{template "pages/base_head" .}}
<style>
/* Product-specific styles - Bold marketing design */
.pages-product {
background: #fff;
}
.pages-product .pages-header {
background: transparent;
border-bottom: none;
position: absolute;
width: 100%;
z-index: 100;
}
.pages-product .pages-nav-brand,
.pages-product .pages-nav-link {
color: #fff;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.pages-hero-product {
background: linear-gradient(135deg, var(--pages-primary) 0%, #1a365d 100%);
color: #fff;
padding: 120px 0 80px;
position: relative;
overflow: hidden;
}
.pages-hero-product::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
}
.pages-hero-product .container {
position: relative;
z-index: 1;
}
.pages-product .pages-title {
color: #fff;
font-size: 3.5rem;
margin-bottom: 24px;
}
.pages-product .pages-tagline {
color: rgba(255,255,255,0.9);
font-size: 1.5rem;
max-width: 700px;
}
.pages-product .pages-logo {
max-height: 100px;
filter: brightness(0) invert(1);
}
.pages-features {
padding: 80px 0;
background: #fff;
}
.pages-features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 48px;
}
.pages-feature {
text-align: center;
padding: 32px;
}
.pages-feature-icon {
width: 64px;
height: 64px;
margin: 0 auto 24px;
background: linear-gradient(135deg, var(--pages-primary) 0%, #1a365d 100%);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.pages-feature-icon img {
max-width: 32px;
max-height: 32px;
}
.pages-feature-title {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 12px;
color: #24292e;
}
.pages-feature-description {
color: #586069;
line-height: 1.6;
margin: 0;
}
.pages-product .pages-stats {
background: linear-gradient(135deg, #f6f8fa 0%, #e1e4e8 100%);
padding: 64px 0;
}
.pages-cta-bottom {
margin-top: 32px;
text-align: center;
}
.pages-product .pages-readme {
background: #fff;
padding: 80px 0;
}
@media (max-width: 768px) {
.pages-product .pages-title {
font-size: 2.5rem;
}
.pages-hero-product {
padding: 100px 0 60px;
}
}
</style>
<div class="pages-landing pages-product">
{{template "pages/header" .}}
<main class="pages-main">
<!-- Hero Section -->
<section class="pages-hero pages-hero-product" {{if .Config.Hero.Background}}style="background-image: url('{{.Config.Hero.Background}}')"{{end}}>
<div class="pages-hero-overlay"></div>
<div class="container">
{{if .Config.Branding.Logo}}
<img src="{{.Config.Branding.Logo}}" alt="{{.Repository.Name}}" class="pages-logo">
@ -16,12 +126,12 @@
{{end}}
<div class="pages-cta">
{{if .Config.Hero.CTAPrimary.Text}}
<a href="{{.Config.Hero.CTAPrimary.Link}}" class="ui large primary button">
<a href="{{.Config.Hero.CTAPrimary.Link}}" class="ui large primary button" style="background: #fff; color: var(--pages-primary);">
{{.Config.Hero.CTAPrimary.Text}}
</a>
{{end}}
{{if .Config.Hero.CTASecondary.Text}}
<a href="{{.Config.Hero.CTASecondary.Link}}" class="ui large button {{if eq .Config.Hero.CTASecondary.Style "outline"}}basic inverted{{end}}">
<a href="{{.Config.Hero.CTASecondary.Link}}" class="ui large button basic inverted">
{{.Config.Hero.CTASecondary.Text}}
</a>
{{end}}
@ -38,11 +148,7 @@
<div class="pages-feature">
{{if .Icon}}
<div class="pages-feature-icon">
{{if hasPrefix .Icon "./"}}
<img src="{{.Icon}}" alt="{{.Title}}">
{{else}}
{{svg (printf "octicon-%s" .Icon) 32}}
{{end}}
</div>
{{end}}
<h3 class="pages-feature-title">{{.Title}}</h3>
@ -89,4 +195,4 @@
{{template "pages/footer" .}}
</div>
{{template "base/footer" .}}
{{template "pages/base_footer" .}}

View File

@ -1,5 +1,5 @@
{{template "base/head" .}}
<div class="page-content pages-landing pages-simple">
{{template "pages/base_head" .}}
<div class="pages-landing pages-simple">
{{template "pages/header" .}}
<main class="pages-main">
@ -43,18 +43,15 @@
<div class="container">
<div class="pages-stats-grid">
<div class="pages-stat">
<span class="pages-stat-icon">{{svg "octicon-star"}}</span>
<span class="pages-stat-value">{{.NumStars}}</span>
<span class="pages-stat-label">{{ctx.Locale.Tr "repo.stars"}}</span>
</div>
<div class="pages-stat">
<span class="pages-stat-icon">{{svg "octicon-repo-forked"}}</span>
<span class="pages-stat-value">{{.NumForks}}</span>
<span class="pages-stat-label">{{ctx.Locale.Tr "repo.forks"}}</span>
</div>
{{if .Repository.PrimaryLanguage}}
<div class="pages-stat">
<span class="pages-stat-icon language-color" style="background-color: {{.Repository.PrimaryLanguage.Color}}"></span>
<span class="pages-stat-value">{{.Repository.PrimaryLanguage.Language}}</span>
<span class="pages-stat-label">{{ctx.Locale.Tr "repo.language"}}</span>
</div>
@ -66,4 +63,4 @@
{{template "pages/footer" .}}
</div>
{{template "base/footer" .}}
{{template "pages/base_footer" .}}

View File

@ -65,6 +65,7 @@
{{if not $.DisableStars}}
{{template "repo/star_unstar" $}}
{{end}}
{{template "repo/pin_unpin" $}}
{{if and (not .IsEmpty) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}}
<div class="ui labeled button
{{if or (not $.IsSigned) (and (not $.CanSignedUserFork) (not $.UserAndOrgForks))}}
@ -154,7 +155,7 @@
</a>
{{end}}
{{if and .Repository.CanEnablePulls (.Permission.CanRead ctx.Consts.RepoUnitTypePullRequests)}}
{{if and ctx.IsSigned .Repository.CanEnablePulls (.Permission.CanRead ctx.Consts.RepoUnitTypePullRequests)}}
<a class="{{if .PageIsPullList}}active {{end}}item" href="{{.RepoLink}}/pulls">
{{svg "octicon-git-pull-request"}} {{ctx.Locale.Tr "repo.pulls"}}
{{if .Repository.NumOpenPulls}}
@ -163,7 +164,7 @@
</a>
{{end}}
{{if and .EnableActions (.Permission.CanRead ctx.Consts.RepoUnitTypeActions) (not .IsEmptyRepo)}}
{{if and .EnableActions (.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo)}}
<a class="{{if .PageIsActions}}active {{end}}item" href="{{.RepoLink}}/actions">
{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}
{{if .Repository.NumOpenActionRuns}}
@ -179,7 +180,7 @@
{{end}}
{{$projectsUnit := .Repository.MustGetUnit ctx ctx.Consts.RepoUnitTypeProjects}}
{{if and (not ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
{{if and ctx.IsSigned (not ctx.Consts.RepoUnitTypeProjects.UnitGlobalDisabled) (.Permission.CanRead ctx.Consts.RepoUnitTypeProjects) ($projectsUnit.ProjectsConfig.IsProjectsAllowed "repo")}}
<a href="{{.RepoLink}}/projects" class="{{if .IsProjectsPage}}active {{end}}item">
{{svg "octicon-project"}} {{ctx.Locale.Tr "repo.projects"}}
{{if .Repository.NumOpenProjects}}
@ -209,7 +210,7 @@
</a>
{{end}}
{{if and (.Permission.CanReadAny ctx.Consts.RepoUnitTypePullRequests ctx.Consts.RepoUnitTypeIssues ctx.Consts.RepoUnitTypeReleases ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo)}}
{{if and (.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo)}}
<a class="{{if .PageIsActivity}}active {{end}}item" href="{{.RepoLink}}/activity">
{{svg "octicon-pulse"}} {{ctx.Locale.Tr "repo.activity"}}
</a>

View File

@ -0,0 +1,33 @@
{{if $.IsSigned}}
<div class="ui labeled button" id="pin-repo-dropdown">
<div class="ui compact small basic button dropdown" data-tooltip-content="{{ctx.Locale.Tr "repo.pin.tooltip"}}">
{{svg "octicon-pin" 16}}<span class="text not-mobile">{{ctx.Locale.Tr "repo.pin"}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
{{/* Pin to user profile */}}
{{if $.IsRepoPinnedToUser}}
<a class="item" href="{{$.RepoLink}}/action/unpin?type=user&redirect_to={{$.Link}}">
{{svg "octicon-pin-slash" 16}} {{ctx.Locale.Tr "repo.pin.unpin_from_profile"}}
</a>
{{else}}
<a class="item" href="{{$.RepoLink}}/action/pin?type=user&redirect_to={{$.Link}}">
{{svg "octicon-person" 16}} {{ctx.Locale.Tr "repo.pin.pin_to_profile"}}
</a>
{{end}}
{{/* Pin to organization (if applicable) */}}
{{if and .Repository.Owner.IsOrganization $.IsOrganizationMember}}
{{if $.IsRepoPinnedToOrg}}
<a class="item" href="{{$.RepoLink}}/action/unpin?type=org&redirect_to={{$.Link}}">
{{svg "octicon-pin-slash" 16}} {{ctx.Locale.Tr "repo.pin.unpin_from_org"}}
</a>
{{else}}
<a class="item" href="{{$.RepoLink}}/action/pin?type=org&redirect_to={{$.Link}}">
{{svg "octicon-organization" 16}} {{ctx.Locale.Tr "repo.pin.pin_to_org"}}
</a>
{{end}}
{{end}}
</div>
</div>
</div>
{{end}}

View File

@ -88,12 +88,44 @@
{{$release.RenderedNote}}
</div>
<div class="divider"></div>
<details class="download" {{if eq $idx 0}}open{{end}}>
{{/* For the first/latest release, show downloads directly without collapsible */}}
{{if eq $idx 0}}
<div class="download-section">
{{else}}
<details class="download">
<summary>
{{ctx.Locale.Tr "repo.release.downloads"}}
</summary>
<ul class="ui divided list attachment-list">
{{end}}
{{/* Group attachments by OS */}}
{{$windowsFiles := newSlice}}
{{$macosFiles := newSlice}}
{{$linuxFiles := newSlice}}
{{$otherFiles := newSlice}}
{{range $att := $release.Attachments}}
{{$name := StringUtils.ToLower $att.Name}}
{{if or (StringUtils.Contains $name "windows") (StringUtils.Contains $name "win64") (StringUtils.Contains $name "win32") (StringUtils.Contains $name "-win.") (StringUtils.Contains $name "_win.") (StringUtils.Contains $name "-win-") (StringUtils.Contains $name "_win_") (StringUtils.Contains $name ".exe") (StringUtils.Contains $name ".msi")}}
{{$windowsFiles = Append $windowsFiles $att}}
{{else if or (StringUtils.Contains $name "darwin") (StringUtils.Contains $name "macos") (StringUtils.Contains $name "-mac.") (StringUtils.Contains $name "_mac.") (StringUtils.Contains $name "-mac-") (StringUtils.Contains $name "_mac_") (StringUtils.Contains $name "osx") (StringUtils.Contains $name ".dmg") (StringUtils.Contains $name ".pkg")}}
{{$macosFiles = Append $macosFiles $att}}
{{else if or (StringUtils.Contains $name "linux") (StringUtils.Contains $name "-lin.") (StringUtils.Contains $name "_lin.") (StringUtils.Contains $name "-lin-") (StringUtils.Contains $name "_lin_") (StringUtils.Contains $name ".deb") (StringUtils.Contains $name ".rpm") (StringUtils.Contains $name ".appimage")}}
{{$linuxFiles = Append $linuxFiles $att}}
{{else}}
{{$otherFiles = Append $otherFiles $att}}
{{end}}
{{end}}
{{/* Windows Downloads */}}
{{if $windowsFiles}}
<div class="tw-pt-2 tw-mb-3">
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2 tw-font-medium">
{{svg "octicon-device-desktop" 16}} Windows
</h5>
<ul class="ui divided list attachment-list tw-ml-4">
{{range $att := $windowsFiles}}
<li class="item">
<a target="_blank" class="tw-flex-1 gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}">
<strong class="flex-text-inline">{{svg "octicon-package" 16 "download-icon"}}<span class="gt-ellipsis">{{$att.Name}}</span></strong>
@ -108,7 +140,95 @@
</div>
</li>
{{end}}
</ul>
</div>
{{end}}
{{/* macOS Downloads */}}
{{if $macosFiles}}
<div class="tw-pt-2 tw-mb-3">
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2 tw-font-medium">
{{svg "octicon-device-desktop" 16}} macOS
</h5>
<ul class="ui divided list attachment-list tw-ml-4">
{{range $att := $macosFiles}}
<li class="item">
<a target="_blank" class="tw-flex-1 gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}">
<strong class="flex-text-inline">{{svg "octicon-package" 16 "download-icon"}}<span class="gt-ellipsis">{{$att.Name}}</span></strong>
</a>
<div class="attachment-right-info flex-text-inline">
<span class="tw-pl-5">{{$att.Size | FileSize}}</span>
<span class="flex-text-inline" data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber $att.DownloadCount)}}">
{{svg "octicon-info"}}
</span>
<div class="tw-flex-1"></div>
{{DateUtils.TimeSince $att.CreatedUnix}}
</div>
</li>
{{end}}
</ul>
</div>
{{end}}
{{/* Linux Downloads */}}
{{if $linuxFiles}}
<div class="tw-pt-2 tw-mb-3">
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2 tw-font-medium">
{{svg "octicon-terminal" 16}} Linux
</h5>
<ul class="ui divided list attachment-list tw-ml-4">
{{range $att := $linuxFiles}}
<li class="item">
<a target="_blank" class="tw-flex-1 gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}">
<strong class="flex-text-inline">{{svg "octicon-package" 16 "download-icon"}}<span class="gt-ellipsis">{{$att.Name}}</span></strong>
</a>
<div class="attachment-right-info flex-text-inline">
<span class="tw-pl-5">{{$att.Size | FileSize}}</span>
<span class="flex-text-inline" data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber $att.DownloadCount)}}">
{{svg "octicon-info"}}
</span>
<div class="tw-flex-1"></div>
{{DateUtils.TimeSince $att.CreatedUnix}}
</div>
</li>
{{end}}
</ul>
</div>
{{end}}
{{/* Other Downloads */}}
{{if $otherFiles}}
<div class="tw-pt-2 tw-mb-3">
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2 tw-font-medium">
{{svg "octicon-file" 16}} Other
</h5>
<ul class="ui divided list attachment-list tw-ml-4">
{{range $att := $otherFiles}}
<li class="item">
<a target="_blank" class="tw-flex-1 gt-ellipsis" rel="nofollow" download href="{{$att.DownloadURL}}">
<strong class="flex-text-inline">{{svg "octicon-package" 16 "download-icon"}}<span class="gt-ellipsis">{{$att.Name}}</span></strong>
</a>
<div class="attachment-right-info flex-text-inline">
<span class="tw-pl-5">{{$att.Size | FileSize}}</span>
<span class="flex-text-inline" data-tooltip-content="{{ctx.Locale.Tr "repo.release.download_count" (ctx.Locale.PrettyNumber $att.DownloadCount)}}">
{{svg "octicon-info"}}
</span>
<div class="tw-flex-1"></div>
{{DateUtils.TimeSince $att.CreatedUnix}}
</div>
</li>
{{end}}
</ul>
</div>
{{end}}
{{/* Source Code Archives */}}
{{if and (not $.DisableDownloadSourceArchives) (not $release.IsDraft) ($.Permission.CanRead ctx.Consts.RepoUnitTypeCode)}}
<div class="tw-pt-2 tw-mb-3">
<h5 class="tw-flex tw-items-center tw-gap-2 tw-mb-2 tw-font-medium">
{{svg "octicon-code" 16}} Source Code
</h5>
<ul class="ui divided list attachment-list tw-ml-4">
<li class="item">
<a class="archive-link" download href="{{$.RepoLink}}/archive/{{$release.TagName | PathEscapeSegments}}.zip" rel="nofollow">
<strong class="flex-text-inline">{{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (ZIP)</strong>
@ -119,9 +239,15 @@
<strong class="flex-text-inline">{{svg "octicon-file-zip" 16 "download-icon"}}{{ctx.Locale.Tr "repo.release.source_code"}} (TAR.GZ)</strong>
</a>
</li>
{{end}}
</ul>
</div>
{{end}}
{{if eq $idx 0}}
</div>
{{else}}
</details>
{{end}}
</div>
</li>
{{end}}

View File

@ -3,13 +3,19 @@
{{if $canReadReleases}}
<div class="flex-text-block">
<div class="tw-flex-1 tw-flex tw-items-center">
<div class="tw-flex-1 tw-flex tw-items-center tw-gap-4">
<h2 class="ui compact small menu small-menu-items">
<a class="{{if and .PageIsReleaseList (not .PageIsSingleTag)}}active {{end}}item" href="{{.RepoLink}}/releases">{{ctx.Locale.PrettyNumber .NumReleases}} {{ctx.Locale.TrN .NumReleases "repo.release" "repo.releases"}}</a>
{{if $canReadCode}}
<a class="{{if or .PageIsTagList .PageIsSingleTag}}active {{end}}item" href="{{.RepoLink}}/tags">{{ctx.Locale.PrettyNumber .NumTags}} {{ctx.Locale.TrN .NumTags "repo.tag" "repo.tags"}}</a>
{{end}}
</h2>
{{if and .PageIsReleaseList (not .PageIsSingleTag)}}
<div class="ui checkbox">
<input type="checkbox" id="show-archived" {{if .ShowArchived}}checked{{end}} onchange="window.location.href='{{.RepoLink}}/releases?archived=' + (this.checked ? 'true' : 'false')">
<label for="show-archived">{{ctx.Locale.Tr "repo.release.show_archived"}}</label>
</div>
{{end}}
</div>
{{if .EnableFeed}}
<a class="ui small button" href="{{.RepoLink}}/{{if .PageIsTagList}}tags{{else}}releases{{end}}.rss">
@ -22,14 +28,6 @@
</a>
{{end}}
</div>
{{if and .PageIsReleaseList (not .PageIsSingleTag)}}
<div class="tw-flex tw-items-center tw-mb-2">
<div class="ui checkbox">
<input type="checkbox" id="show-archived" {{if .ShowArchived}}checked{{end}} onchange="window.location.href='{{.RepoLink}}/releases?archived=' + (this.checked ? 'true' : 'false')">
<label for="show-archived">{{ctx.Locale.Tr "repo.release.show_archived"}}</label>
</div>
</div>
{{end}}
<div class="divider"></div>
{{else if $canReadCode}}
{{/* if the "repo.releases" unit is disabled, only show the "commits / branches / tags" sub menu */}}

View File

@ -8,7 +8,7 @@
<div class="ui positive message">
<div class="header">{{ctx.Locale.Tr "repo.settings.pages.enabled"}}</div>
<p>{{ctx.Locale.Tr "repo.settings.pages.enabled_desc"}}</p>
<p><strong>{{ctx.Locale.Tr "repo.settings.pages.subdomain"}}:</strong> <code>{{.PagesSubdomain}}</code></p>
<p><strong>{{ctx.Locale.Tr "repo.settings.pages.subdomain"}}:</strong> <a href="{{.PagesURL}}" target="_blank" rel="noopener noreferrer">{{.PagesURL}}</a></p>
</div>
<form class="ui form" method="post">
@ -104,6 +104,13 @@
{{end}}
</td>
<td class="tw-text-right">
{{if and .Verified (eq .SSLStatus "pending")}}
<form method="post" class="tw-inline-block">
<input type="hidden" name="action" value="activate_ssl">
<input type="hidden" name="domain_id" value="{{.ID}}">
<button class="ui green tiny button">{{ctx.Locale.Tr "repo.settings.pages.activate_ssl"}}</button>
</form>
{{end}}
{{if not .Verified}}
<form method="post" class="tw-inline-block">
<input type="hidden" name="action" value="verify_domain">
@ -118,6 +125,13 @@
</form>
</td>
</tr>
{{if and .Verified (eq .SSLStatus "pending")}}
<form method="post" class="tw-inline-block">
<input type="hidden" name="action" value="activate_ssl">
<input type="hidden" name="domain_id" value="{{.ID}}">
<button class="ui green tiny button">{{ctx.Locale.Tr "repo.settings.pages.activate_ssl"}}</button>
</form>
{{end}}
{{if not .Verified}}
<tr>
<td colspan="4">
@ -138,6 +152,13 @@
<div class="inline field">
<label>{{ctx.Locale.Tr "repo.settings.pages.add_domain"}}</label>
<input name="domain" type="text" placeholder="example.com">
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="ssl_external" id="ssl_external">
<label for="ssl_external">{{ctx.Locale.Tr "repo.settings.pages.ssl_external"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.pages.ssl_external_desc"}}</p>
</div>
<button class="ui primary button">{{ctx.Locale.Tr "repo.settings.pages.add"}}</button>
</div>
</form>

View File

@ -3,73 +3,197 @@
{{ctx.Locale.Tr "actions.runners.runner_title"}} {{.Runner.ID}} {{.Runner.Name}}
</h4>
<div class="ui attached segment">
<form class="ui form" method="post">
{{template "base/disable_form_autofill"}}
<div class="runner-basic-info">
<div class="field tw-inline-block tw-mr-4">
<label>{{ctx.Locale.Tr "actions.runners.status"}}</label>
<span class="ui {{if .Runner.IsOnline}}green{{else}}basic{{end}} label">{{.Runner.StatusLocaleName ctx.Locale}}</span>
</div>
<div class="field tw-inline-block tw-mr-4">
<label>{{ctx.Locale.Tr "actions.runners.last_online"}}</label>
<span>{{if .Runner.LastOnline}}{{DateUtils.TimeSince .Runner.LastOnline}}{{else}}{{ctx.Locale.Tr "never"}}{{end}}</span>
</div>
<div class="field tw-inline-block tw-mr-4">
<label>{{ctx.Locale.Tr "actions.runners.labels"}}</label>
<span>
{{range .Runner.AgentLabels}}
<span class="ui label">{{.}}</span>
{{end}}
<!-- Health Status Tiles -->
<div class="ui three column stackable grid tw-mb-4">
<div class="column">
<div class="ui segment tw-text-center">
<div class="tw-text-sm tw-mb-1" style="opacity: 0.8;">Status</div>
<span class="ui {{if .Runner.IsOnline}}green{{else}}red{{end}} large label">
{{if .Runner.IsOnline}}{{svg "octicon-check-circle" 16}}{{else}}{{svg "octicon-x-circle" 16}}{{end}}
{{.Runner.StatusLocaleName ctx.Locale}}
</span>
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">
{{if .Runner.IsOnline}}Connected{{else if .Runner.LastOnline}}Last seen {{DateUtils.TimeSince .Runner.LastOnline}}{{else}}Never connected{{end}}
</div>
</div>
</div>
<div class="column">
<div class="ui segment tw-text-center">
<div class="tw-text-sm tw-mb-1" style="opacity: 0.8;">Disk Space</div>
{{if and .RunnerCapabilities .RunnerCapabilities.Disk}}
{{$diskUsed := .RunnerCapabilities.Disk.UsedPercent}}
{{$diskFreeGB := DivideFloat64 (Int64ToFloat64 .RunnerCapabilities.Disk.Free) 1073741824.0}}
{{$diskTotalGB := DivideFloat64 (Int64ToFloat64 .RunnerCapabilities.Disk.Total) 1073741824.0}}
<span class="ui {{if ge $diskUsed 95.0}}red{{else if ge $diskUsed 85.0}}yellow{{else}}green{{end}} large label">
{{if ge $diskUsed 95.0}}{{svg "octicon-alert" 16}}{{else if ge $diskUsed 85.0}}{{svg "octicon-alert" 16}}{{else}}{{svg "octicon-database" 16}}{{end}}
{{printf "%.0f" $diskUsed}}% used
</span>
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">
{{printf "%.1f" $diskFreeGB}} GB free of {{printf "%.0f" $diskTotalGB}} GB
</div>
{{else}}
<span class="ui grey large label">{{svg "octicon-database" 16}} No data</span>
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">Waiting for report</div>
{{end}}
</div>
</div>
<div class="column">
<div class="ui segment tw-text-center">
<div class="tw-text-sm tw-mb-1" style="opacity: 0.8;">Network</div>
{{if and .RunnerCapabilities .RunnerCapabilities.Bandwidth}}
<span class="ui {{if ge .RunnerCapabilities.Bandwidth.DownloadMbps 100.0}}green{{else if ge .RunnerCapabilities.Bandwidth.DownloadMbps 10.0}}blue{{else}}yellow{{end}} large label">
{{svg "octicon-arrow-down" 16}} {{printf "%.0f" .RunnerCapabilities.Bandwidth.DownloadMbps}} Mbps
</span>
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">
{{if gt .RunnerCapabilities.Bandwidth.Latency 0.0}}{{printf "%.0f" .RunnerCapabilities.Bandwidth.Latency}} ms latency{{end}}
{{if .RunnerCapabilities.Bandwidth.TestedAt}}- tested {{DateUtils.TimeSince .RunnerCapabilities.Bandwidth.TestedAt}}{{end}}
</div>
{{else}}
<span class="ui grey large label">{{svg "octicon-globe" 16}} No data</span>
<div class="tw-text-xs tw-mt-2" style="opacity: 0.7;">Waiting for test</div>
{{end}}
</div>
<div class="field tw-inline-block tw-mr-4">
<label>{{ctx.Locale.Tr "actions.runners.owner_type"}}</label>
<span data-tooltip-content="{{.Runner.BelongsToOwnerName}}">{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}</span>
</div>
</div>
{{if .Runner.CapabilitiesJSON}}
<div class="divider"></div>
<div class="field">
<label>{{ctx.Locale.Tr "actions.runners.capabilities"}}</label>
<div class="ui segment runner-capabilities">
<div class="ui two column stackable grid">
<!-- Left Column: Runner Info & Controls -->
<div class="column">
<div class="ui segment">
<h5 class="ui header">Runner Information</h5>
<table class="ui very basic table">
<tbody>
<tr>
<td style="width: 100px; opacity: 0.8;">Version</td>
<td><span class="ui small blue label">{{.Runner.Version}}</span></td>
</tr>
<tr>
<td style="opacity: 0.8;">Owner</td>
<td data-tooltip-content="{{.Runner.BelongsToOwnerName}}">{{.Runner.BelongsToOwnerType.LocaleString ctx.Locale}}</td>
</tr>
<tr>
<td style="opacity: 0.8;">Labels</td>
<td>
{{range .Runner.AgentLabels}}
<form method="post" action="{{$.Link}}/remove-label" style="display:inline;">
{{$.CsrfTokenHtml}}
<input type="hidden" name="label" value="{{.}}">
<button type="submit" class="ui small blue label tw-my-1" style="cursor:pointer;">{{.}} {{svg "octicon-x" 12}}</button>
</form>
{{end}}
{{if not .Runner.AgentLabels}}<span style="opacity: 0.6;">No labels</span>{{end}}
</td>
</tr>
</tbody>
</table>
</div>
<!-- Suggested Labels Section -->
{{if .RunnerCapabilities}}
<div class="ui segment">
<h5 class="ui header">{{svg "octicon-light-bulb" 16}} Suggested Labels</h5>
<p class="tw-text-sm tw-mb-2" style="opacity: 0.7;">Based on detected capabilities. Click + to add individually.</p>
<div class="tw-flex tw-flex-wrap tw-gap-2" id="suggested-labels">
{{$labels := .Runner.AgentLabels}}
{{if eq .RunnerCapabilities.OS "linux"}}
{{if not (SliceUtils.Contains $labels "linux")}}
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="linux"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} linux</button></form>
{{else}}<span class="ui small teal label">linux</span>{{end}}
{{if not (SliceUtils.Contains $labels "linux-latest")}}
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="linux-latest"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} linux-latest</button></form>
{{else}}<span class="ui small teal label">linux-latest</span>{{end}}
{{else if eq .RunnerCapabilities.OS "windows"}}
{{if not (SliceUtils.Contains $labels "windows")}}
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="windows"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} windows</button></form>
{{else}}<span class="ui small teal label">windows</span>{{end}}
{{if not (SliceUtils.Contains $labels "windows-latest")}}
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="windows-latest"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} windows-latest</button></form>
{{else}}<span class="ui small teal label">windows-latest</span>{{end}}
{{else if eq .RunnerCapabilities.OS "darwin"}}
{{if not (SliceUtils.Contains $labels "macos")}}
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="macos"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} macos</button></form>
{{else}}<span class="ui small teal label">macos</span>{{end}}
{{if not (SliceUtils.Contains $labels "macos-latest")}}
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="macos-latest"><button type="submit" class="ui small teal label" style="cursor:pointer;">{{svg "octicon-plus" 12}} macos-latest</button></form>
{{else}}<span class="ui small teal label">macos-latest</span>{{end}}
{{end}}
{{if and .RunnerCapabilities.Distro .RunnerCapabilities.Distro.ID}}
{{$distro := .RunnerCapabilities.Distro.ID}}
{{$distroLatest := printf "%s-latest" .RunnerCapabilities.Distro.ID}}
{{if not (SliceUtils.Contains $labels $distro)}}
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="{{$distro}}"><button type="submit" class="ui small purple label" style="cursor:pointer;">{{svg "octicon-plus" 12}} {{$distro}}</button></form>
{{else}}<span class="ui small purple label">{{$distro}}</span>{{end}}
{{if not (SliceUtils.Contains $labels $distroLatest)}}
<form method="post" action="{{$.Link}}/add-label" style="display:inline;">{{$.CsrfTokenHtml}}<input type="hidden" name="label" value="{{$distroLatest}}"><button type="submit" class="ui small purple label" style="cursor:pointer;">{{svg "octicon-plus" 12}} {{$distroLatest}}</button></form>
{{else}}<span class="ui small purple label">{{$distroLatest}}</span>{{end}}
{{end}}
</div>
</div>
{{end}}
<form class="ui form" method="post">
{{template "base/disable_form_autofill"}}
<div class="ui segment">
<h5 class="ui header">AI Instructions</h5>
<p class="tw-text-sm tw-mb-2" style="opacity: 0.7;">Additional context for AI when selecting this runner for jobs.</p>
<div class="field">
<textarea id="description" name="description" rows="3" placeholder="e.g., Use for heavy builds, has GPU, limited to 2 concurrent jobs...">{{.Runner.Description}}</textarea>
</div>
</div>
</form>
</div>
<!-- Right Column: Capabilities -->
<div class="column">
{{if .Runner.CapabilitiesJSON}}
<div class="ui segment runner-capabilities">
<h5 class="ui header">{{ctx.Locale.Tr "actions.runners.capabilities"}}</h5>
{{if .RunnerCapabilities}}
<div class="tw-flex tw-flex-wrap tw-gap-4">
{{if .RunnerCapabilities.OS}}
<div class="tw-inline-block tw-mr-4">
<strong>{{ctx.Locale.Tr "actions.runners.capabilities.os"}}:</strong>
<span class="ui label">{{.RunnerCapabilities.OS}}/{{.RunnerCapabilities.Arch}}</span>
<div class="field tw-mb-3">
<label>{{ctx.Locale.Tr "actions.runners.capabilities.os"}}</label>
<span class="ui small blue label">{{.RunnerCapabilities.OS}}/{{.RunnerCapabilities.Arch}}</span>
{{if and .RunnerCapabilities.Distro .RunnerCapabilities.Distro.PrettyName}}
<span class="ui small label">{{.RunnerCapabilities.Distro.PrettyName}}</span>
{{end}}
</div>
{{end}}
<div class="field tw-mb-3">
<label>{{ctx.Locale.Tr "actions.runners.capabilities.docker"}}</label>
{{if .RunnerCapabilities.Docker}}
<div class="tw-inline-block tw-mr-4">
<strong>{{ctx.Locale.Tr "actions.runners.capabilities.docker"}}:</strong>
<span class="ui green label">{{svg "octicon-check" 14}} {{ctx.Locale.Tr "actions.runners.capabilities.available"}}</span>
</div>
<span class="ui small green label">{{svg "octicon-check" 14}} Available</span>
{{else}}
<span class="ui small orange label">{{svg "octicon-x" 14}} Not available</span>
{{end}}
</div>
{{if .RunnerCapabilities.Shell}}
<div class="tw-inline-block tw-mr-4">
<strong>{{ctx.Locale.Tr "actions.runners.capabilities.shells"}}:</strong>
<div class="field tw-mb-3">
<label>{{ctx.Locale.Tr "actions.runners.capabilities.shells"}}</label>
<div>
{{range .RunnerCapabilities.Shell}}
<span class="ui label">{{.}}</span>
<span class="ui small teal label tw-mr-1">{{.}}</span>
{{end}}
</div>
{{end}}
</div>
{{end}}
{{if .RunnerCapabilities.Tools}}
<div class="tw-mt-2">
<strong>{{ctx.Locale.Tr "actions.runners.capabilities.tools"}}:</strong>
<div class="tw-flex tw-flex-wrap tw-gap-2 tw-mt-1">
<div class="field tw-mb-3">
<label>{{ctx.Locale.Tr "actions.runners.capabilities.tools"}}</label>
<div class="tw-flex tw-flex-wrap tw-gap-1">
{{range $tool, $versions := .RunnerCapabilities.Tools}}
<span class="ui label">{{$tool}} {{range $versions}}{{.}} {{end}}</span>
<span class="ui small purple label">{{$tool}} {{range $versions}}{{.}} {{end}}</span>
{{end}}
</div>
</div>
{{end}}
{{if .RunnerCapabilities.Limitations}}
<div class="tw-mt-2">
<strong>{{ctx.Locale.Tr "actions.runners.capabilities.limitations"}}:</strong>
<ul class="tw-mt-1 tw-ml-4">
<div class="field tw-mb-3">
<label>{{ctx.Locale.Tr "actions.runners.capabilities.limitations"}}</label>
<ul class="tw-mt-1 tw-ml-4 tw-text-sm">
{{range .RunnerCapabilities.Limitations}}
<li>{{.}}</li>
{{end}}
@ -80,23 +204,39 @@
<pre class="tw-text-sm"><code>{{.Runner.CapabilitiesJSON}}</code></pre>
{{end}}
</div>
{{else}}
<div class="ui segment">
<h5 class="ui header">{{ctx.Locale.Tr "actions.runners.capabilities"}}</h5>
<p style="opacity: 0.7;">No capabilities reported</p>
</div>
{{end}}
<div class="divider"></div>
<div class="field">
<label for="description">{{ctx.Locale.Tr "actions.runners.description"}}</label>
<input id="description" name="description" value="{{.Runner.Description}}">
</div>
</div>
<div class="divider"></div>
<div class="field">
<button class="ui primary button" data-url="{{.Link}}">{{ctx.Locale.Tr "actions.runners.update_runner"}}</button>
<button class="ui red button delete-button" data-url="{{.Link}}/delete" data-modal="#runner-delete-modal">
{{ctx.Locale.Tr "actions.runners.delete_runner"}}</button>
<!-- Action Buttons - Full Width -->
<div class="tw-flex tw-gap-2 tw-flex-wrap tw-mt-4">
<button class="ui primary button" form="runner-form" data-url="{{.Link}}">
{{svg "octicon-check" 14}} Update Instructions
</button>
{{if .RunnerCapabilities}}
<button class="ui teal button" type="button" onclick="document.getElementById('suggested-labels-form').submit()">
{{svg "octicon-light-bulb" 14}} Use All Suggested Labels
</button>
{{end}}
<button class="ui secondary button" type="button" onclick="document.getElementById('bandwidth-form').submit()">
{{svg "octicon-sync" 14}} Check Bandwidth
</button>
<button class="ui red button delete-button" data-url="{{.Link}}/delete" data-modal="#runner-delete-modal" type="button">
{{svg "octicon-trash" 14}} Delete
</button>
</div>
<!-- Hidden Forms -->
<form id="bandwidth-form" method="post" action="{{.Link}}/bandwidth-test" style="display:none">
{{.CsrfTokenHtml}}
</form>
<form id="suggested-labels-form" method="post" action="{{.Link}}/use-suggested-labels" style="display:none">
{{.CsrfTokenHtml}}
</form>
</div>
@ -148,3 +288,94 @@
{{template "base/modal_actions_confirm" .}}
</div>
</div>
<script>
(function() {
const statusUrl = '{{.Link}}/status';
const pollInterval = 10000; // 10 seconds
function formatBytes(bytes) {
const gb = bytes / 1073741824;
return gb.toFixed(1) + ' GB';
}
function updateStatus() {
fetch(statusUrl, {
headers: {'Accept': 'application/json'}
})
.then(response => response.json())
.then(data => {
// Update status tile
const statusTile = document.querySelector('.runner-container .column:first-child .segment');
if (statusTile) {
const statusLabel = statusTile.querySelector('.label');
const statusText = statusTile.querySelector('.tw-text-xs');
if (statusLabel) {
statusLabel.className = 'ui ' + (data.is_online ? 'green' : 'red') + ' large label';
statusLabel.innerHTML = (data.is_online ?
'<svg class="svg octicon-check-circle" width="16" height="16"><use xlink:href="#octicon-check-circle"></use></svg>' :
'<svg class="svg octicon-x-circle" width="16" height="16"><use xlink:href="#octicon-x-circle"></use></svg>') +
' ' + data.status;
}
if (statusText) {
statusText.textContent = data.is_online ? 'Connected' :
(data.last_online ? 'Last seen ' + new Date(data.last_online).toLocaleString() : 'Never connected');
}
}
// Update disk tile
if (data.disk) {
const diskTile = document.querySelector('.runner-container .column:nth-child(2) .segment');
if (diskTile) {
const diskLabel = diskTile.querySelector('.label');
const diskText = diskTile.querySelector('.tw-text-xs');
const usedPct = data.disk.used_percent;
if (diskLabel) {
const color = usedPct >= 95 ? 'red' : (usedPct >= 85 ? 'yellow' : 'green');
const icon = usedPct >= 85 ? 'octicon-alert' : 'octicon-database';
diskLabel.className = 'ui ' + color + ' large label';
diskLabel.innerHTML = '<svg class="svg ' + icon + '" width="16" height="16"><use xlink:href="#' + icon + '"></use></svg> ' +
Math.round(usedPct) + '% used';
}
if (diskText) {
diskText.textContent = formatBytes(data.disk.free_bytes) + ' free of ' + formatBytes(data.disk.total_bytes);
}
}
}
// Update bandwidth tile
if (data.bandwidth) {
const bwTile = document.querySelector('.runner-container .column:nth-child(3) .segment');
if (bwTile) {
const bwLabel = bwTile.querySelector('.label');
const bwText = bwTile.querySelector('.tw-text-xs');
const mbps = data.bandwidth.download_mbps;
if (bwLabel) {
const color = mbps >= 100 ? 'green' : (mbps >= 10 ? 'blue' : 'yellow');
bwLabel.className = 'ui ' + color + ' large label';
bwLabel.innerHTML = '<svg class="svg octicon-arrow-down" width="16" height="16"><use xlink:href="#octicon-arrow-down"></use></svg> ' +
Math.round(mbps) + ' Mbps';
}
if (bwText && data.bandwidth.latency_ms) {
let text = Math.round(data.bandwidth.latency_ms) + ' ms latency';
if (data.bandwidth.tested_at) {
text += ' - tested ' + new Date(data.bandwidth.tested_at).toLocaleString();
}
bwText.textContent = text;
}
}
}
})
.catch(err => console.log('Status poll error:', err));
}
// Start polling
setInterval(updateStatus, pollInterval);
})();
</script>

View File

@ -43,7 +43,7 @@
{{end}}
<div class="tw-mt-8 tw-text-center">
{{if or .SignedUser.IsAdmin .ShowFooterVersion}}<p>{{ctx.Locale.Tr "admin.config.app_ver"}}: {{AppVer}}</p>{{end}}
{{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message" "https://github.com/go-gitea/gitea/issues"}}</p>{{end}}
{{if .SignedUser.IsAdmin}}<p>{{ctx.Locale.Tr "error.report_message" "https://git.marketally.com/gitcaddy/gitea/issues"}}</p>{{end}}
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
<overflow-menu class="ui secondary pointing tabular borderless menu">
<div class="overflow-menu-items">
{{if and .HasUserProfileReadme .ContextUser.IsIndividual}}
{{if .ContextUser.IsIndividual}}
<a class="{{if eq .TabName "overview"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=overview">
{{svg "octicon-info"}} {{ctx.Locale.Tr "user.overview"}}
</a>

View File

@ -26,7 +26,87 @@
{{else if eq .TabName "followers"}}
{{template "repo/user_cards" .}}
{{else if eq .TabName "overview"}}
{{/* Activity Heatmap on Overview */}}
{{if and .ContextUser.ShowHeatmapOnProfile .HeatmapData}}
<div class="ui segment tw-mb-4">
<h4 class="ui header tw-flex tw-items-center">
{{svg "octicon-graph" 16}} {{ctx.Locale.Tr "user.activity_heatmap"}}
</h4>
{{template "user/heatmap" .}}
</div>
{{end}}
{{/* Pinned Repositories Section */}}
{{if or .UserPinnedRepos .IsContextUserProfile}}
<div class="ui segment pinned-repos-section tw-mb-4">
<h4 class="ui header tw-flex tw-items-center">
{{svg "octicon-pin" 16}} {{ctx.Locale.Tr "user.pinned_repos"}}
{{if .IsContextUserProfile}}
<span class="tw-ml-auto text grey tw-text-sm">{{ctx.Locale.Tr "user.pinned_repos_hint"}}</span>
{{end}}
</h4>
{{if .UserPinnedRepos}}
<div class="ui three stackable cards pinned-repos">
{{range .UserPinnedRepos}}
{{if .Repo}}
<a class="ui card" href="{{.Repo.Link}}">
<div class="content tw-text-center">
{{if .Repo.Avatar}}
<img class="tw-inline-block tw-rounded" style="max-width: 80px; max-height: 80px; object-fit: contain;" src="{{.Repo.RelAvatarLink ctx}}" alt="">
{{else}}
<div class="tw-inline-block tw-p-4">
{{if .Repo.IsPrivate}}{{svg "octicon-lock" 48}}{{else if .Repo.IsFork}}{{svg "octicon-repo-forked" 48}}{{else if .Repo.IsMirror}}{{svg "octicon-mirror" 48}}{{else}}{{svg "octicon-repo" 48}}{{end}}
</div>
{{end}}
<div class="header tw-mt-2">
{{if and .Repo.Owner (ne .Repo.OwnerID $.ContextUser.ID)}}
<span class="text grey">{{.Repo.Owner.Name}}/</span>
{{end}}
{{.Repo.Name}}
</div>
{{if .Repo.Description}}
<div class="description text grey tw-text-sm tw-mt-1">{{.Repo.Description}}</div>
{{end}}
</div>
<div class="extra content">
{{if .Repo.PrimaryLanguage}}
<span class="tw-mr-2">
<span class="repo-language-color" style="background-color: {{.Repo.PrimaryLanguage.Color}}"></span>
{{.Repo.PrimaryLanguage.Language}}
</span>
{{end}}
{{if .Repo.NumStars}}
<span class="tw-mr-2">{{svg "octicon-star" 14}} {{.Repo.NumStars}}</span>
{{end}}
{{if .Repo.NumForks}}
<span>{{svg "octicon-repo-forked" 14}} {{.Repo.NumForks}}</span>
{{end}}
</div>
</a>
{{end}}
{{end}}
</div>
{{else if .IsContextUserProfile}}
<div class="ui placeholder segment tw-text-center">
<div class="ui icon header">
{{svg "octicon-pin" 32}}
<div class="content">
{{ctx.Locale.Tr "user.pinned_repos_empty_title"}}
<div class="sub header">
{{ctx.Locale.Tr "user.pinned_repos_empty_desc"}}
</div>
</div>
</div>
</div>
{{end}}
</div>
{{end}}
{{/* Profile README */}}
{{if .ProfileReadmeContent}}
<div id="readme_profile" class="render-content markup">{{.ProfileReadmeContent}}</div>
{{end}}
{{else if eq .TabName "organizations"}}
{{template "repo/user_cards" .}}
{{else}}

View File

@ -88,6 +88,13 @@
</div>
</div>
<div class="field">
<div class="ui checkbox" id="show-heatmap-on-profile">
<label data-tooltip-content="{{ctx.Locale.Tr "settings.show_heatmap_on_profile_popup"}}"><strong>{{ctx.Locale.Tr "settings.show_heatmap_on_profile"}}</strong></label>
<input name="show_heatmap_on_profile" type="checkbox" {{if .SignedUser.ShowHeatmapOnProfile}}checked{{end}}>
</div>
</div>
<div class="divider"></div>
<div class="field">

View File

@ -57,3 +57,23 @@
then the layout from top to bottom is: size, filename, progress */
top: 7em;
}
/* Fix dark mode dropzone details */
.dropzone .dz-preview .dz-details {
background: var(--color-body) !important;
color: var(--color-text) !important;
}
.dropzone .dz-preview .dz-details .dz-size,
.dropzone .dz-preview .dz-details .dz-filename {
color: var(--color-text) !important;
}
.dropzone .dz-preview .dz-details .dz-filename span {
background: transparent !important;
}
.dropzone .dz-preview .dz-success-mark,
.dropzone .dz-preview .dz-error-mark {
background: var(--color-body) !important;
}