v2.1.3: Add AI Learning admin UI and server status dashboard tiles
Some checks are pending
Build and Release / Lint (push) Waiting to run
Build and Release / Unit Tests (push) Waiting to run
Build and Release / Integration Tests (PostgreSQL) (push) Waiting to run
Build and Release / Create Release (push) Waiting to run
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
Some checks are pending
Build and Release / Lint (push) Waiting to run
Build and Release / Unit Tests (push) Waiting to run
Build and Release / Integration Tests (PostgreSQL) (push) Waiting to run
Build and Release / Create Release (push) Waiting to run
Build and Release / Build Binaries (amd64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, linux) (push) Blocked by required conditions
Build and Release / Build Binaries (amd64, windows) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, darwin) (push) Blocked by required conditions
Build and Release / Build Binaries (arm64, linux) (push) Blocked by required conditions
- Add AI Learning admin section for viewing/editing error patterns - Add server status tiles to admin dashboard (CPU load, memory, disk) - Auto-refresh dashboard tiles using HTMX - Fix error template text (GitCaddy Server) - Dark mode compatibility for all new UI elements 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -251,7 +251,7 @@ jobs:
|
||||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always --dirty 2>/dev/null | sed "s/-gitcaddy//" || echo "dev")
|
||||
LDFLAGS="-X code.gitea.io/gitea/modules/setting.AppVer=${VERSION}"
|
||||
LDFLAGS="-X main.Version=${VERSION}"
|
||||
|
||||
EXT=""
|
||||
if [ "$GOOS" = "windows" ]; then
|
||||
|
||||
@@ -112,7 +112,7 @@ func loadActionsFrom(rootCfg ConfigProvider) error {
|
||||
Actions.RunnerHealthCheck.MinDiskPercent = 5.0 // Need at least 5% free
|
||||
}
|
||||
if Actions.RunnerHealthCheck.MaxDiskUsagePercent <= 0 {
|
||||
Actions.RunnerHealthCheck.MaxDiskUsagePercent = 95.0 // Alert at 95% used
|
||||
Actions.RunnerHealthCheck.MaxDiskUsagePercent = 85.0 // Alert at 85% used
|
||||
}
|
||||
if Actions.RunnerHealthCheck.MaxLatencyMs <= 0 {
|
||||
Actions.RunnerHealthCheck.MaxLatencyMs = 500.0 // 500ms max latency
|
||||
|
||||
@@ -207,7 +207,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 GitCaddy bug, please search for issues on <a href=\"%s\" target=\"_blank\">GitCaddy GitCaddy</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 Server</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": "Steeped in your workflow",
|
||||
@@ -3961,5 +3961,37 @@
|
||||
"actions.runners.queue_depth": "Job Queue",
|
||||
"actions.runners.current_task": "Current Task",
|
||||
"actions.runners.cleanup_requested": "Cleanup request sent to runner",
|
||||
"actions.runners.cleanup_request_failed": "Failed to send cleanup request"
|
||||
"actions.runners.cleanup_request_failed": "Failed to send cleanup request",
|
||||
"admin.ai_learning": "AI Learning",
|
||||
"admin.ai_learning.edit": "Edit Pattern",
|
||||
"admin.ai_learning.total_patterns": "Total Patterns",
|
||||
"admin.ai_learning.total_occurrences": "Total Occurrences",
|
||||
"admin.ai_learning.total_successes": "Solutions Used",
|
||||
"admin.ai_learning.success_rate": "Success Rate",
|
||||
"admin.ai_learning.filter": "Filter",
|
||||
"admin.ai_learning.clear_filters": "Clear",
|
||||
"admin.ai_learning.all": "All",
|
||||
"admin.ai_learning.filter_pattern": "Pattern Code",
|
||||
"admin.ai_learning.filter_runner_type": "Runner Type",
|
||||
"admin.ai_learning.filter_project_type": "Project Type",
|
||||
"admin.ai_learning.pattern": "Pattern",
|
||||
"admin.ai_learning.pattern_help": "Error code or pattern identifier (e.g., NETSDK1147)",
|
||||
"admin.ai_learning.pattern_regex": "Regex Pattern",
|
||||
"admin.ai_learning.pattern_regex_help": "Optional regex for matching error messages",
|
||||
"admin.ai_learning.runner_type": "Runner Type",
|
||||
"admin.ai_learning.project_type": "Project Type",
|
||||
"admin.ai_learning.framework": "Framework",
|
||||
"admin.ai_learning.error_message": "Error Message",
|
||||
"admin.ai_learning.diagnosis": "Diagnosis",
|
||||
"admin.ai_learning.solution": "Solution",
|
||||
"admin.ai_learning.solution_diff": "Solution Diff",
|
||||
"admin.ai_learning.occurrences": "Occurrences",
|
||||
"admin.ai_learning.successes": "Successes",
|
||||
"admin.ai_learning.actions": "Actions",
|
||||
"admin.ai_learning.created": "Created",
|
||||
"admin.ai_learning.updated": "Pattern updated successfully",
|
||||
"admin.ai_learning.no_patterns": "No error patterns found",
|
||||
"admin.ai_learning.delete_confirm": "Are you sure you want to delete this pattern?",
|
||||
"admin.ai_learning.delete_selected": "Delete Selected",
|
||||
"admin.ai_learning.deleted": "Pattern(s) deleted successfully"
|
||||
}
|
||||
|
||||
@@ -1104,7 +1104,7 @@ THE SOFTWARE.
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@citation-js/name@0.4.2 - MIT
|
||||
@citation-js/date@0.5.1 - MIT
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
const (
|
||||
tplDashboard templates.TplName = "admin/dashboard"
|
||||
tplSystemStatus templates.TplName = "admin/system_status"
|
||||
tplServerStats templates.TplName = "admin/server_stats"
|
||||
tplSelfCheck templates.TplName = "admin/self_check"
|
||||
tplCron templates.TplName = "admin/cron"
|
||||
tplQueue templates.TplName = "admin/queue"
|
||||
@@ -140,6 +141,7 @@ func Dashboard(ctx *context.Context) {
|
||||
ctx.Data["RemoteVersion"] = updatechecker.GetRemoteVersion(ctx)
|
||||
updateSystemStatus()
|
||||
ctx.Data["SysStatus"] = sysStatus
|
||||
ctx.Data["ServerStats"] = GetServerStats()
|
||||
ctx.Data["SSH"] = setting.SSH
|
||||
prepareStartupProblemsAlert(ctx)
|
||||
ctx.HTML(http.StatusOK, tplDashboard)
|
||||
@@ -148,9 +150,16 @@ func Dashboard(ctx *context.Context) {
|
||||
func SystemStatus(ctx *context.Context) {
|
||||
updateSystemStatus()
|
||||
ctx.Data["SysStatus"] = sysStatus
|
||||
ctx.Data["ServerStats"] = GetServerStats()
|
||||
ctx.HTML(http.StatusOK, tplSystemStatus)
|
||||
}
|
||||
|
||||
// ServerStats returns the server stats partial for HTMX refresh
|
||||
func DashboardServerStats(ctx *context.Context) {
|
||||
ctx.Data["ServerStats"] = GetServerStats()
|
||||
ctx.HTML(http.StatusOK, tplServerStats)
|
||||
}
|
||||
|
||||
// DashboardPost run an admin operation
|
||||
func DashboardPost(ctx *context.Context) {
|
||||
form := web.GetForm(ctx).(*forms.AdminDashboardForm)
|
||||
@@ -158,6 +167,7 @@ func DashboardPost(ctx *context.Context) {
|
||||
ctx.Data["PageIsAdminDashboard"] = true
|
||||
updateSystemStatus()
|
||||
ctx.Data["SysStatus"] = sysStatus
|
||||
ctx.Data["ServerStats"] = GetServerStats()
|
||||
|
||||
// Run operation.
|
||||
if form.Op != "" {
|
||||
|
||||
217
routers/web/admin/ai_learning.go
Normal file
217
routers/web/admin/ai_learning.go
Normal file
@@ -0,0 +1,217 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
ai_model "code.gitea.io/gitea/models/ai"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
)
|
||||
|
||||
const (
|
||||
tplAILearning templates.TplName = "admin/ai_learning"
|
||||
tplAILearningEdit templates.TplName = "admin/ai_learning_edit"
|
||||
)
|
||||
|
||||
// AILearningStats holds statistics for the AI Learning dashboard
|
||||
type AILearningStats struct {
|
||||
TotalPatterns int64
|
||||
TotalOccurrences int64
|
||||
TotalSuccesses int64
|
||||
SuccessRate float64
|
||||
TopRunnerTypes []string
|
||||
TopProjectTypes []string
|
||||
}
|
||||
|
||||
// AILearning shows the AI Learning patterns admin page
|
||||
func AILearning(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.ai_learning")
|
||||
ctx.Data["PageIsAdminAILearning"] = true
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
pageSize := setting.UI.Admin.NoticePagingNum
|
||||
if pageSize == 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
// Get filter params
|
||||
patternFilter := ctx.FormString("pattern")
|
||||
runnerType := ctx.FormString("runner_type")
|
||||
projectType := ctx.FormString("project_type")
|
||||
|
||||
// Get patterns
|
||||
patterns, err := ai_model.GetErrorPatterns(ctx, patternFilter, runnerType, projectType)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetErrorPatterns", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate stats
|
||||
stats := &AILearningStats{}
|
||||
var totalOccurrences, totalSuccesses int64
|
||||
runnerTypes := make(map[string]bool)
|
||||
projectTypes := make(map[string]bool)
|
||||
|
||||
for _, p := range patterns {
|
||||
totalOccurrences += int64(p.OccurrenceCount)
|
||||
totalSuccesses += int64(p.SuccessCount)
|
||||
if p.RunnerType != "" {
|
||||
runnerTypes[p.RunnerType] = true
|
||||
}
|
||||
if p.ProjectType != "" {
|
||||
projectTypes[p.ProjectType] = true
|
||||
}
|
||||
}
|
||||
|
||||
stats.TotalPatterns = int64(len(patterns))
|
||||
stats.TotalOccurrences = totalOccurrences
|
||||
stats.TotalSuccesses = totalSuccesses
|
||||
if totalOccurrences > 0 {
|
||||
stats.SuccessRate = float64(totalSuccesses) / float64(totalOccurrences) * 100
|
||||
}
|
||||
|
||||
for rt := range runnerTypes {
|
||||
stats.TopRunnerTypes = append(stats.TopRunnerTypes, rt)
|
||||
}
|
||||
for pt := range projectTypes {
|
||||
stats.TopProjectTypes = append(stats.TopProjectTypes, pt)
|
||||
}
|
||||
|
||||
// Paginate patterns
|
||||
total := len(patterns)
|
||||
start := (page - 1) * pageSize
|
||||
end := start + pageSize
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
if start < total {
|
||||
patterns = patterns[start:end]
|
||||
} else {
|
||||
patterns = []*ai_model.ErrorPattern{}
|
||||
}
|
||||
|
||||
ctx.Data["Stats"] = stats
|
||||
ctx.Data["Patterns"] = patterns
|
||||
ctx.Data["Total"] = total
|
||||
ctx.Data["PatternFilter"] = patternFilter
|
||||
ctx.Data["RunnerTypeFilter"] = runnerType
|
||||
ctx.Data["ProjectTypeFilter"] = projectType
|
||||
ctx.Data["Page"] = context.NewPagination(total, pageSize, page, 5)
|
||||
|
||||
ctx.HTML(http.StatusOK, tplAILearning)
|
||||
}
|
||||
|
||||
// AILearningEdit shows the edit form for an error pattern
|
||||
func AILearningEdit(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.ai_learning.edit")
|
||||
ctx.Data["PageIsAdminAILearning"] = true
|
||||
|
||||
id := ctx.PathParamInt64("id")
|
||||
if id == 0 {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
pattern, err := ai_model.GetErrorPatternByID(ctx, id)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetErrorPatternByID", err)
|
||||
return
|
||||
}
|
||||
if pattern == nil {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["Pattern"] = pattern
|
||||
ctx.HTML(http.StatusOK, tplAILearningEdit)
|
||||
}
|
||||
|
||||
// AILearningEditPost handles the edit form submission
|
||||
func AILearningEditPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.ai_learning.edit")
|
||||
ctx.Data["PageIsAdminAILearning"] = true
|
||||
|
||||
id := ctx.PathParamInt64("id")
|
||||
if id == 0 {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
pattern, err := ai_model.GetErrorPatternByID(ctx, id)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetErrorPatternByID", err)
|
||||
return
|
||||
}
|
||||
if pattern == nil {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*forms.AILearningForm)
|
||||
pattern.Pattern = form.Pattern
|
||||
pattern.PatternRegex = form.PatternRegex
|
||||
pattern.RunnerType = form.RunnerType
|
||||
pattern.ProjectType = form.ProjectType
|
||||
pattern.Framework = form.Framework
|
||||
pattern.ErrorMessage = form.ErrorMessage
|
||||
pattern.Diagnosis = form.Diagnosis
|
||||
pattern.Solution = form.Solution
|
||||
pattern.SolutionDiff = form.SolutionDiff
|
||||
|
||||
if _, err := db.GetEngine(ctx).ID(id).AllCols().Update(pattern); err != nil {
|
||||
ctx.ServerError("UpdateErrorPattern", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.ai_learning.updated"))
|
||||
ctx.Redirect(setting.AppSubURL + "/-/admin/ai-learning")
|
||||
}
|
||||
|
||||
// AILearningDelete handles deletion of error patterns
|
||||
func AILearningDelete(ctx *context.Context) {
|
||||
id := ctx.PathParamInt64("id")
|
||||
if id == 0 {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).ID(id).Delete(&ai_model.ErrorPattern{}); err != nil {
|
||||
ctx.Flash.Error("Delete error pattern: " + err.Error())
|
||||
ctx.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("admin.ai_learning.deleted"))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/ai-learning")
|
||||
}
|
||||
|
||||
// AILearningDeleteMultiple handles bulk deletion of error patterns
|
||||
func AILearningDeleteMultiple(ctx *context.Context) {
|
||||
strs := ctx.FormStrings("ids[]")
|
||||
ids := make([]int64, 0, len(strs))
|
||||
for i := range strs {
|
||||
id, _ := strconv.ParseInt(strs[i], 10, 64)
|
||||
if id > 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.DeleteByIDs[ai_model.ErrorPattern](ctx, ids...); err != nil {
|
||||
ctx.Flash.Error("DeleteErrorPatterns: " + err.Error())
|
||||
ctx.Status(http.StatusInternalServerError)
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("admin.ai_learning.deleted"))
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
}
|
||||
141
routers/web/admin/server_stats.go
Normal file
141
routers/web/admin/server_stats.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ServerStats holds server system statistics
|
||||
type ServerStats struct {
|
||||
// CPU
|
||||
NumCPU int
|
||||
NumGoroutine int
|
||||
CPULoad float64 // 1-minute load average
|
||||
|
||||
// Memory (from runtime)
|
||||
MemTotal int64
|
||||
MemUsed int64
|
||||
MemPercent float64
|
||||
|
||||
// Disk
|
||||
DiskTotal int64
|
||||
DiskUsed int64
|
||||
DiskFree int64
|
||||
DiskPercent float64
|
||||
|
||||
// Status
|
||||
Uptime time.Duration
|
||||
StartTime time.Time
|
||||
Status string
|
||||
}
|
||||
|
||||
var serverStartTime = time.Now()
|
||||
|
||||
// GetServerStats collects current server statistics
|
||||
func GetServerStats() *ServerStats {
|
||||
stats := &ServerStats{
|
||||
NumCPU: runtime.NumCPU(),
|
||||
NumGoroutine: runtime.NumGoroutine(),
|
||||
StartTime: serverStartTime,
|
||||
Uptime: time.Since(serverStartTime),
|
||||
Status: "healthy",
|
||||
}
|
||||
|
||||
// Get CPU load average
|
||||
stats.CPULoad = getCPULoad()
|
||||
|
||||
// Memory stats from runtime
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
stats.MemUsed = int64(m.Alloc)
|
||||
stats.MemTotal = int64(m.Sys)
|
||||
if stats.MemTotal > 0 {
|
||||
stats.MemPercent = float64(stats.MemUsed) / float64(stats.MemTotal) * 100
|
||||
}
|
||||
|
||||
// Disk stats
|
||||
diskPath := "/"
|
||||
if runtime.GOOS == "windows" {
|
||||
diskPath = "C:\\"
|
||||
}
|
||||
|
||||
if stat, err := getDiskUsage(diskPath); err == nil {
|
||||
stats.DiskTotal = stat.Total
|
||||
stats.DiskUsed = stat.Used
|
||||
stats.DiskFree = stat.Free
|
||||
if stats.DiskTotal > 0 {
|
||||
stats.DiskPercent = float64(stats.DiskUsed) / float64(stats.DiskTotal) * 100
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// getCPULoad returns the 1-minute load average on Linux
|
||||
func getCPULoad() float64 {
|
||||
if runtime.GOOS != "linux" {
|
||||
return 0
|
||||
}
|
||||
|
||||
data, err := os.ReadFile("/proc/loadavg")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) < 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
load, err := strconv.ParseFloat(fields[0], 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return load
|
||||
}
|
||||
|
||||
type diskUsage struct {
|
||||
Total int64
|
||||
Free int64
|
||||
Used int64
|
||||
}
|
||||
|
||||
func getDiskUsage(path string) (*diskUsage, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return getDiskUsageWindows(path)
|
||||
}
|
||||
return getDiskUsageUnix(path)
|
||||
}
|
||||
|
||||
func getDiskUsageUnix(path string) (*diskUsage, error) {
|
||||
var stat syscall.Statfs_t
|
||||
if err := syscall.Statfs(path, &stat); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
total := int64(stat.Blocks) * stat.Bsize
|
||||
free := int64(stat.Bavail) * stat.Bsize
|
||||
used := total - free
|
||||
|
||||
return &diskUsage{
|
||||
Total: total,
|
||||
Free: free,
|
||||
Used: used,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getDiskUsageWindows(_ string) (*diskUsage, error) {
|
||||
return &diskUsage{
|
||||
Total: 0,
|
||||
Free: 0,
|
||||
Used: 0,
|
||||
}, nil
|
||||
}
|
||||
@@ -769,6 +769,7 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Group("/-/admin", func() {
|
||||
m.Get("", admin.Dashboard)
|
||||
m.Get("/system_status", admin.SystemStatus)
|
||||
m.Get("/server_stats", admin.DashboardServerStats)
|
||||
m.Post("", web.Bind(forms.AdminDashboardForm{}), admin.DashboardPost)
|
||||
|
||||
m.Get("/self_check", admin.SelfCheck)
|
||||
@@ -859,6 +860,14 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Post("/empty", admin.EmptyNotices)
|
||||
})
|
||||
|
||||
m.Group("/ai-learning", func() {
|
||||
m.Get("", admin.AILearning)
|
||||
m.Get("/{id}", admin.AILearningEdit)
|
||||
m.Post("/{id}", web.Bind(forms.AILearningForm{}), admin.AILearningEditPost)
|
||||
m.Post("/{id}/delete", admin.AILearningDelete)
|
||||
m.Post("/delete", admin.AILearningDeleteMultiple)
|
||||
})
|
||||
|
||||
m.Group("/applications", func() {
|
||||
m.Get("", admin.Applications)
|
||||
m.Post("/oauth2", web.Bind(forms.EditOAuth2ApplicationForm{}), admin.ApplicationsPost)
|
||||
|
||||
@@ -71,3 +71,22 @@ func (f *AdminDashboardForm) Validate(req *http.Request, errs binding.Errors) bi
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
// AILearningForm form for editing AI learning patterns
|
||||
type AILearningForm struct {
|
||||
Pattern string `binding:"Required"`
|
||||
PatternRegex string
|
||||
RunnerType string
|
||||
ProjectType string
|
||||
Framework string
|
||||
ErrorMessage string
|
||||
Diagnosis string
|
||||
Solution string
|
||||
SolutionDiff string
|
||||
}
|
||||
|
||||
// Validate validates form fields
|
||||
func (f *AILearningForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||
ctx := context.GetValidateContext(req)
|
||||
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||
}
|
||||
|
||||
185
templates/admin/ai_learning.tmpl
Normal file
185
templates/admin/ai_learning.tmpl
Normal file
@@ -0,0 +1,185 @@
|
||||
{{template "admin/layout_head" .}}
|
||||
<div class="admin ai-learning">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.ai_learning"}}
|
||||
</h4>
|
||||
|
||||
<!-- Stats Tiles -->
|
||||
<div class="ui attached segment">
|
||||
<div style="display: flex; justify-content: space-around; text-align: center;">
|
||||
<div style="padding: 15px 25px; background: var(--color-box-body); border: 1px solid var(--color-secondary); border-radius: 8px; min-width: 150px;">
|
||||
<div style="font-size: 2em; font-weight: bold; color: #2185d0;">{{.Stats.TotalPatterns}}</div>
|
||||
<div style="color: var(--color-text-light); margin-top: 5px;">{{ctx.Locale.Tr "admin.ai_learning.total_patterns"}}</div>
|
||||
</div>
|
||||
<div style="padding: 15px 25px; background: var(--color-box-body); border: 1px solid var(--color-secondary); border-radius: 8px; min-width: 150px;">
|
||||
<div style="font-size: 2em; font-weight: bold; color: #6435c9;">{{.Stats.TotalOccurrences}}</div>
|
||||
<div style="color: var(--color-text-light); margin-top: 5px;">{{ctx.Locale.Tr "admin.ai_learning.total_occurrences"}}</div>
|
||||
</div>
|
||||
<div style="padding: 15px 25px; background: var(--color-box-body); border: 1px solid var(--color-secondary); border-radius: 8px; min-width: 150px;">
|
||||
<div style="font-size: 2em; font-weight: bold; color: #21ba45;">{{.Stats.TotalSuccesses}}</div>
|
||||
<div style="color: var(--color-text-light); margin-top: 5px;">{{ctx.Locale.Tr "admin.ai_learning.total_successes"}}</div>
|
||||
</div>
|
||||
<div style="padding: 15px 25px; background: var(--color-box-body); border: 1px solid var(--color-secondary); border-radius: 8px; min-width: 150px;">
|
||||
<div style="font-size: 2em; font-weight: bold; color: #f2711c;">{{printf "%.1f" .Stats.SuccessRate}}%</div>
|
||||
<div style="color: var(--color-text-light); margin-top: 5px;">{{ctx.Locale.Tr "admin.ai_learning.success_rate"}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" method="get">
|
||||
<div class="fields" style="align-items: flex-end; margin-bottom: 0;">
|
||||
<div class="four wide field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.filter_pattern"}}</label>
|
||||
<input type="text" name="pattern" value="{{.PatternFilter}}" placeholder="e.g., NETSDK1147">
|
||||
</div>
|
||||
<div class="three wide field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.filter_runner_type"}}</label>
|
||||
<select class="ui dropdown" name="runner_type">
|
||||
<option value="">{{ctx.Locale.Tr "admin.ai_learning.all"}}</option>
|
||||
{{range .Stats.TopRunnerTypes}}
|
||||
<option value="{{.}}" {{if eq . $.RunnerTypeFilter}}selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="four wide field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.filter_project_type"}}</label>
|
||||
<select class="ui dropdown" name="project_type">
|
||||
<option value="">{{ctx.Locale.Tr "admin.ai_learning.all"}}</option>
|
||||
{{range .Stats.TopProjectTypes}}
|
||||
<option value="{{.}}" {{if eq . $.ProjectTypeFilter}}selected{{end}}>{{.}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "admin.ai_learning.filter"}}</button>
|
||||
</div>
|
||||
<div class="field">
|
||||
<a class="ui button" href="{{AppSubUrl}}/-/admin/ai-learning">{{ctx.Locale.Tr "admin.ai_learning.clear_filters"}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Pattern List -->
|
||||
<div class="ui attached segment">
|
||||
<form method="post" action="{{AppSubUrl}}/-/admin/ai-learning/delete">
|
||||
{{.CsrfTokenHtml}}
|
||||
<table class="ui celled striped table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="collapsing">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" class="select-all">
|
||||
<label></label>
|
||||
</div>
|
||||
</th>
|
||||
<th>{{ctx.Locale.Tr "admin.ai_learning.pattern"}}</th>
|
||||
<th>{{ctx.Locale.Tr "admin.ai_learning.runner_type"}}</th>
|
||||
<th>{{ctx.Locale.Tr "admin.ai_learning.project_type"}}</th>
|
||||
<th class="collapsing">{{ctx.Locale.Tr "admin.ai_learning.occurrences"}}</th>
|
||||
<th class="collapsing">{{ctx.Locale.Tr "admin.ai_learning.successes"}}</th>
|
||||
<th class="collapsing">{{ctx.Locale.Tr "admin.ai_learning.actions"}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Patterns}}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="ids[]" value="{{.ID}}">
|
||||
<label></label>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{.Pattern}}</strong>
|
||||
{{if .Diagnosis}}
|
||||
<br><small class="text truncate" style="color: var(--color-text-light); max-width: 300px; display: inline-block;">{{.Diagnosis}}</small>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .RunnerType}}
|
||||
<span class="ui small label" style="background-color: {{if eq .RunnerType "linux"}}#21ba45{{else if eq .RunnerType "windows"}}#2185d0{{else if eq .RunnerType "macos"}}#a333c8{{else}}#767676{{end}}; color: white;">
|
||||
{{.RunnerType}}
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="ui small grey label">any</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .ProjectType}}
|
||||
<span class="ui small teal label">{{.ProjectType}}</span>
|
||||
{{if .Framework}}
|
||||
<span class="ui small label">{{.Framework}}</span>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<span class="ui small grey label">any</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="center aligned">
|
||||
<span class="ui small label" style="background-color: #6435c9; color: white;">{{.OccurrenceCount}}</span>
|
||||
</td>
|
||||
<td class="center aligned">
|
||||
<span class="ui small label" style="background-color: #21ba45; color: white;">{{.SuccessCount}}</span>
|
||||
</td>
|
||||
<td class="collapsing">
|
||||
<a class="ui mini primary button" href="{{AppSubUrl}}/-/admin/ai-learning/{{.ID}}">
|
||||
{{svg "octicon-pencil" 14}} {{ctx.Locale.Tr "edit"}}
|
||||
</a>
|
||||
<button class="ui mini red button link-action" type="button"
|
||||
data-url="{{AppSubUrl}}/-/admin/ai-learning/{{.ID}}/delete"
|
||||
data-modal-confirm="{{ctx.Locale.Tr "admin.ai_learning.delete_confirm"}}">
|
||||
{{svg "octicon-trash" 14}}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td colspan="7" class="center aligned">
|
||||
<i>{{ctx.Locale.Tr "admin.ai_learning.no_patterns"}}</i>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{{if .Patterns}}
|
||||
<div class="ui divider"></div>
|
||||
<button class="ui red button delete-selected" type="submit" disabled>
|
||||
{{svg "octicon-trash" 14}} {{ctx.Locale.Tr "admin.ai_learning.delete_selected"}}
|
||||
</button>
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{template "base/paginate" .}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectAll = document.querySelector('.select-all');
|
||||
const checkboxes = document.querySelectorAll('input[name="ids[]"]');
|
||||
const deleteBtn = document.querySelector('.delete-selected');
|
||||
|
||||
if (selectAll) {
|
||||
selectAll.addEventListener('change', function() {
|
||||
checkboxes.forEach(cb => cb.checked = this.checked);
|
||||
updateDeleteBtn();
|
||||
});
|
||||
}
|
||||
|
||||
checkboxes.forEach(cb => {
|
||||
cb.addEventListener('change', updateDeleteBtn);
|
||||
});
|
||||
|
||||
function updateDeleteBtn() {
|
||||
if (deleteBtn) {
|
||||
const checked = document.querySelectorAll('input[name="ids[]"]:checked');
|
||||
deleteBtn.disabled = checked.length === 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{{template "admin/layout_footer" .}}
|
||||
103
templates/admin/ai_learning_edit.tmpl
Normal file
103
templates/admin/ai_learning_edit.tmpl
Normal file
@@ -0,0 +1,103 @@
|
||||
{{template "admin/layout_head" .}}
|
||||
<div class="admin ai-learning-edit">
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.ai_learning.edit"}} - {{.Pattern.Pattern}}
|
||||
</h4>
|
||||
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" method="post" action="{{AppSubUrl}}/-/admin/ai-learning/{{.Pattern.ID}}">
|
||||
{{.CsrfTokenHtml}}
|
||||
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.pattern"}}</label>
|
||||
<input type="text" name="pattern" value="{{.Pattern.Pattern}}" required>
|
||||
<small>{{ctx.Locale.Tr "admin.ai_learning.pattern_help"}}</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.pattern_regex"}}</label>
|
||||
<input type="text" name="pattern_regex" value="{{.Pattern.PatternRegex}}">
|
||||
<small>{{ctx.Locale.Tr "admin.ai_learning.pattern_regex_help"}}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="three fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.runner_type"}}</label>
|
||||
<select class="ui dropdown" name="runner_type">
|
||||
<option value="" {{if eq .Pattern.RunnerType ""}}selected{{end}}>Any</option>
|
||||
<option value="linux" {{if eq .Pattern.RunnerType "linux"}}selected{{end}}>Linux</option>
|
||||
<option value="windows" {{if eq .Pattern.RunnerType "windows"}}selected{{end}}>Windows</option>
|
||||
<option value="macos" {{if eq .Pattern.RunnerType "macos"}}selected{{end}}>macOS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.project_type"}}</label>
|
||||
<input type="text" name="project_type" value="{{.Pattern.ProjectType}}" placeholder="e.g., dotnet-maui, go, node">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.framework"}}</label>
|
||||
<input type="text" name="framework" value="{{.Pattern.Framework}}" placeholder="e.g., net10.0-android">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.error_message"}}</label>
|
||||
<textarea name="error_message" rows="3" placeholder="Full error message example">{{.Pattern.ErrorMessage}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.diagnosis"}}</label>
|
||||
<textarea name="diagnosis" rows="3" placeholder="What this error means">{{.Pattern.Diagnosis}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.solution"}}</label>
|
||||
<textarea name="solution" rows="4" placeholder="How to fix this error">{{.Pattern.Solution}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.solution_diff"}}</label>
|
||||
<textarea name="solution_diff" rows="6" placeholder="Code/config changes that fix it">{{.Pattern.SolutionDiff}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="ui segment secondary">
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.occurrences"}}</label>
|
||||
<div class="ui small label" style="background-color: #6435c9; color: white;">{{.Pattern.OccurrenceCount}}</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.successes"}}</label>
|
||||
<div class="ui small label" style="background-color: #21ba45; color: white;">{{.Pattern.SuccessCount}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="two fields">
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.created"}}</label>
|
||||
<span>{{DateUtils.TimeSince .Pattern.CreatedUnix}}</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "admin.ai_learning.updated"}}</label>
|
||||
<span>{{DateUtils.TimeSince .Pattern.UpdatedUnix}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<button class="ui primary button" type="submit">
|
||||
{{svg "octicon-check" 14}} {{ctx.Locale.Tr "save"}}
|
||||
</button>
|
||||
<a class="ui button" href="{{AppSubUrl}}/-/admin/ai-learning">
|
||||
{{ctx.Locale.Tr "cancel"}}
|
||||
</a>
|
||||
<button class="ui red button right floated link-action" type="button"
|
||||
data-url="{{AppSubUrl}}/-/admin/ai-learning/{{.Pattern.ID}}/delete"
|
||||
data-modal-confirm="{{ctx.Locale.Tr "admin.ai_learning.delete_confirm"}}">
|
||||
{{svg "octicon-trash" 14}} {{ctx.Locale.Tr "delete"}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{template "admin/layout_footer" .}}
|
||||
@@ -5,6 +5,15 @@
|
||||
<p>{{ctx.Locale.Tr "admin.dashboard.new_version_hint" .RemoteVersion AppVer "https://blog.gitea.com"}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- Server Status Tiles (auto-refresh) -->
|
||||
<div class="ui segment">
|
||||
<div class="no-loading-indicator tw-hidden"></div>
|
||||
<div hx-get="{{$.Link}}/server_stats" hx-swap="morph:innerHTML" hx-trigger="every 5s" hx-indicator=".no-loading-indicator">
|
||||
{{template "admin/server_stats" .}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.dashboard.maintenance_operations"}}
|
||||
</h4>
|
||||
@@ -74,7 +83,6 @@
|
||||
<h4 class="ui top attached header">
|
||||
{{ctx.Locale.Tr "admin.dashboard.system_status"}}
|
||||
</h4>
|
||||
{{/* TODO: make these stats work in multi-server deployments, likely needs per-server stats in DB */}}
|
||||
<div class="ui attached table segment">
|
||||
<div class="no-loading-indicator tw-hidden"></div>
|
||||
<div hx-get="{{$.Link}}/system_status" hx-swap="morph:innerHTML" hx-trigger="every 5s" hx-indicator=".no-loading-indicator">
|
||||
|
||||
@@ -112,5 +112,8 @@
|
||||
</a>
|
||||
</div>
|
||||
</details>
|
||||
<a class="{{if .PageIsAdminAILearning}}active {{end}}item" href="{{AppSubUrl}}/-/admin/ai-learning">
|
||||
{{ctx.Locale.Tr "admin.ai_learning"}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
24
templates/admin/server_stats.tmpl
Normal file
24
templates/admin/server_stats.tmpl
Normal file
@@ -0,0 +1,24 @@
|
||||
{{if .ServerStats}}
|
||||
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: stretch; gap: 20px;">
|
||||
<div style="flex: 1; padding: 20px; background: var(--color-box-body); border: 1px solid var(--color-secondary); border-radius: 8px; text-align: center;">
|
||||
<div style="font-size: 2em; font-weight: bold; margin-top: 13px; color: #21ba45;">{{svg "octicon-check-circle" 32}}</div>
|
||||
<div style="font-size: 1.2em; font-weight: bold; margin-top: 8px;">{{.ServerStats.Status}}</div>
|
||||
<div style="color: var(--color-text-light); margin-top: 5px; font-size: 0.9em;">Server Status</div>
|
||||
</div>
|
||||
<div style="flex: 1; padding: 20px; background: var(--color-box-body); border: 1px solid var(--color-secondary); border-radius: 8px; text-align: center;">
|
||||
<div style="font-size: 2em; font-weight: bold; margin-top: 13px; color: #2185d0;">{{printf "%.2f" .ServerStats.CPULoad}}</div>
|
||||
<div style="font-size: 1.2em; font-weight: bold; color: var(--color-text-light); margin-top: 10px;">{{.ServerStats.NumCPU}} cores, {{.ServerStats.NumGoroutine}} goroutines</div>
|
||||
<div style="color: var(--color-text-light); margin-top: 8px; font-size: 0.9em;">CPU Load (1m avg)</div>
|
||||
</div>
|
||||
<div style="flex: 1; padding: 20px; background: var(--color-box-body); border: 1px solid var(--color-secondary); border-radius: 8px; text-align: center;">
|
||||
<div style="font-size: 2em; font-weight: bold; margin-top: 13px; color: #6435c9;">{{printf "%.1f" .ServerStats.MemPercent}}%</div>
|
||||
<div style="font-size: 1.2em; font-weight: bold; color: var(--color-text-light); margin-top: 10px;">{{FileSize .ServerStats.MemUsed}} / {{FileSize .ServerStats.MemTotal}}</div>
|
||||
<div style="color: var(--color-text-light); margin-top: 8px; font-size: 0.9em;">Memory Usage</div>
|
||||
</div>
|
||||
<div style="flex: 1; padding: 20px; background: var(--color-box-body); border: 1px solid var(--color-secondary); border-radius: 8px; text-align: center;">
|
||||
<div style="font-size: 2em; font-weight: bold; margin-top: 13px; color: #f2711c;">{{printf "%.1f" .ServerStats.DiskPercent}}%</div>
|
||||
<div style="font-size: 1.2em; font-weight: bold; color: var(--color-text-light); margin-top: 10px;">{{FileSize .ServerStats.DiskFree}} free</div>
|
||||
<div style="color: var(--color-text-light); margin-top: 8px; font-size: 0.9em;">Disk Usage</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user