Compare commits
35 Commits
v1.26.6-gi
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15bd1d61c4 | ||
|
|
b569c3f8a8 | ||
|
|
ded40c34c5 | ||
|
|
e53c8fd040 | ||
|
|
a3c1aa3011 | ||
|
|
469551095b | ||
|
|
5ca3661c33 | ||
|
|
a68d691750 | ||
| 3a1075d6a0 | |||
| 6795122e00 | |||
| 2fc3e5a1c7 | |||
| 1af82412c0 | |||
| 5832d93f0a | |||
| 44f04a7866 | |||
| 2ba34c0abb | |||
| 1717a0c45c | |||
| e871e65342 | |||
| 8b8812f81c | |||
| 67ff066157 | |||
| 3fb751bc24 | |||
| 6cfd51e4c7 | |||
| 659e08da6c | |||
| d664ce29d8 | |||
| 4580e5c87f | |||
| 11b2ee48e9 | |||
| 85d73a2d85 | |||
| 54510ce582 | |||
| 1986d90df0 | |||
| 5b0442d357 | |||
| d44fea18d5 | |||
| e57b4f1654 | |||
| 69d7c72ba8 | |||
|
|
919746c756 | ||
|
|
853ff29ae2 | ||
|
|
7292421334 |
4
go.mod
4
go.mod
@ -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
6
go.sum
@ -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=
|
||||
|
||||
@ -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"`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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
137
models/user/user_pinned.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -206,7 +206,7 @@
|
||||
"filter.string.asc": "A–Z",
|
||||
"filter.string.desc": "Z–A",
|
||||
"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:",
|
||||
@ -3704,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",
|
||||
@ -3777,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."
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
134
routers/api/v2/runners.go
Normal 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)
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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
128
routers/web/repo/pin.go
Normal 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)
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 != "" {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -219,6 +219,7 @@ type UpdateProfileForm struct {
|
||||
Description string `binding:"MaxSize(255)"`
|
||||
Visibility structs.VisibleType
|
||||
KeepActivityPrivate bool
|
||||
ShowHeatmapOnProfile bool
|
||||
}
|
||||
|
||||
// Validate validates the fields
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
2
templates/pages/base_footer.tmpl
Normal file
2
templates/pages/base_footer.tmpl
Normal file
@ -0,0 +1,2 @@
|
||||
</body>
|
||||
</html>
|
||||
170
templates/pages/base_head.tmpl
Normal file
170
templates/pages/base_head.tmpl
Normal 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">
|
||||
@ -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" .}}
|
||||
|
||||
@ -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" .}}
|
||||
|
||||
@ -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 StringUtils.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" .}}
|
||||
|
||||
@ -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" .}}
|
||||
|
||||
@ -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>
|
||||
|
||||
33
templates/repo/pin_unpin.tmpl
Normal file
33
templates/repo/pin_unpin.tmpl
Normal 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}}
|
||||
@ -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}}
|
||||
|
||||
@ -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 */}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user