Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5daac3366 | |||
| 916211004d | |||
| 02fdc1a194 | |||
| 1b0bba09b9 | |||
| 0c0d1c1493 | |||
| 9461599b57 | |||
| 414560f470 | |||
| b43345986a | |||
| 7fbbd26b20 | |||
| b26bf4bfe8 | |||
| 242ebf2dc1 | |||
| 46f7570d25 | |||
| 48aab974fe | |||
| 965ef8966f | |||
| 17028589c8 | |||
| 80096bfbf9 | |||
| 22844f6437 | |||
| 737e323fcb | |||
| 00024298d0 | |||
| b27dd0cda8 | |||
| 43e490d933 | |||
| d02f25c0ba | |||
| 12341079e1 | |||
| c5e35e3466 | |||
| c0fcf16794 | |||
| c5786aab2b | |||
| fe5b504b97 | |||
| a1f477a381 | |||
| 4e32224330 | |||
| 66ce2b1d70 | |||
| cdf47b5ecd | |||
| d45354b538 | |||
| f3eba7dd34 | |||
| 818c2db411 | |||
| fa016ab865 | |||
| dea935bec7 | |||
| e8b6303971 | |||
| c24d329a6a | |||
| 433214fb91 | |||
| d32bcc4d8c | |||
| 222c21bf98 | |||
| 5e165b97be | |||
| c7b5a2af19 | |||
| fca0461ca4 | |||
| 044c65e425 | |||
| 3801e0ba0d | |||
| 741ddd5805 | |||
| 5c6b91f6c9 | |||
| 573aa49a22 | |||
| 679810687f | |||
| fc86952bf4 | |||
| 79a0c2683e | |||
| 6007a19bed | |||
| 123eb78b54 | |||
| 56fd71ad6b | |||
| 82eddb0b09 | |||
| a2644bb47f | |||
| cb2791709e | |||
| 85ab93145e | |||
| fb89a8b55c | |||
| 81c7b07ca3 | |||
| 5788123e00 | |||
| a2edcdabe7 | |||
| 3a8bdd936c | |||
| 64b4a9ceed | |||
| 727ae54f91 | |||
| 238f0974d8 | |||
| b7d8fcc719 | |||
| bf59c1cd5f | |||
| 6dff66b5ba | |||
| 734dd895bb | |||
| 6ceb4f0ad4 | |||
| eca22df63a | |||
| 5f2420d353 | |||
| b5db61c34c | |||
| c59a0f746e | |||
| 438a41cd78 | |||
| 8cf6c08841 | |||
| 112130747c | |||
| 23b6860378 | |||
| aeaea13ab7 | |||
| bd4d53d0f8 | |||
| 34a2c8faf0 | |||
| 79353f4756 | |||
| 5bda6de937 | |||
| dd84db7608 | |||
| 14232eec68 | |||
| 56cf9d1833 | |||
| de7d6a719e | |||
| e932582f54 | |||
| 174d18db22 | |||
| f42c6c39f9 | |||
| 813e3bcbb4 | |||
| 14338d8fd4 | |||
| c9f6c4e7d2 | |||
| eb1a5d497e | |||
| 8ad6664b92 | |||
| 26793bf898 | |||
| 7102167351 | |||
| dcceabc713 | |||
| 9bdd35284f | |||
| 33858109d5 | |||
| b2adcdf969 | |||
| fb2d53ba7a | |||
| ae9cb010ab | |||
| d42b629458 | |||
| 68d4264f9e | |||
| fb8124d8f1 | |||
| a10dbda7ac | |||
| f6e54207dc | |||
| 5c9385f4a2 | |||
| 53d4c529a7 | |||
| e19874f4b3 | |||
| 33898561db | |||
| f40550d79b |
@@ -16,7 +16,7 @@ env:
|
||||
GOPRIVATE: git.marketally.com
|
||||
GONOSUMDB: git.marketally.com
|
||||
GOTOOLCHAIN: local
|
||||
GO_VERSION: "1.25.0"
|
||||
GO_VERSION: "1.25.5"
|
||||
NODE_VERSION: "22"
|
||||
|
||||
jobs:
|
||||
@@ -24,9 +24,13 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: linux-latest
|
||||
env:
|
||||
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
- name: Clone vault dependency
|
||||
env:
|
||||
@@ -40,9 +44,6 @@ jobs:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
|
||||
- name: Tidy modules
|
||||
run: go mod tidy
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -52,7 +53,8 @@ jobs:
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: make deps-frontend deps-backend
|
||||
run: |
|
||||
make deps-frontend deps-backend
|
||||
|
||||
- name: Run Go linter
|
||||
run: make lint-go
|
||||
@@ -65,9 +67,13 @@ jobs:
|
||||
test-unit:
|
||||
name: Unit Tests
|
||||
runs-on: linux-latest
|
||||
env:
|
||||
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-unit
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
- name: Clone vault dependency
|
||||
env:
|
||||
@@ -82,7 +88,8 @@ jobs:
|
||||
cache: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: go mod download
|
||||
run: |
|
||||
go mod download
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
@@ -98,6 +105,8 @@ jobs:
|
||||
test-pgsql:
|
||||
name: Integration Tests (PostgreSQL)
|
||||
runs-on: linux-latest
|
||||
env:
|
||||
GOMODCACHE: /tmp/gomod-${{ github.run_id }}-pgsql
|
||||
services:
|
||||
pgsql:
|
||||
image: postgres:15
|
||||
@@ -115,6 +124,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
- name: Clone vault dependency
|
||||
env:
|
||||
@@ -128,9 +139,6 @@ jobs:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
cache: false
|
||||
|
||||
- name: Tidy modules
|
||||
run: go mod tidy
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
@@ -140,7 +148,8 @@ jobs:
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: make deps-frontend deps-backend
|
||||
run: |
|
||||
make deps-frontend deps-backend
|
||||
|
||||
- name: Build frontend
|
||||
run: make frontend
|
||||
@@ -245,7 +254,7 @@ jobs:
|
||||
if: matrix.goos != 'windows'
|
||||
id: vault
|
||||
run: |
|
||||
VERSION=$(curl -sf "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
|
||||
VERSION=$(curl -sf -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
|
||||
echo "version=$VERSION"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -254,7 +263,7 @@ jobs:
|
||||
id: vault-win
|
||||
shell: pwsh
|
||||
run: |
|
||||
$response = Invoke-RestMethod -Uri "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest"
|
||||
$response = Invoke-RestMethod -Uri "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" -Headers @{ Authorization = "token ${{ secrets.RELEASE_TOKEN }}" }
|
||||
$version = $response.tag_name
|
||||
Write-Host "version=$version"
|
||||
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
@@ -262,6 +271,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure git line endings (Windows)
|
||||
@@ -377,7 +387,8 @@ jobs:
|
||||
|
||||
- name: Install dependencies (Unix)
|
||||
if: matrix.goos != 'windows'
|
||||
run: make deps-frontend deps-backend
|
||||
run: |
|
||||
make deps-frontend deps-backend
|
||||
|
||||
- name: Install dependencies (Windows)
|
||||
if: matrix.goos == 'windows'
|
||||
@@ -421,7 +432,7 @@ jobs:
|
||||
TAGS: bindata sqlite sqlite_unlock_notify
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always 2>/dev/null | sed "s/-gitcaddy//" || echo "dev")
|
||||
VAULT_VERSION=$(curl -sf "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4 || echo "dev")
|
||||
VAULT_VERSION=$(curl -sf -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4 || echo "dev")
|
||||
LDFLAGS="-X main.Version=${VERSION} -X git.marketally.com/gitcaddy/gitcaddy-vault.Version=${VAULT_VERSION}"
|
||||
OUTPUT="gitcaddy-server-${VERSION}-${GOOS}-${GOARCH}"
|
||||
|
||||
@@ -457,7 +468,7 @@ jobs:
|
||||
|
||||
$VERSION = (git describe --tags --always 2>$null) -replace '-gitcaddy',''
|
||||
if (-not $VERSION) { $VERSION = "dev" }
|
||||
$VAULT_VERSION = (Invoke-RestMethod -Uri "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" -ErrorAction SilentlyContinue).tag_name
|
||||
$VAULT_VERSION = (Invoke-RestMethod -Uri "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" -Headers @{ Authorization = "token ${{ secrets.RELEASE_TOKEN }}" } -ErrorAction SilentlyContinue).tag_name
|
||||
if (-not $VAULT_VERSION) { $VAULT_VERSION = "dev" }
|
||||
$LDFLAGS = "-X main.Version=$VERSION -X git.marketally.com/gitcaddy/gitcaddy-vault.Version=$VAULT_VERSION"
|
||||
$OUTPUT = "gitcaddy-server-$VERSION-$env:GOOS-$env:GOARCH.exe"
|
||||
@@ -584,7 +595,7 @@ jobs:
|
||||
- name: Get latest vault version
|
||||
id: vault
|
||||
run: |
|
||||
VERSION=$(curl -sf "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
|
||||
VERSION=$(curl -sf -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4)
|
||||
echo "version=$VERSION"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -598,6 +609,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Configure private repo access
|
||||
@@ -646,7 +658,7 @@ jobs:
|
||||
- name: Build binary
|
||||
run: |
|
||||
VERSION=$(git describe --tags --always 2>/dev/null | sed "s/-gitcaddy//" || echo "dev")
|
||||
VAULT_VERSION=$(curl -sf "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4 || echo "dev")
|
||||
VAULT_VERSION=$(curl -sf -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" "https://direct.git.marketally.com/api/v1/repos/gitcaddy/gitcaddy-vault/releases/latest" | grep -o '"tag_name":"[^"]*"' | cut -d'"' -f4 || echo "dev")
|
||||
LDFLAGS="-X main.Version=${VERSION} -X git.marketally.com/gitcaddy/gitcaddy-vault.Version=${VAULT_VERSION}"
|
||||
OUTPUT="gitcaddy-server-${VERSION}-linux-arm64"
|
||||
TAGS="bindata sqlite sqlite_unlock_notify"
|
||||
|
||||
@@ -21,6 +21,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
@@ -64,6 +66,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
@@ -91,6 +95,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
8
.notes/note-1770858253427-5d5d6gaia.json
Normal file
8
.notes/note-1770858253427-5d5d6gaia.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"id": "note-1770858253427-5d5d6gaia",
|
||||
"title": "Issues/Releases",
|
||||
"content": " Access matrix after changes\n ┌────────────────────────┬────────────┬────────────┬────────────┬─────────────┐\n │ Endpoint │ Public │ Limited │ Private │ Private Org │\n ├────────────────────────┼────────────┼────────────┼────────────┼─────────────┤\n │ POST submit.json │ anonymous │ anonymous │ anonymous │ anonymous │\n ├────────────────────────┼────────────┼────────────┼────────────┼─────────────┤\n │ GET status.json │ anonymous* │ anonymous* │ anonymous* │ anonymous* │\n ├────────────────────────┼────────────┼────────────┼────────────┼─────────────┤\n │ GET external/{id}.json │ anonymous │ anonymous │ anonymous │ anonymous │\n ├────────────────────────┼────────────┼────────────┼────────────┼─────────────┤\n │ GET latest.json │ anonymous │ anonymous │ anonymous │ anonymous │\n └────────────────────────┴────────────┴────────────┴────────────┴─────────────┘\n * Only returns issues where external_source = \"gitcaddy-desktop\"",
|
||||
"createdAt": 1770858253425,
|
||||
"updatedAt": 1770858258193,
|
||||
"tags": []
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 gitcaddy
|
||||
Copyright (c) 2026 GitCaddy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
|
||||
6
Makefile
6
Makefile
@@ -763,6 +763,12 @@ generate-go: $(TAGS_PREREQ)
|
||||
@echo "Running go generate..."
|
||||
@CC= GOOS= GOARCH= CGO_ENABLED=0 $(GO) generate -tags '$(TAGS)' ./...
|
||||
|
||||
.PHONY: generate-plugin-proto
|
||||
generate-plugin-proto:
|
||||
protoc --go_out=. --go_opt=paths=source_relative \
|
||||
--connect-go_out=. --connect-go_opt=paths=source_relative \
|
||||
modules/plugins/pluginv1/plugin.proto
|
||||
|
||||
.PHONY: security-check
|
||||
security-check:
|
||||
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
||||
|
||||
577
PLUGINS.md
Normal file
577
PLUGINS.md
Normal file
@@ -0,0 +1,577 @@
|
||||
# GitCaddy Plugin Development Guide
|
||||
|
||||
This guide explains how to build external plugins for GitCaddy. Plugins are standalone services that communicate with the server over gRPC (HTTP/2) using a well-defined protocol.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Protocol](#protocol)
|
||||
- [Service Definition](#service-definition)
|
||||
- [Lifecycle](#lifecycle)
|
||||
- [Messages](#messages)
|
||||
- [Plugin Manifest](#plugin-manifest)
|
||||
- [Routes](#routes)
|
||||
- [Events](#events)
|
||||
- [Permissions](#permissions)
|
||||
- [Health Monitoring](#health-monitoring)
|
||||
- [Protocol Versioning](#protocol-versioning)
|
||||
- [Configuration](#configuration)
|
||||
- [External Mode](#external-mode)
|
||||
- [Managed Mode](#managed-mode)
|
||||
- [Configuration Reference](#configuration-reference)
|
||||
- [Transport](#transport)
|
||||
- [Example: Go Plugin](#example-go-plugin)
|
||||
- [Example: C# Plugin](#example-c-plugin)
|
||||
- [Example: Python Plugin](#example-python-plugin)
|
||||
- [Debugging](#debugging)
|
||||
|
||||
## Overview
|
||||
|
||||
A GitCaddy plugin is any process that implements the `PluginService` gRPC interface. The server connects to the plugin on startup, calls `Initialize` to get its manifest, and then:
|
||||
|
||||
- **Health checks** the plugin periodically (default: every 30 seconds)
|
||||
- **Dispatches events** the plugin has subscribed to (e.g., `license:updated`)
|
||||
- **Proxies HTTP requests** to routes the plugin has declared
|
||||
- **Shuts down** the plugin gracefully when the server stops
|
||||
|
||||
Plugins can run in two modes:
|
||||
- **External mode** - The plugin runs independently (Docker, systemd, etc.). The server connects to it.
|
||||
- **Managed mode** - The server launches the plugin binary and manages its process lifecycle.
|
||||
|
||||
## Protocol
|
||||
|
||||
The protocol is defined in [`modules/plugins/pluginv1/plugin.proto`](modules/plugins/pluginv1/plugin.proto).
|
||||
|
||||
### Service Definition
|
||||
|
||||
```protobuf
|
||||
service PluginService {
|
||||
rpc Initialize(InitializeRequest) returns (InitializeResponse);
|
||||
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
|
||||
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
rpc GetManifest(GetManifestRequest) returns (PluginManifest);
|
||||
rpc OnEvent(PluginEvent) returns (EventResponse);
|
||||
rpc HandleHTTP(HTTPRequest) returns (HTTPResponse);
|
||||
}
|
||||
```
|
||||
|
||||
All 6 RPCs are unary (request-response). The server is the client; the plugin is the server.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
```
|
||||
Server starts
|
||||
│
|
||||
▼
|
||||
Initialize(server_version, config)
|
||||
│ Plugin returns: success + PluginManifest
|
||||
│
|
||||
▼
|
||||
Plugin is ONLINE
|
||||
│
|
||||
├──► HealthCheck() every 30s
|
||||
│ Plugin returns: healthy, status, details
|
||||
│
|
||||
├──► OnEvent(event_type, payload, repo_id, org_id)
|
||||
│ Dispatched for subscribed events (fire-and-forget with 30s timeout)
|
||||
│
|
||||
├──► HandleHTTP(method, path, headers, body)
|
||||
│ Proxied when an incoming request matches a declared route
|
||||
│
|
||||
▼
|
||||
Server shutting down
|
||||
│
|
||||
▼
|
||||
Shutdown(reason)
|
||||
│ Plugin returns: success
|
||||
│
|
||||
▼
|
||||
Plugin process is sent SIGINT (managed mode only)
|
||||
```
|
||||
|
||||
### Messages
|
||||
|
||||
#### InitializeRequest
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `server_version` | string | The GitCaddy server version (e.g., `"3.0.0"`) |
|
||||
| `config` | map<string, string> | Server-provided configuration key-value pairs |
|
||||
| `protocol_version` | int32 | Plugin protocol version the server supports (current: `1`). `0` means pre-versioning. |
|
||||
|
||||
#### InitializeResponse
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `success` | bool | Whether initialization succeeded |
|
||||
| `error` | string | Error message if `success` is false |
|
||||
| `manifest` | PluginManifest | The plugin's capability manifest |
|
||||
| `protocol_version` | int32 | Plugin protocol version the plugin supports (current: `1`). `0` means pre-versioning, treated as `1`. |
|
||||
|
||||
#### HealthCheckRequest
|
||||
|
||||
Empty message. No fields.
|
||||
|
||||
#### HealthCheckResponse
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `healthy` | bool | Whether the plugin considers itself healthy |
|
||||
| `status` | string | Human-readable status (e.g., `"operational"`, `"degraded"`) |
|
||||
| `details` | map<string, string> | Arbitrary key-value details (version, uptime, etc.) |
|
||||
|
||||
#### PluginEvent
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `event_type` | string | The event name (e.g., `"license:updated"`, `"repo:push"`) |
|
||||
| `payload` | google.protobuf.Struct | Event-specific data as a JSON-like structure |
|
||||
| `timestamp` | google.protobuf.Timestamp | When the event occurred |
|
||||
| `repo_id` | int64 | Repository ID (0 if not repo-specific) |
|
||||
| `org_id` | int64 | Organization ID (0 if not org-specific) |
|
||||
|
||||
#### EventResponse
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `handled` | bool | Whether the plugin handled the event |
|
||||
| `error` | string | Error message if handling failed |
|
||||
|
||||
#### HTTPRequest / HTTPResponse
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `method` | string | HTTP method (`GET`, `POST`, etc.) |
|
||||
| `path` | string | Request path (e.g., `/api/v1/health`) |
|
||||
| `headers` | map<string, string> | HTTP headers |
|
||||
| `body` | bytes | Request/response body |
|
||||
| `query_params` | map<string, string> | Query parameters (request only) |
|
||||
| `status_code` | int32 | HTTP status code (response only) |
|
||||
|
||||
## Plugin Manifest
|
||||
|
||||
The manifest declares what your plugin does. It is returned during `Initialize` and can be re-fetched via `GetManifest`.
|
||||
|
||||
```protobuf
|
||||
message PluginManifest {
|
||||
string name = 1;
|
||||
string version = 2;
|
||||
string description = 3;
|
||||
repeated string subscribed_events = 4;
|
||||
repeated PluginRoute routes = 5;
|
||||
repeated string required_permissions = 6;
|
||||
string license_tier = 7;
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description | Example |
|
||||
|-------|-------------|---------|
|
||||
| `name` | Display name | `"My Analytics Plugin"` |
|
||||
| `version` | Semver version | `"1.2.0"` |
|
||||
| `description` | What the plugin does | `"Tracks repository analytics"` |
|
||||
| `subscribed_events` | Events to receive | `["repo:push", "issue:created"]` |
|
||||
| `routes` | HTTP routes the plugin handles | See below |
|
||||
| `required_permissions` | Permissions the plugin needs | `["repo:read", "issue:write"]` |
|
||||
| `license_tier` | Minimum license tier required | `"standard"`, `"professional"`, `"enterprise"` |
|
||||
|
||||
### Routes
|
||||
|
||||
Routes declare which HTTP paths your plugin handles. When the server receives a request matching a plugin's route, it proxies the request via `HandleHTTP`.
|
||||
|
||||
```protobuf
|
||||
message PluginRoute {
|
||||
string method = 1; // "GET", "POST", etc.
|
||||
string path = 2; // "/api/v1/my-plugin/endpoint"
|
||||
string description = 3;
|
||||
}
|
||||
```
|
||||
|
||||
Route matching uses prefix matching: a declared path of `/api/v1/analytics` will match `/api/v1/analytics/events`.
|
||||
|
||||
### Events
|
||||
|
||||
Subscribe to server events by listing them in `subscribed_events`. Use `"*"` to subscribe to all events.
|
||||
|
||||
Available events include:
|
||||
- `license:updated` - License key changed
|
||||
- `repo:push` - Code pushed to a repository
|
||||
- `repo:created` - New repository created
|
||||
- `issue:created` - New issue opened
|
||||
- `issue:comment` - Comment added to an issue
|
||||
- `pull_request:opened` - New pull request opened
|
||||
- `pull_request:merged` - Pull request merged
|
||||
|
||||
Events are dispatched asynchronously (fire-and-forget) with a 30-second timeout per plugin.
|
||||
|
||||
### Permissions
|
||||
|
||||
The `required_permissions` field declares what server resources your plugin needs access to. The server logs these at startup for admin review.
|
||||
|
||||
## Health Monitoring
|
||||
|
||||
The server health-checks every registered plugin at a configurable interval (default: 30 seconds).
|
||||
|
||||
**Behavior:**
|
||||
|
||||
| Consecutive Failures | Action |
|
||||
|---------------------|--------|
|
||||
| 1-2 | Warning logged, plugin stays online |
|
||||
| 3+ | Plugin marked **offline**, error logged |
|
||||
| 3+ (managed mode) | Automatic restart attempted |
|
||||
|
||||
When a previously offline plugin responds to a health check, it is marked **online** and an info log is emitted.
|
||||
|
||||
If `HealthCheckResponse.healthy` is `false` (the RPC succeeds but the plugin reports unhealthy), the plugin is marked as **error** status. This allows plugins to report degraded operation (e.g., missing API key, expired license) without being treated as crashed.
|
||||
|
||||
**Health check timeout** is configured per-plugin via `HEALTH_TIMEOUT` (default: 5 seconds).
|
||||
|
||||
## Protocol Versioning
|
||||
|
||||
The plugin protocol uses explicit version negotiation to ensure forward compatibility. Both the server and plugin exchange their supported protocol version during `Initialize`:
|
||||
|
||||
1. The server sends `protocol_version = 1` in `InitializeRequest`
|
||||
2. The plugin returns `protocol_version = 1` in `InitializeResponse`
|
||||
3. The server stores the plugin's version and checks it before calling any RPCs added in later versions
|
||||
|
||||
**What this means for plugin developers:**
|
||||
|
||||
- **You don't need to recompile** when the server adds new fields to existing messages. Protobuf handles this automatically — unknown fields are ignored, missing fields use zero-value defaults.
|
||||
- **You don't need to recompile** when the server adds new event types. Your plugin only receives events it subscribed to.
|
||||
- **You only need to update** if you want to use features from a newer protocol version (e.g., new RPCs added in protocol v2).
|
||||
|
||||
**Version history:**
|
||||
|
||||
| Version | RPCs | Notes |
|
||||
|---------|------|-------|
|
||||
| 1 | Initialize, Shutdown, HealthCheck, GetManifest, OnEvent, HandleHTTP | Initial release |
|
||||
|
||||
**Pre-versioning plugins** (those that don't set `protocol_version` in their response) return `0`, which the server treats as version `1`. This means all existing plugins are compatible without changes.
|
||||
|
||||
## Configuration
|
||||
|
||||
Plugins are configured in the server's `app.ini`.
|
||||
|
||||
### External Mode
|
||||
|
||||
The plugin runs independently. The server connects to its gRPC endpoint.
|
||||
|
||||
```ini
|
||||
[plugins]
|
||||
ENABLED = true
|
||||
HEALTH_CHECK_INTERVAL = 30s
|
||||
|
||||
[plugins.my-plugin]
|
||||
ENABLED = true
|
||||
ADDRESS = localhost:9090
|
||||
HEALTH_TIMEOUT = 5s
|
||||
SUBSCRIBED_EVENTS = repo:push, issue:created
|
||||
```
|
||||
|
||||
### Managed Mode
|
||||
|
||||
The server launches the plugin binary and manages its lifecycle. If the plugin crashes, the server restarts it automatically.
|
||||
|
||||
```ini
|
||||
[plugins.my-plugin]
|
||||
ENABLED = true
|
||||
BINARY = /opt/plugins/my-plugin
|
||||
ARGS = --port 9090 --log-level info
|
||||
ADDRESS = localhost:9090
|
||||
HEALTH_TIMEOUT = 5s
|
||||
```
|
||||
|
||||
When `BINARY` is set, the server:
|
||||
1. Starts the process with the specified arguments
|
||||
2. Waits 2 seconds for the process to initialize
|
||||
3. Calls `Initialize` via gRPC
|
||||
4. Sends `SIGINT` on server shutdown
|
||||
5. Auto-restarts the process if health checks fail 3 consecutive times
|
||||
|
||||
### Configuration Reference
|
||||
|
||||
#### `[plugins]` Section
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `ENABLED` | bool | `true` | Master switch for the plugin framework |
|
||||
| `PATH` | string | `data/plugins` | Directory for plugin data |
|
||||
| `HEALTH_CHECK_INTERVAL` | duration | `30s` | How often to health-check plugins |
|
||||
|
||||
#### `[plugins.<name>]` Section
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `ENABLED` | bool | `true` | Whether this plugin is active |
|
||||
| `ADDRESS` | string | (required) | gRPC endpoint (e.g., `localhost:9090`) |
|
||||
| `BINARY` | string | (optional) | Path to plugin binary (enables managed mode) |
|
||||
| `ARGS` | string | (optional) | Arguments for the binary |
|
||||
| `HEALTH_TIMEOUT` | duration | `5s` | Timeout for health check RPCs |
|
||||
| `SUBSCRIBED_EVENTS` | string | (optional) | Comma-separated event names |
|
||||
|
||||
A plugin must have either `BINARY` or `ADDRESS` (or both for managed mode). Entries with neither are skipped with a warning.
|
||||
|
||||
## Transport
|
||||
|
||||
Plugins communicate over **cleartext HTTP/2 (h2c)** by default. The server uses the gRPC wire protocol via [Connect RPC](https://connectrpc.com/).
|
||||
|
||||
**Requirements for your plugin's gRPC server:**
|
||||
- Listen on a TCP port
|
||||
- Support HTTP/2 (standard for any gRPC server)
|
||||
- No TLS required for local communication (h2c)
|
||||
|
||||
The server constructs its gRPC client with `connect.WithGRPC()`, which uses the standard gRPC binary protocol. This means your plugin can use **any** gRPC server implementation:
|
||||
|
||||
| Language | gRPC Library |
|
||||
|----------|-------------|
|
||||
| Go | `google.golang.org/grpc` or `connectrpc.com/connect` |
|
||||
| C# | `Grpc.AspNetCore` |
|
||||
| Python | `grpcio` |
|
||||
| Java | `io.grpc` |
|
||||
| Rust | `tonic` |
|
||||
| Node.js | `@grpc/grpc-js` |
|
||||
|
||||
## Example: Go Plugin
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
|
||||
"code.gitcaddy.com/server/v3/modules/plugins/pluginv1/pluginv1connect"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
type myPlugin struct {
|
||||
pluginv1connect.UnimplementedPluginServiceHandler
|
||||
}
|
||||
|
||||
func (p *myPlugin) Initialize(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pluginv1.InitializeRequest],
|
||||
) (*connect.Response[pluginv1.InitializeResponse], error) {
|
||||
log.Printf("Initialized by server %s (protocol v%d)", req.Msg.ServerVersion, req.Msg.ProtocolVersion)
|
||||
return connect.NewResponse(&pluginv1.InitializeResponse{
|
||||
Success: true,
|
||||
ProtocolVersion: 1,
|
||||
Manifest: &pluginv1.PluginManifest{
|
||||
Name: "My Plugin",
|
||||
Version: "1.0.0",
|
||||
Description: "Does something useful",
|
||||
SubscribedEvents: []string{"repo:push"},
|
||||
Routes: []*pluginv1.PluginRoute{
|
||||
{Method: "GET", Path: "/api/v1/my-plugin/status", Description: "Plugin status"},
|
||||
},
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p *myPlugin) HealthCheck(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pluginv1.HealthCheckRequest],
|
||||
) (*connect.Response[pluginv1.HealthCheckResponse], error) {
|
||||
return connect.NewResponse(&pluginv1.HealthCheckResponse{
|
||||
Healthy: true,
|
||||
Status: "operational",
|
||||
Details: map[string]string{"version": "1.0.0"},
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p *myPlugin) Shutdown(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pluginv1.ShutdownRequest],
|
||||
) (*connect.Response[pluginv1.ShutdownResponse], error) {
|
||||
log.Printf("Shutdown requested: %s", req.Msg.Reason)
|
||||
return connect.NewResponse(&pluginv1.ShutdownResponse{Success: true}), nil
|
||||
}
|
||||
|
||||
func (p *myPlugin) OnEvent(
|
||||
ctx context.Context,
|
||||
req *connect.Request[pluginv1.PluginEvent],
|
||||
) (*connect.Response[pluginv1.EventResponse], error) {
|
||||
log.Printf("Event: %s for repo %d", req.Msg.EventType, req.Msg.RepoId)
|
||||
return connect.NewResponse(&pluginv1.EventResponse{Handled: true}), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
mux := http.NewServeMux()
|
||||
path, handler := pluginv1connect.NewPluginServiceHandler(&myPlugin{})
|
||||
mux.Handle(path, handler)
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":9090",
|
||||
Handler: h2c.NewHandler(mux, &http2.Server{}),
|
||||
}
|
||||
log.Println("Plugin listening on :9090")
|
||||
log.Fatal(server.ListenAndServe())
|
||||
}
|
||||
```
|
||||
|
||||
**app.ini:**
|
||||
```ini
|
||||
[plugins.my-plugin]
|
||||
ENABLED = true
|
||||
ADDRESS = localhost:9090
|
||||
SUBSCRIBED_EVENTS = repo:push
|
||||
```
|
||||
|
||||
## Example: C# Plugin
|
||||
|
||||
```csharp
|
||||
using Grpc.Core;
|
||||
// Assumes plugin.proto is included in the .csproj with GrpcServices="Server"
|
||||
|
||||
public class MyPlugin : PluginService.PluginServiceBase
|
||||
{
|
||||
public override Task<InitializeResponse> Initialize(
|
||||
InitializeRequest request, ServerCallContext context)
|
||||
{
|
||||
var manifest = new PluginManifest
|
||||
{
|
||||
Name = "My C# Plugin",
|
||||
Version = "1.0.0",
|
||||
Description = "A C# plugin for GitCaddy"
|
||||
};
|
||||
manifest.SubscribedEvents.Add("issue:created");
|
||||
|
||||
return Task.FromResult(new InitializeResponse
|
||||
{
|
||||
Success = true,
|
||||
ProtocolVersion = 1,
|
||||
Manifest = manifest
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<HealthCheckResponse> HealthCheck(
|
||||
HealthCheckRequest request, ServerCallContext context)
|
||||
{
|
||||
return Task.FromResult(new HealthCheckResponse
|
||||
{
|
||||
Healthy = true,
|
||||
Status = "operational"
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<ShutdownResponse> Shutdown(
|
||||
ShutdownRequest request, ServerCallContext context)
|
||||
{
|
||||
return Task.FromResult(new ShutdownResponse { Success = true });
|
||||
}
|
||||
|
||||
public override Task<EventResponse> OnEvent(
|
||||
PluginEvent request, ServerCallContext context)
|
||||
{
|
||||
Console.WriteLine($"Event: {request.EventType} for repo {request.RepoId}");
|
||||
return Task.FromResult(new EventResponse { Handled = true });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Program.cs:**
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddGrpc();
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.ListenAnyIP(9090, o =>
|
||||
o.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http2);
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
app.MapGrpcService<MyPlugin>();
|
||||
app.Run();
|
||||
```
|
||||
|
||||
## Example: Python Plugin
|
||||
|
||||
```python
|
||||
import grpc
|
||||
from concurrent import futures
|
||||
|
||||
# Generated from plugin.proto using grpcio-tools:
|
||||
# python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. plugin.proto
|
||||
import plugin_pb2
|
||||
import plugin_pb2_grpc
|
||||
|
||||
class MyPlugin(plugin_pb2_grpc.PluginServiceServicer):
|
||||
def Initialize(self, request, context):
|
||||
manifest = plugin_pb2.PluginManifest(
|
||||
name="My Python Plugin",
|
||||
version="1.0.0",
|
||||
description="A Python plugin for GitCaddy",
|
||||
subscribed_events=["repo:push"],
|
||||
)
|
||||
return plugin_pb2.InitializeResponse(success=True, protocol_version=1, manifest=manifest)
|
||||
|
||||
def HealthCheck(self, request, context):
|
||||
return plugin_pb2.HealthCheckResponse(
|
||||
healthy=True,
|
||||
status="operational",
|
||||
details={"version": "1.0.0"},
|
||||
)
|
||||
|
||||
def Shutdown(self, request, context):
|
||||
print(f"Shutdown: {request.reason}")
|
||||
return plugin_pb2.ShutdownResponse(success=True)
|
||||
|
||||
def OnEvent(self, request, context):
|
||||
print(f"Event: {request.event_type} for repo {request.repo_id}")
|
||||
return plugin_pb2.EventResponse(handled=True)
|
||||
|
||||
def HandleHTTP(self, request, context):
|
||||
return plugin_pb2.HTTPResponse(status_code=501)
|
||||
|
||||
def GetManifest(self, request, context):
|
||||
return plugin_pb2.PluginManifest(
|
||||
name="My Python Plugin",
|
||||
version="1.0.0",
|
||||
)
|
||||
|
||||
def serve():
|
||||
server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
|
||||
plugin_pb2_grpc.add_PluginServiceServicer_to_server(MyPlugin(), server)
|
||||
server.add_insecure_port("[::]:9090")
|
||||
server.start()
|
||||
print("Plugin listening on :9090")
|
||||
server.wait_for_termination()
|
||||
|
||||
if __name__ == "__main__":
|
||||
serve()
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
**Server logs** show plugin lifecycle events:
|
||||
|
||||
```
|
||||
[I] Loaded external plugin config: my-plugin (managed=false)
|
||||
[I] External plugin my-plugin is online (managed=false)
|
||||
[W] Health check failed for plugin my-plugin: connection refused
|
||||
[E] Plugin my-plugin is now offline after 3 consecutive health check failures
|
||||
[I] Plugin my-plugin is back online
|
||||
[I] Shutting down external plugin: my-plugin
|
||||
```
|
||||
|
||||
**Tips:**
|
||||
|
||||
- Use `HEALTH_TIMEOUT = 10s` during development to avoid false positives
|
||||
- Set `HEALTH_CHECK_INTERVAL = 5s` for faster feedback during testing
|
||||
- Check that your plugin supports cleartext HTTP/2 (h2c) - this is the most common connection issue
|
||||
- Use `grpcurl` to test your plugin's gRPC service independently:
|
||||
|
||||
```bash
|
||||
# List services
|
||||
grpcurl -plaintext localhost:9090 list
|
||||
|
||||
# Call HealthCheck
|
||||
grpcurl -plaintext localhost:9090 plugin.v1.PluginService/HealthCheck
|
||||
|
||||
# Call Initialize
|
||||
grpcurl -plaintext -d '{"server_version": "3.0.0"}' \
|
||||
localhost:9090 plugin.v1.PluginService/Initialize
|
||||
```
|
||||
321
README.md
321
README.md
@@ -17,6 +17,11 @@ The AI-native Git platform. Self-hosted, fast, and designed for the age of AI-as
|
||||
- [Package Registry](#package-registry)
|
||||
- [Vault System (Pro/Enterprise)](#vault-system-proenterprise)
|
||||
- [AI-Powered Features](#ai-powered-features)
|
||||
- [Tier 1 - Light Operations](#ai-powered-features)
|
||||
- [Tier 2 - Agent Operations](#ai-powered-features)
|
||||
- [Configuration Cascade](#ai-powered-features)
|
||||
- [Built-in Safety](#ai-powered-features)
|
||||
- [Plugin Framework](#plugin-framework)
|
||||
- [Landing Pages & Public Releases](#landing-pages--public-releases)
|
||||
- [App Update API (Electron/Squirrel Compatible)](#app-update-api-electronsquirrel-compatible)
|
||||
- [Release Archive](#release-archive)
|
||||
@@ -30,6 +35,8 @@ The AI-native Git platform. Self-hosted, fast, and designed for the age of AI-as
|
||||
- [Configuration](#configuration)
|
||||
- [Database Setup](#database-setup)
|
||||
- [AI Features Configuration](#ai-features-configuration)
|
||||
- [Plugin Framework Configuration](#plugin-framework-configuration)
|
||||
- [V2 API Configuration](#v2-api-configuration)
|
||||
- [Authentication Sources](#authentication-sources)
|
||||
- [Email/SMTP Setup](#emailsmtp-setup)
|
||||
- [Unsplash Integration](#unsplash-integration)
|
||||
@@ -42,6 +49,11 @@ The AI-native Git platform. Self-hosted, fast, and designed for the age of AI-as
|
||||
- [Vault Usage (Pro/Enterprise)](#vault-usage-proenterprise)
|
||||
- [Landing Pages Configuration](#landing-pages-configuration)
|
||||
- [AI Features Usage](#ai-features-usage)
|
||||
- [Automatic Operations](#automatic-operations-event-driven)
|
||||
- [Manual API Triggers](#manual-api-triggers)
|
||||
- [Viewing AI Operation History](#viewing-ai-operation-history)
|
||||
- [Escalation](#escalation)
|
||||
- [AI Context APIs](#ai-context-apis-for-external-ai-tools)
|
||||
- [GitCaddy Runner](#gitcaddy-runner)
|
||||
- [API Documentation](#api-documentation)
|
||||
- [Internationalization](#internationalization)
|
||||
@@ -262,10 +274,43 @@ Enterprise-grade encrypted secrets management:
|
||||
|
||||
### AI-Powered Features
|
||||
|
||||
- **AI Code Review:** Automated code review suggestions on pull requests
|
||||
- **Issue Triage:** Automatic categorization and priority assignment
|
||||
- **Code Explanation:** Generate documentation and explain complex code
|
||||
- **Error Diagnosis:** AI learning patterns for debugging assistance
|
||||
GitCaddy integrates with the [gitcaddy-ai](https://git.marketally.com/gitcaddy/gitcaddy-ai) sidecar service to provide two tiers of AI operations:
|
||||
|
||||
**Tier 1 - Light Operations** (seconds, via gRPC/REST to gitcaddy-ai):
|
||||
- **AI Code Review:** Automatic review comments on pull requests when opened or updated
|
||||
- **Issue Auto-Respond:** AI generates helpful responses to new issues and @bot mentions
|
||||
- **Issue Triage:** Automatic label assignment based on issue content
|
||||
- **Code Explanation:** On-demand explanation of code sections
|
||||
- **Documentation Generation:** Generate docs for code files
|
||||
|
||||
**Tier 2 - Agent Operations** (minutes, via Actions runners with Claude Code):
|
||||
- **Agent Fix:** AI agent clones the repo, investigates issues, creates branches and pull requests
|
||||
- **Triggered by labels** (e.g., adding `ai-fix` label) or manual API call
|
||||
- **Sandboxed** in existing Actions runner infrastructure
|
||||
|
||||
**Configuration Cascade:**
|
||||
Settings resolve from most specific to least: Repo > Org > System. Each level can override the AI provider, model, and API key.
|
||||
|
||||
**Built-in Safety:**
|
||||
- Dedicated bot user (`gitcaddy-ai`) for clear attribution
|
||||
- Loop prevention: bot-created events never re-trigger AI
|
||||
- Per-repo rate limiting (ops/hour)
|
||||
- Admin feature gates for every operation type
|
||||
- Escalation to staff when AI confidence is low or operations fail
|
||||
- Full audit log of every AI operation with provider, tokens, duration, and status
|
||||
|
||||
### Plugin Framework
|
||||
|
||||
Extend GitCaddy with external plugins that communicate over gRPC (HTTP/2). The server manages plugin lifecycle, health monitoring, and event dispatch.
|
||||
|
||||
- **gRPC Protocol**: Type-safe plugin contract defined in `plugin.proto` with 6 RPCs (Initialize, Shutdown, HealthCheck, GetManifest, OnEvent, HandleHTTP)
|
||||
- **Health Monitoring**: Automatic periodic health checks with configurable interval; plugins marked offline after 3 consecutive failures
|
||||
- **Auto-Restart**: Managed plugins are automatically restarted when they become unresponsive
|
||||
- **Event Dispatch**: Plugins subscribe to server events (e.g., `license:updated`) and receive them in real-time
|
||||
- **Route Declaration**: Plugins declare REST routes in their manifest; the server can proxy HTTP requests to the plugin
|
||||
- **Managed & External Modes**: Server can launch and manage plugin processes, or connect to already-running services
|
||||
|
||||
See [PLUGINS.md](PLUGINS.md) for the full plugin development guide.
|
||||
|
||||
### Landing Pages & Public Releases
|
||||
|
||||
@@ -492,15 +537,147 @@ SSL_MODE = disable
|
||||
|
||||
### AI Features Configuration
|
||||
|
||||
Enable and configure AI-powered features in `app.ini`:
|
||||
AI features require the [gitcaddy-ai](https://git.marketally.com/gitcaddy/gitcaddy-ai) sidecar service running alongside your GitCaddy instance. See the gitcaddy-ai README for deployment instructions (Docker Compose recommended).
|
||||
|
||||
**1. Enable AI in `app.ini`:**
|
||||
|
||||
```ini
|
||||
[server]
|
||||
ROOT_URL = https://your-instance.com/
|
||||
|
||||
[actions]
|
||||
[ai]
|
||||
; Master switch — nothing AI-related runs unless this is true
|
||||
ENABLED = true
|
||||
|
||||
; gitcaddy-ai sidecar address (REST port)
|
||||
SERVICE_URL = localhost:5050
|
||||
SERVICE_TOKEN =
|
||||
|
||||
; Connection settings
|
||||
TIMEOUT = 30s
|
||||
MAX_RETRIES = 3
|
||||
|
||||
; Default provider/model (fallback when org/repo don't override)
|
||||
DEFAULT_PROVIDER = claude ; claude, openai, or gemini
|
||||
DEFAULT_MODEL = claude-sonnet-4-20250514
|
||||
|
||||
; System API keys (used when org/repo don't provide their own)
|
||||
CLAUDE_API_KEY = sk-ant-...
|
||||
OPENAI_API_KEY =
|
||||
GEMINI_API_KEY =
|
||||
|
||||
; Rate limiting (per repo)
|
||||
MAX_OPERATIONS_PER_HOUR = 100
|
||||
MAX_TOKENS_PER_OPERATION = 8192
|
||||
|
||||
; Feature gates — admin controls what's available instance-wide
|
||||
ENABLE_CODE_REVIEW = true ; Tier 1: PR review
|
||||
ENABLE_ISSUE_TRIAGE = true ; Tier 1: auto-label issues
|
||||
ENABLE_DOC_GEN = true ; Tier 1: documentation generation
|
||||
ENABLE_EXPLAIN_CODE = true ; Tier 1: code explanation
|
||||
ENABLE_CHAT = true ; Tier 1: chat interface
|
||||
ALLOW_AUTO_RESPOND = true ; Tier 1: auto-respond to issues
|
||||
ALLOW_AUTO_REVIEW = true ; Tier 1: auto-review PRs
|
||||
ALLOW_AGENT_MODE = false ; Tier 2: agent fix (off by default, requires runner setup)
|
||||
|
||||
; Content limits
|
||||
MAX_FILE_SIZE_KB = 500
|
||||
MAX_DIFF_LINES = 5000
|
||||
|
||||
; Bot user (created automatically on first startup)
|
||||
BOT_USER_NAME = gitcaddy-ai
|
||||
```
|
||||
|
||||
**2. Configure org-level settings (optional):**
|
||||
|
||||
Organization admins can override the provider, model, and API key for all repos in the org. This is done via:
|
||||
- **Web UI:** `/{org}/settings/ai`
|
||||
- **API:** `PUT /api/v2/orgs/{org}/ai/settings`
|
||||
|
||||
Org settings include:
|
||||
- Provider & model selection
|
||||
- API key (encrypted at rest)
|
||||
- Rate limits (max ops/hour)
|
||||
- Allowed operations list
|
||||
- Agent mode toggle
|
||||
|
||||
**3. Enable AI on a repository:**
|
||||
|
||||
Repository admins enable AI and choose which operations to activate. This is done via:
|
||||
- **Web UI:** `/{owner}/{repo}/settings/ai`
|
||||
- **API:** `PUT /api/v2/repos/{owner}/{repo}/ai/settings`
|
||||
|
||||
Repo settings include:
|
||||
- **Enable/Disable:** Toggles the AI unit on the repo
|
||||
- **Tier 1 Operations:** Auto-respond to issues, auto-review PRs, auto-triage issues, auto-inspect workflows
|
||||
- **Tier 2 Agent Mode:** Enable agent fix, configure trigger labels (e.g., `ai-fix`), set max run time
|
||||
- **Escalation:** Enable staff escalation on failure, set escalation label and team
|
||||
- **Provider Override:** Override org/system provider and model for this repo
|
||||
- **Custom Instructions:** System instructions, review instructions, issue response instructions
|
||||
|
||||
**4. Set up Tier 2 agent runners (optional):**
|
||||
|
||||
For agent mode (AI that creates branches and PRs), you need an Actions runner with Claude Code installed:
|
||||
|
||||
1. Register a runner with the `ai-runner` label
|
||||
2. Install the Claude Code CLI on the runner
|
||||
3. Place the agent workflow file in your repo at `.gitea/workflows/ai-agent.yml` (a template is available via the API)
|
||||
4. Set the `AI_API_KEY` secret on the repo or org
|
||||
|
||||
When a trigger label is added to an issue (or an agent fix is requested via API), the server dispatches the workflow, the runner clones the repo, runs Claude Code headless, and creates a PR with the proposed fix.
|
||||
|
||||
### Plugin Framework Configuration
|
||||
|
||||
The plugin framework allows external services (like the AI sidecar) to register with GitCaddy for lifecycle management, health monitoring, and event dispatch. Plugins communicate over gRPC (HTTP/2) using the protocol defined in `modules/plugins/pluginv1/plugin.proto`.
|
||||
|
||||
**1. Enable the plugin framework in `app.ini`:**
|
||||
|
||||
```ini
|
||||
[plugins]
|
||||
; Master switch for the plugin framework
|
||||
ENABLED = true
|
||||
; Directory for plugin data
|
||||
PATH = data/plugins
|
||||
; How often to health-check external plugins
|
||||
HEALTH_CHECK_INTERVAL = 30s
|
||||
```
|
||||
|
||||
**2. Register external plugins:**
|
||||
|
||||
Each plugin gets its own `[plugins.<name>]` section:
|
||||
|
||||
```ini
|
||||
[plugins.gitcaddy-ai]
|
||||
ENABLED = true
|
||||
; Address of the plugin's gRPC endpoint (cleartext HTTP/2)
|
||||
ADDRESS = localhost:5000
|
||||
; Health check timeout
|
||||
HEALTH_TIMEOUT = 5s
|
||||
; Events the plugin subscribes to (comma-separated)
|
||||
SUBSCRIBED_EVENTS = license:updated
|
||||
```
|
||||
|
||||
**Managed vs External Mode:**
|
||||
|
||||
| Mode | Configuration | Use Case |
|
||||
|------|--------------|----------|
|
||||
| **External** | `ADDRESS` only | Plugin runs independently (Docker, systemd, sidecar) |
|
||||
| **Managed** | `BINARY` + `ARGS` | Server launches and manages the plugin process |
|
||||
|
||||
Managed mode example:
|
||||
```ini
|
||||
[plugins.my-plugin]
|
||||
ENABLED = true
|
||||
BINARY = /usr/local/bin/my-plugin
|
||||
ARGS = --port 5001
|
||||
ADDRESS = localhost:5001
|
||||
HEALTH_TIMEOUT = 5s
|
||||
```
|
||||
|
||||
When a managed plugin becomes unresponsive (3 consecutive health check failures), the server automatically restarts its process.
|
||||
|
||||
See [PLUGINS.md](PLUGINS.md) for the full plugin development guide.
|
||||
|
||||
### V2 API Configuration
|
||||
|
||||
```ini
|
||||
[api]
|
||||
; Enable V2 API (enabled by default)
|
||||
ENABLE_V2_API = true
|
||||
@@ -800,36 +977,96 @@ advanced:
|
||||
|
||||
### AI Features Usage
|
||||
|
||||
**AI Code Review:**
|
||||
1. Create a pull request
|
||||
2. Enable AI review in PR settings
|
||||
3. AI will analyze changes and provide suggestions
|
||||
4. Review and apply suggestions as needed
|
||||
#### Automatic Operations (Event-Driven)
|
||||
|
||||
**Issue Triage:**
|
||||
- AI automatically categorizes new issues
|
||||
- Suggests labels based on content
|
||||
- Estimates complexity
|
||||
- Recommends relevant files for investigation
|
||||
Once AI is enabled on a repo with the desired operations toggled on, these fire automatically:
|
||||
|
||||
**Code Explanation:**
|
||||
1. Select code in the file viewer
|
||||
2. Click "Explain with AI"
|
||||
3. View generated documentation
|
||||
| Event | Operations Triggered | Tier |
|
||||
|-------|---------------------|------|
|
||||
| New issue created | Auto-respond + Auto-triage | 1 |
|
||||
| Comment mentions `@gitcaddy-ai` | Auto-respond | 1 |
|
||||
| Pull request opened | Auto-review | 1 |
|
||||
| PR branch updated (push) | Auto-review (re-review) | 1 |
|
||||
| Trigger label added to issue | Agent fix | 2 |
|
||||
|
||||
All operations are queued asynchronously and processed by the AI service. The bot user posts comments/reviews on the issue or PR with the results.
|
||||
|
||||
#### Manual API Triggers
|
||||
|
||||
Trigger AI operations on demand via the v2 API:
|
||||
|
||||
**Using AI Context APIs:**
|
||||
```bash
|
||||
# Get repository summary
|
||||
curl https://your-instance.com/api/v2/ai/repo/summary?owner=owner&repo=repo \
|
||||
# Trigger AI code review on a pull request
|
||||
curl -X POST https://your-instance.com/api/v2/repos/owner/repo/ai/review/1 \
|
||||
-H "Authorization: token YOUR_TOKEN"
|
||||
|
||||
# Get issue context
|
||||
curl https://your-instance.com/api/v2/ai/issue/context?owner=owner&repo=repo&issue=123 \
|
||||
# Trigger AI response to an issue
|
||||
curl -X POST https://your-instance.com/api/v2/repos/owner/repo/ai/respond/42 \
|
||||
-H "Authorization: token YOUR_TOKEN"
|
||||
|
||||
# Get repository navigation
|
||||
curl https://your-instance.com/api/v2/ai/repo/navigation?owner=owner&repo=repo&depth=3 \
|
||||
# Trigger AI triage on an issue
|
||||
curl -X POST https://your-instance.com/api/v2/repos/owner/repo/ai/triage/42 \
|
||||
-H "Authorization: token YOUR_TOKEN"
|
||||
|
||||
# Explain code in a file
|
||||
curl -X POST https://your-instance.com/api/v2/repos/owner/repo/ai/explain \
|
||||
-H "Authorization: token YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"file_path": "main.go", "start_line": 10, "end_line": 50, "question": "What does this function do?"}'
|
||||
|
||||
# Trigger Tier 2 agent fix (creates a PR)
|
||||
curl -X POST https://your-instance.com/api/v2/repos/owner/repo/ai/fix/42 \
|
||||
-H "Authorization: token YOUR_TOKEN"
|
||||
```
|
||||
|
||||
All trigger endpoints return `202 Accepted` with the queued operation details.
|
||||
|
||||
#### Viewing AI Operation History
|
||||
|
||||
Every AI operation is logged with full audit details:
|
||||
|
||||
```bash
|
||||
# List operations for a repo (paginated, filterable)
|
||||
curl "https://your-instance.com/api/v2/repos/owner/repo/ai/operations?status=success&tier=1&page=1" \
|
||||
-H "Authorization: token YOUR_TOKEN"
|
||||
|
||||
# Get a single operation's details
|
||||
curl https://your-instance.com/api/v2/repos/owner/repo/ai/operations/123 \
|
||||
-H "Authorization: token YOUR_TOKEN"
|
||||
|
||||
# Global operation log (site admin only)
|
||||
curl https://your-instance.com/api/v2/admin/ai/operations \
|
||||
-H "Authorization: token YOUR_TOKEN"
|
||||
```
|
||||
|
||||
Each operation log entry includes: operation type, tier, trigger event, provider, model, input/output tokens, duration, status (success/failed/escalated), and the resulting comment or Actions run ID.
|
||||
|
||||
#### Escalation
|
||||
|
||||
When an AI operation fails or returns low confidence, and the repo has escalation configured:
|
||||
1. The configured escalation label (e.g., `needs-human-review`) is added to the issue
|
||||
2. The bot posts a summary comment explaining what it attempted and why it's escalating
|
||||
3. If an escalation team is configured, a team member is assigned
|
||||
|
||||
#### AI Context APIs (for External AI Tools)
|
||||
|
||||
These endpoints provide structured context **to** external AI tools (IDE plugins, MCP servers, etc.):
|
||||
|
||||
```bash
|
||||
# Get repository summary (languages, build system, entry points)
|
||||
curl -X POST https://your-instance.com/api/v2/ai/repo/summary \
|
||||
-H "Authorization: token YOUR_TOKEN" \
|
||||
-d '{"owner": "owner", "repo": "repo"}'
|
||||
|
||||
# Get issue context (details, comments, complexity hints)
|
||||
curl -X POST https://your-instance.com/api/v2/ai/issue/context \
|
||||
-H "Authorization: token YOUR_TOKEN" \
|
||||
-d '{"owner": "owner", "repo": "repo", "issue": 123}'
|
||||
|
||||
# Get repository navigation (directory tree, important paths)
|
||||
curl -X POST https://your-instance.com/api/v2/ai/repo/navigation \
|
||||
-H "Authorization: token YOUR_TOKEN" \
|
||||
-d '{"owner": "owner", "repo": "repo", "depth": 3}'
|
||||
```
|
||||
|
||||
## GitCaddy Runner
|
||||
@@ -924,6 +1161,29 @@ GET /api/v2/batch/files?paths=file1.go,file2.go&owner=owner&repo=repo
|
||||
|
||||
# Stream files (NDJSON)
|
||||
POST /api/v2/stream/files
|
||||
|
||||
# AI: Get/update repo AI settings
|
||||
GET /api/v2/repos/{owner}/{repo}/ai/settings
|
||||
PUT /api/v2/repos/{owner}/{repo}/ai/settings
|
||||
|
||||
# AI: Trigger operations
|
||||
POST /api/v2/repos/{owner}/{repo}/ai/review/{pull}
|
||||
POST /api/v2/repos/{owner}/{repo}/ai/respond/{issue}
|
||||
POST /api/v2/repos/{owner}/{repo}/ai/triage/{issue}
|
||||
POST /api/v2/repos/{owner}/{repo}/ai/explain
|
||||
POST /api/v2/repos/{owner}/{repo}/ai/fix/{issue}
|
||||
|
||||
# AI: Operation audit log
|
||||
GET /api/v2/repos/{owner}/{repo}/ai/operations
|
||||
GET /api/v2/repos/{owner}/{repo}/ai/operations/{id}
|
||||
|
||||
# AI: Org settings
|
||||
GET /api/v2/orgs/{org}/ai/settings
|
||||
PUT /api/v2/orgs/{org}/ai/settings
|
||||
|
||||
# AI: Admin
|
||||
GET /api/v2/admin/ai/status
|
||||
GET /api/v2/admin/ai/operations
|
||||
```
|
||||
|
||||
## Internationalization
|
||||
@@ -1060,6 +1320,7 @@ GitCaddy is a fork of [Gitea](https://gitea.io), the open-source self-hosted Git
|
||||
| Project | Description |
|
||||
|---------|-------------|
|
||||
| [gitcaddy/act_runner](https://git.marketally.com/gitcaddy/act_runner) | Runner with automatic capability detection |
|
||||
| [gitcaddy/gitcaddy-ai](https://git.marketally.com/gitcaddy/gitcaddy-ai) | AI sidecar service (.NET 9, gRPC/REST) for code review, triage, and more |
|
||||
| [gitcaddy/actions-proto-go](https://git.marketally.com/gitcaddy/actions-proto-go) | Protocol buffer definitions for Actions |
|
||||
|
||||
**Support:**
|
||||
|
||||
206
README.zh-cn.md
206
README.zh-cn.md
@@ -1,206 +0,0 @@
|
||||
# Gitea
|
||||
|
||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
|
||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
|
||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||
[](https://opensource.org/licenses/MIT "License: MIT")
|
||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
|
||||
[](https://translate.gitea.com "Crowdin")
|
||||
|
||||
[English](./README.md) | [繁體中文](./README.zh-tw.md)
|
||||
|
||||
## 目的
|
||||
|
||||
这个项目的目标是提供最简单、最快速、最无痛的方式来设置自托管的 Git 服务。
|
||||
|
||||
由于 Gitea 是用 Go 语言编写的,它可以在 Go 支持的所有平台和架构上运行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架构。这个项目自 2016 年 11 月从 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而来,但已经有了很多变化。
|
||||
|
||||
在线演示可以访问 [demo.gitea.com](https://demo.gitea.com)。
|
||||
|
||||
要访问免费的 Gitea 服务(有一定数量的仓库限制),可以访问 [gitea.com](https://gitea.com/user/login)。
|
||||
|
||||
要快速部署您自己的专用 Gitea 实例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 开始免费试用。
|
||||
|
||||
## 文件
|
||||
|
||||
您可以在我们的官方 [文件网站](https://docs.gitea.com/) 上找到全面的文件。
|
||||
|
||||
它包括安装、管理、使用、开发、贡献指南等,帮助您快速入门并有效地探索所有功能。
|
||||
|
||||
如果您有任何建议或想要贡献,可以访问 [文件仓库](https://gitea.com/gitea/docs)
|
||||
|
||||
## 构建
|
||||
|
||||
从源代码树的根目录运行:
|
||||
|
||||
TAGS="bindata" make build
|
||||
|
||||
如果需要 SQLite 支持:
|
||||
|
||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
|
||||
`build` 目标分为两个子目标:
|
||||
|
||||
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定义。
|
||||
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
|
||||
|
||||
需要互联网连接来下载 go 和 npm 模块。从包含预构建前端文件的官方源代码压缩包构建时,不会触发 `frontend` 目标,因此可以在没有 Node.js 的情况下构建。
|
||||
|
||||
更多信息:https://docs.gitea.com/installation/install-from-source
|
||||
|
||||
## 使用
|
||||
|
||||
构建后,默认情况下会在源代码树的根目录生成一个名为 `gitea` 的二进制文件。要运行它,请使用:
|
||||
|
||||
./gitea web
|
||||
|
||||
> [!注意]
|
||||
> 如果您对使用我们的 API 感兴趣,我们提供了实验性支持,并附有 [文件](https://docs.gitea.com/api)。
|
||||
|
||||
## 贡献
|
||||
|
||||
预期的工作流程是:Fork -> Patch -> Push -> Pull Request
|
||||
|
||||
> [!注意]
|
||||
>
|
||||
> 1. **在开始进行 Pull Request 之前,您必须阅读 [贡献者指南](CONTRIBUTING.md)。**
|
||||
> 2. 如果您在项目中发现了漏洞,请私下写信给 **security@gitea.io**。谢谢!
|
||||
|
||||
## 翻译
|
||||
|
||||
[](https://translate.gitea.com)
|
||||
|
||||
翻译通过 [Crowdin](https://translate.gitea.com) 进行。如果您想翻译成新的语言,请在 Crowdin 项目中请求管理员添加新语言。
|
||||
|
||||
您也可以创建一个 issue 来添加语言,或者在 discord 的 #translation 频道上询问。如果您需要上下文或发现一些翻译问题,可以在字符串上留言或在 Discord 上询问。对于一般的翻译问题,文档中有一个部分。目前有点空,但我们希望随着问题的出现而填充它。
|
||||
|
||||
更多信息请参阅 [文件](https://docs.gitea.com/contributing/localization)。
|
||||
|
||||
## 官方和第三方项目
|
||||
|
||||
我们提供了一个官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一个名为 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一个 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
|
||||
|
||||
我们在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 维护了一个 Gitea 相关项目的列表,您可以在那里发现更多的第三方项目,包括 SDK、插件、主题等。
|
||||
|
||||
## 通讯
|
||||
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
|
||||
如果您有任何文件未涵盖的问题,可以在我们的 [Discord 服务器](https://discord.gg/Gitea) 上与我们联系,或者在 [discourse 论坛](https://forum.gitea.com/) 上创建帖子。
|
||||
|
||||
## 作者
|
||||
|
||||
- [维护者](https://github.com/orgs/go-gitea/people)
|
||||
- [贡献者](https://github.com/go-gitea/gitea/graphs/contributors)
|
||||
- [翻译者](options/locale/TRANSLATORS)
|
||||
|
||||
## 支持者
|
||||
|
||||
感谢所有支持者! 🙏 [[成为支持者](https://opencollective.com/gitea#backer)]
|
||||
|
||||
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
|
||||
|
||||
## 赞助商
|
||||
|
||||
通过成为赞助商来支持这个项目。您的标志将显示在这里,并带有链接到您的网站。 [[成为赞助商](https://opencollective.com/gitea#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Gitea 怎么发音?**
|
||||
|
||||
Gitea 的发音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一样,g 是硬音。
|
||||
|
||||
**为什么这个项目没有托管在 Gitea 实例上?**
|
||||
|
||||
我们正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
|
||||
|
||||
**在哪里可以找到安全补丁?**
|
||||
|
||||
在 [发布日志](https://github.com/go-gitea/gitea/releases) 或 [变更日志](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索关键词 `SECURITY` 以找到安全补丁。
|
||||
|
||||
## 许可证
|
||||
|
||||
这个项目是根据 MIT 许可证授权的。
|
||||
请参阅 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以获取完整的许可证文本。
|
||||
|
||||
## 进一步信息
|
||||
|
||||
<details>
|
||||
<summary>寻找界面概述?查看这里!</summary>
|
||||
|
||||
### 登录/注册页面
|
||||
|
||||

|
||||

|
||||
|
||||
### 用户仪表板
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 用户资料
|
||||
|
||||

|
||||
|
||||
### 探索
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### 仓库
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 仓库问题
|
||||
|
||||

|
||||

|
||||
|
||||
#### 仓库拉取请求
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 仓库操作
|
||||
|
||||

|
||||

|
||||
|
||||
#### 仓库活动
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 组织
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
206
README.zh-tw.md
206
README.zh-tw.md
@@ -1,206 +0,0 @@
|
||||
# Gitea
|
||||
|
||||
[](https://github.com/go-gitea/gitea/actions/workflows/release-nightly.yml?query=branch%3Amain "Release Nightly")
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
[](https://goreportcard.com/report/code.gitea.io/gitea "Go Report Card")
|
||||
[](https://pkg.go.dev/code.gitea.io/gitea "GoDoc")
|
||||
[](https://github.com/go-gitea/gitea/releases/latest "GitHub release")
|
||||
[](https://www.codetriage.com/go-gitea/gitea "Help Contribute to Open Source")
|
||||
[](https://opencollective.com/gitea "Become a backer/sponsor of gitea")
|
||||
[](https://opensource.org/licenses/MIT "License: MIT")
|
||||
[](https://gitpod.io/#https://github.com/go-gitea/gitea)
|
||||
[](https://translate.gitea.com "Crowdin")
|
||||
|
||||
[English](./README.md) | [简体中文](./README.zh-cn.md)
|
||||
|
||||
## 目的
|
||||
|
||||
這個項目的目標是提供最簡單、最快速、最無痛的方式來設置自託管的 Git 服務。
|
||||
|
||||
由於 Gitea 是用 Go 語言編寫的,它可以在 Go 支援的所有平台和架構上運行,包括 Linux、macOS 和 Windows 的 x86、amd64、ARM 和 PowerPC 架構。這個項目自 2016 年 11 月從 [Gogs](https://gogs.io) [分叉](https://blog.gitea.com/welcome-to-gitea/) 而來,但已經有了很多變化。
|
||||
|
||||
在線演示可以訪問 [demo.gitea.com](https://demo.gitea.com)。
|
||||
|
||||
要訪問免費的 Gitea 服務(有一定數量的倉庫限制),可以訪問 [gitea.com](https://gitea.com/user/login)。
|
||||
|
||||
要快速部署您自己的專用 Gitea 實例,可以在 [cloud.gitea.com](https://cloud.gitea.com) 開始免費試用。
|
||||
|
||||
## 文件
|
||||
|
||||
您可以在我們的官方 [文件網站](https://docs.gitea.com/) 上找到全面的文件。
|
||||
|
||||
它包括安裝、管理、使用、開發、貢獻指南等,幫助您快速入門並有效地探索所有功能。
|
||||
|
||||
如果您有任何建議或想要貢獻,可以訪問 [文件倉庫](https://gitea.com/gitea/docs)
|
||||
|
||||
## 構建
|
||||
|
||||
從源代碼樹的根目錄運行:
|
||||
|
||||
TAGS="bindata" make build
|
||||
|
||||
如果需要 SQLite 支援:
|
||||
|
||||
TAGS="bindata sqlite sqlite_unlock_notify" make build
|
||||
|
||||
`build` 目標分為兩個子目標:
|
||||
|
||||
- `make backend` 需要 [Go Stable](https://go.dev/dl/),所需版本在 [go.mod](/go.mod) 中定義。
|
||||
- `make frontend` 需要 [Node.js LTS](https://nodejs.org/en/download/) 或更高版本。
|
||||
|
||||
需要互聯網連接來下載 go 和 npm 模塊。從包含預構建前端文件的官方源代碼壓縮包構建時,不會觸發 `frontend` 目標,因此可以在沒有 Node.js 的情況下構建。
|
||||
|
||||
更多信息:https://docs.gitea.com/installation/install-from-source
|
||||
|
||||
## 使用
|
||||
|
||||
構建後,默認情況下會在源代碼樹的根目錄生成一個名為 `gitea` 的二進制文件。要運行它,請使用:
|
||||
|
||||
./gitea web
|
||||
|
||||
> [!注意]
|
||||
> 如果您對使用我們的 API 感興趣,我們提供了實驗性支援,並附有 [文件](https://docs.gitea.com/api)。
|
||||
|
||||
## 貢獻
|
||||
|
||||
預期的工作流程是:Fork -> Patch -> Push -> Pull Request
|
||||
|
||||
> [!注意]
|
||||
>
|
||||
> 1. **在開始進行 Pull Request 之前,您必須閱讀 [貢獻者指南](CONTRIBUTING.md)。**
|
||||
> 2. 如果您在項目中發現了漏洞,請私下寫信給 **security@gitea.io**。謝謝!
|
||||
|
||||
## 翻譯
|
||||
|
||||
[](https://translate.gitea.com)
|
||||
|
||||
翻譯通過 [Crowdin](https://translate.gitea.com) 進行。如果您想翻譯成新的語言,請在 Crowdin 項目中請求管理員添加新語言。
|
||||
|
||||
您也可以創建一個 issue 來添加語言,或者在 discord 的 #translation 頻道上詢問。如果您需要上下文或發現一些翻譯問題,可以在字符串上留言或在 Discord 上詢問。對於一般的翻譯問題,文檔中有一個部分。目前有點空,但我們希望隨著問題的出現而填充它。
|
||||
|
||||
更多信息請參閱 [文件](https://docs.gitea.com/contributing/localization)。
|
||||
|
||||
## 官方和第三方項目
|
||||
|
||||
我們提供了一個官方的 [go-sdk](https://gitea.com/gitea/go-sdk),一個名為 [tea](https://gitea.com/gitea/tea) 的 CLI 工具和一個 Gitea Action 的 [action runner](https://gitea.com/gitea/act_runner)。
|
||||
|
||||
我們在 [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea) 維護了一個 Gitea 相關項目的列表,您可以在那裡發現更多的第三方項目,包括 SDK、插件、主題等。
|
||||
|
||||
## 通訊
|
||||
|
||||
[](https://discord.gg/Gitea "Join the Discord chat at https://discord.gg/Gitea")
|
||||
|
||||
如果您有任何文件未涵蓋的問題,可以在我們的 [Discord 服務器](https://discord.gg/Gitea) 上與我們聯繫,或者在 [discourse 論壇](https://forum.gitea.com/) 上創建帖子。
|
||||
|
||||
## 作者
|
||||
|
||||
- [維護者](https://github.com/orgs/go-gitea/people)
|
||||
- [貢獻者](https://github.com/go-gitea/gitea/graphs/contributors)
|
||||
- [翻譯者](options/locale/TRANSLATORS)
|
||||
|
||||
## 支持者
|
||||
|
||||
感謝所有支持者! 🙏 [[成為支持者](https://opencollective.com/gitea#backer)]
|
||||
|
||||
<a href="https://opencollective.com/gitea#backers" target="_blank"><img src="https://opencollective.com/gitea/backers.svg?width=890"></a>
|
||||
|
||||
## 贊助商
|
||||
|
||||
通過成為贊助商來支持這個項目。您的標誌將顯示在這裡,並帶有鏈接到您的網站。 [[成為贊助商](https://opencollective.com/gitea#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/gitea/sponsor/0/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/1/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/2/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/3/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/4/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/5/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/6/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/7/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/8/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/gitea/sponsor/9/website" target="_blank"><img src="https://opencollective.com/gitea/sponsor/9/avatar.svg"></a>
|
||||
|
||||
## 常見問題
|
||||
|
||||
**Gitea 怎麼發音?**
|
||||
|
||||
Gitea 的發音是 [/ɡɪ’ti:/](https://youtu.be/EM71-2uDAoY),就像 "gi-tea" 一樣,g 是硬音。
|
||||
|
||||
**為什麼這個項目沒有託管在 Gitea 實例上?**
|
||||
|
||||
我們正在 [努力](https://github.com/go-gitea/gitea/issues/1029)。
|
||||
|
||||
**在哪裡可以找到安全補丁?**
|
||||
|
||||
在 [發佈日誌](https://github.com/go-gitea/gitea/releases) 或 [變更日誌](https://github.com/go-gitea/gitea/blob/main/CHANGELOG.md) 中,搜索關鍵詞 `SECURITY` 以找到安全補丁。
|
||||
|
||||
## 許可證
|
||||
|
||||
這個項目是根據 MIT 許可證授權的。
|
||||
請參閱 [LICENSE](https://github.com/go-gitea/gitea/blob/main/LICENSE) 文件以獲取完整的許可證文本。
|
||||
|
||||
## 進一步信息
|
||||
|
||||
<details>
|
||||
<summary>尋找界面概述?查看這裡!</summary>
|
||||
|
||||
### 登錄/註冊頁面
|
||||
|
||||

|
||||

|
||||
|
||||
### 用戶儀表板
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 用戶資料
|
||||
|
||||

|
||||
|
||||
### 探索
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### 倉庫
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 倉庫問題
|
||||
|
||||

|
||||

|
||||
|
||||
#### 倉庫拉取請求
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
#### 倉庫操作
|
||||
|
||||

|
||||

|
||||
|
||||
#### 倉庫活動
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### 組織
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
5
go.mod
5
go.mod
@@ -16,7 +16,7 @@ require (
|
||||
connectrpc.com/connect v1.18.1
|
||||
|
||||
// GitCaddy Vault plugin (compiled-in)
|
||||
git.marketally.com/gitcaddy/gitcaddy-vault v1.0.52
|
||||
git.marketally.com/gitcaddy/gitcaddy-vault v1.0.60
|
||||
gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed
|
||||
gitea.com/go-chi/cache v0.2.1
|
||||
gitea.com/go-chi/captcha v0.0.0-20240315150714-fb487f629098
|
||||
@@ -326,6 +326,9 @@ replace github.com/go-ini/ini => github.com/go-ini/ini v1.66.6
|
||||
// Use GitCaddy fork with capability support
|
||||
replace code.gitea.io/actions-proto-go => git.marketally.com/gitcaddy/actions-proto-go v0.5.8
|
||||
|
||||
// Mirror of deleted github.com/hexops/gotextdiff
|
||||
replace github.com/hexops/gotextdiff => git.marketally.com/mirrors/gotextdiff v1.0.3
|
||||
|
||||
// Vault plugin - use local directory for development
|
||||
replace git.marketally.com/gitcaddy/gitcaddy-vault => ../gitcaddy-vault
|
||||
|
||||
|
||||
4
go.sum
4
go.sum
@@ -31,6 +31,8 @@ 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.8 h1:MBipeHvY6A0jcobvziUtzgatZTrV4fs/HE1rPQxREN4=
|
||||
git.marketally.com/gitcaddy/actions-proto-go v0.5.8/go.mod h1:RPu21UoRD3zSAujoZR6LJwuVNa2uFRBveadslczCRfQ=
|
||||
git.marketally.com/mirrors/gotextdiff v1.0.3 h1:Mxf+YurdCHT4y1GNiZCTDWYtVXSxhlLUeG7g7i9Za70=
|
||||
git.marketally.com/mirrors/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
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=
|
||||
@@ -490,8 +492,6 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
|
||||
@@ -125,6 +125,21 @@ func (opts FindRunOptions) ToOrders() string {
|
||||
return "`action_run`.`id` DESC"
|
||||
}
|
||||
|
||||
// GetLatestRunPerWorkflow returns the most recent run for each workflow in a repo.
|
||||
// Uses a subquery to find the MAX(id) per workflow_id, then loads those runs.
|
||||
func GetLatestRunPerWorkflow(ctx context.Context, repoID int64) (RunList, error) {
|
||||
subQuery := builder.Select("MAX(id)").
|
||||
From("`action_run`").
|
||||
Where(builder.Eq{"`action_run`.repo_id": repoID}).
|
||||
GroupBy("`action_run`.workflow_id")
|
||||
|
||||
var runs RunList
|
||||
err := db.GetEngine(ctx).
|
||||
Where(builder.In("`action_run`.id", subQuery)).
|
||||
Find(&runs)
|
||||
return runs, err
|
||||
}
|
||||
|
||||
type StatusInfo struct {
|
||||
Status int
|
||||
DisplayedStatus string
|
||||
|
||||
@@ -191,9 +191,26 @@ func (r *ActionRunner) GenerateToken() (err error) {
|
||||
|
||||
// CanMatchLabels checks whether the runner's labels can match a job's "runs-on"
|
||||
// See https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on
|
||||
//
|
||||
// Labels are matched by name, ignoring any ":scheme" suffix (e.g., ":host", ":docker").
|
||||
// This means a runner with label "germany-linux:host" will match runs-on "germany-linux",
|
||||
// and a job with runs-on "germany-linux:host" will also match.
|
||||
func (r *ActionRunner) CanMatchLabels(jobRunsOn []string) bool {
|
||||
runnerLabelSet := container.SetOf(r.AgentLabels...)
|
||||
return runnerLabelSet.Contains(jobRunsOn...) // match all labels
|
||||
// Build a set of runner label names (stripped of :scheme suffix)
|
||||
runnerNames := make(container.Set[string], len(r.AgentLabels))
|
||||
for _, label := range r.AgentLabels {
|
||||
name, _, _ := strings.Cut(label, ":")
|
||||
runnerNames.Add(name)
|
||||
}
|
||||
|
||||
// Check that every runs-on label (also stripped of :scheme) is in the runner set
|
||||
for _, requiredLabel := range jobRunsOn {
|
||||
name, _, _ := strings.Cut(requiredLabel, ":")
|
||||
if !runnerNames.Contains(name) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
207
models/ai/operation_log.go
Normal file
207
models/ai/operation_log.go
Normal file
@@ -0,0 +1,207 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
"code.gitcaddy.com/server/v3/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(OperationLog))
|
||||
}
|
||||
|
||||
// OperationLog records every AI operation for auditing
|
||||
type OperationLog struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
Operation string `xorm:"VARCHAR(50) NOT NULL"` // "code-review", "issue-response", etc.
|
||||
Tier int `xorm:"NOT NULL"` // 1 or 2
|
||||
TriggerEvent string `xorm:"VARCHAR(100) NOT NULL"`
|
||||
TriggerUserID int64 `xorm:"INDEX"`
|
||||
TargetID int64 `xorm:"INDEX"` // issue/PR ID
|
||||
TargetType string `xorm:"VARCHAR(20)"` // "issue", "pull", "commit"
|
||||
Provider string `xorm:"VARCHAR(20)"`
|
||||
Model string `xorm:"VARCHAR(100)"`
|
||||
InputTokens int `xorm:"DEFAULT 0"`
|
||||
OutputTokens int `xorm:"DEFAULT 0"`
|
||||
Status string `xorm:"VARCHAR(20) NOT NULL"` // "success", "failed", "escalated", "pending"
|
||||
ResultCommentID int64 `xorm:"DEFAULT 0"`
|
||||
ActionRunID int64 `xorm:"DEFAULT 0"` // for Tier 2
|
||||
ErrorMessage string `xorm:"TEXT"`
|
||||
DurationMs int64 `xorm:"DEFAULT 0"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for OperationLog
|
||||
func (OperationLog) TableName() string {
|
||||
return "ai_operation_log"
|
||||
}
|
||||
|
||||
// OperationStatus constants
|
||||
const (
|
||||
OperationStatusPending = "pending"
|
||||
OperationStatusSuccess = "success"
|
||||
OperationStatusFailed = "failed"
|
||||
OperationStatusEscalated = "escalated"
|
||||
)
|
||||
|
||||
// InsertOperationLog creates a new operation log entry
|
||||
func InsertOperationLog(ctx context.Context, log *OperationLog) error {
|
||||
return db.Insert(ctx, log)
|
||||
}
|
||||
|
||||
// UpdateOperationLog updates an existing operation log entry
|
||||
func UpdateOperationLog(ctx context.Context, log *OperationLog) error {
|
||||
_, err := db.GetEngine(ctx).ID(log.ID).AllCols().Update(log)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOperationLog returns a single operation log entry by ID
|
||||
func GetOperationLog(ctx context.Context, id int64) (*OperationLog, error) {
|
||||
log := &OperationLog{}
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return log, nil
|
||||
}
|
||||
|
||||
// FindOperationLogsOptions represents options for finding operation logs
|
||||
type FindOperationLogsOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
Operation string
|
||||
Status string
|
||||
Tier int
|
||||
}
|
||||
|
||||
func (opts FindOperationLogsOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
if opts.Operation != "" {
|
||||
cond = cond.And(builder.Eq{"operation": opts.Operation})
|
||||
}
|
||||
if opts.Status != "" {
|
||||
cond = cond.And(builder.Eq{"status": opts.Status})
|
||||
}
|
||||
if opts.Tier > 0 {
|
||||
cond = cond.And(builder.Eq{"tier": opts.Tier})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts FindOperationLogsOptions) ToOrders() string {
|
||||
return "created_unix DESC"
|
||||
}
|
||||
|
||||
// CountRecentOperations counts operations in the last hour for rate limiting
|
||||
func CountRecentOperations(ctx context.Context, repoID int64) (int64, error) {
|
||||
oneHourAgo := timeutil.TimeStampNow() - 3600
|
||||
return db.GetEngine(ctx).Where("repo_id = ? AND created_unix > ?", repoID, oneHourAgo).Count(new(OperationLog))
|
||||
}
|
||||
|
||||
// GlobalOperationStats holds aggregate AI operation statistics for admin dashboard
|
||||
type GlobalOperationStats struct {
|
||||
TotalOperations int64 `json:"total_operations"`
|
||||
Operations24h int64 `json:"operations_24h"`
|
||||
SuccessCount int64 `json:"success_count"`
|
||||
FailedCount int64 `json:"failed_count"`
|
||||
EscalatedCount int64 `json:"escalated_count"`
|
||||
PendingCount int64 `json:"pending_count"`
|
||||
CountByTier map[int]int64 `json:"count_by_tier"`
|
||||
TopRepos []RepoOpCount `json:"top_repos"`
|
||||
TotalInputTokens int64 `json:"total_input_tokens"`
|
||||
TotalOutputTokens int64 `json:"total_output_tokens"`
|
||||
}
|
||||
|
||||
// RepoOpCount holds a repo's operation count for the top-repos list
|
||||
type RepoOpCount struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
// GetGlobalOperationStats returns aggregate statistics across all repos for the admin dashboard
|
||||
func GetGlobalOperationStats(ctx context.Context) (*GlobalOperationStats, error) {
|
||||
e := db.GetEngine(ctx)
|
||||
stats := &GlobalOperationStats{
|
||||
CountByTier: make(map[int]int64),
|
||||
}
|
||||
|
||||
// Total operations
|
||||
total, err := e.Count(new(OperationLog))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.TotalOperations = total
|
||||
|
||||
// Operations in last 24 hours
|
||||
oneDayAgo := timeutil.TimeStampNow() - 86400
|
||||
stats.Operations24h, err = e.Where("created_unix > ?", oneDayAgo).Count(new(OperationLog))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Counts by status
|
||||
stats.SuccessCount, err = e.Where("status = ?", OperationStatusSuccess).Count(new(OperationLog))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.FailedCount, err = e.Where("status = ?", OperationStatusFailed).Count(new(OperationLog))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.EscalatedCount, err = e.Where("status = ?", OperationStatusEscalated).Count(new(OperationLog))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.PendingCount, err = e.Where("status = ?", OperationStatusPending).Count(new(OperationLog))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Counts by tier
|
||||
type tierCount struct {
|
||||
Tier int `xorm:"tier"`
|
||||
Count int64 `xorm:"count"`
|
||||
}
|
||||
var tierCounts []tierCount
|
||||
if err := e.Table("ai_operation_log").Select("tier, count(*) as count").GroupBy("tier").Find(&tierCounts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, tc := range tierCounts {
|
||||
stats.CountByTier[tc.Tier] = tc.Count
|
||||
}
|
||||
|
||||
// Top 5 repos by operation count
|
||||
var topRepos []RepoOpCount
|
||||
if err := e.Table("ai_operation_log").Select("repo_id, count(*) as count").
|
||||
GroupBy("repo_id").OrderBy("count DESC").Limit(5).Find(&topRepos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.TopRepos = topRepos
|
||||
|
||||
// Total tokens
|
||||
type tokenSum struct {
|
||||
InputTokens int64 `xorm:"input_tokens"`
|
||||
OutputTokens int64 `xorm:"output_tokens"`
|
||||
}
|
||||
var ts tokenSum
|
||||
if _, err := e.Table("ai_operation_log").Select("COALESCE(SUM(input_tokens),0) as input_tokens, COALESCE(SUM(output_tokens),0) as output_tokens").Get(&ts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.TotalInputTokens = ts.InputTokens
|
||||
stats.TotalOutputTokens = ts.OutputTokens
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
135
models/ai/settings.go
Normal file
135
models/ai/settings.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
secret_module "code.gitcaddy.com/server/v3/modules/secret"
|
||||
"code.gitcaddy.com/server/v3/modules/setting"
|
||||
"code.gitcaddy.com/server/v3/modules/timeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(OrgAISettings))
|
||||
}
|
||||
|
||||
// OrgAISettings stores AI configuration per organization
|
||||
type OrgAISettings struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
OrgID int64 `xorm:"UNIQUE NOT NULL INDEX"`
|
||||
Provider string `xorm:"NOT NULL DEFAULT ''"`
|
||||
Model string `xorm:"NOT NULL DEFAULT ''"`
|
||||
APIKeyEncrypted string `xorm:"TEXT"`
|
||||
MaxOpsPerHour int `xorm:"NOT NULL DEFAULT 0"`
|
||||
AllowedOps string `xorm:"TEXT"` // JSON array of allowed operation names
|
||||
AgentModeAllowed bool `xorm:"NOT NULL DEFAULT false"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX UPDATED"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for OrgAISettings
|
||||
func (OrgAISettings) TableName() string {
|
||||
return "org_ai_settings"
|
||||
}
|
||||
|
||||
// SetAPIKey encrypts and stores the API key
|
||||
func (s *OrgAISettings) SetAPIKey(key string) error {
|
||||
if key == "" {
|
||||
s.APIKeyEncrypted = ""
|
||||
return nil
|
||||
}
|
||||
encrypted, err := secret_module.EncryptSecret(setting.SecretKey, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.APIKeyEncrypted = encrypted
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAPIKey decrypts and returns the API key
|
||||
func (s *OrgAISettings) GetAPIKey() (string, error) {
|
||||
if s.APIKeyEncrypted == "" {
|
||||
return "", nil
|
||||
}
|
||||
return secret_module.DecryptSecret(setting.SecretKey, s.APIKeyEncrypted)
|
||||
}
|
||||
|
||||
// GetOrgAISettings returns the AI settings for an organization
|
||||
func GetOrgAISettings(ctx context.Context, orgID int64) (*OrgAISettings, error) {
|
||||
settings := &OrgAISettings{OrgID: orgID}
|
||||
has, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Get(settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
// CreateOrUpdateOrgAISettings creates or updates AI settings for an organization
|
||||
func CreateOrUpdateOrgAISettings(ctx context.Context, settings *OrgAISettings) error {
|
||||
existing := &OrgAISettings{}
|
||||
has, err := db.GetEngine(ctx).Where("org_id = ?", settings.OrgID).Get(existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if has {
|
||||
settings.ID = existing.ID
|
||||
_, err = db.GetEngine(ctx).ID(existing.ID).AllCols().Update(settings)
|
||||
return err
|
||||
}
|
||||
return db.Insert(ctx, settings)
|
||||
}
|
||||
|
||||
// ResolveProvider resolves the AI provider using the cascade: repo → org → system
|
||||
func ResolveProvider(ctx context.Context, orgID int64, repoProvider string) string {
|
||||
if repoProvider != "" {
|
||||
return repoProvider
|
||||
}
|
||||
if orgID > 0 {
|
||||
if orgSettings, err := GetOrgAISettings(ctx, orgID); err == nil && orgSettings != nil && orgSettings.Provider != "" {
|
||||
return orgSettings.Provider
|
||||
}
|
||||
}
|
||||
return setting.AI.DefaultProvider
|
||||
}
|
||||
|
||||
// ResolveModel resolves the AI model using the cascade: repo → org → system
|
||||
func ResolveModel(ctx context.Context, orgID int64, repoModel string) string {
|
||||
if repoModel != "" {
|
||||
return repoModel
|
||||
}
|
||||
if orgID > 0 {
|
||||
if orgSettings, err := GetOrgAISettings(ctx, orgID); err == nil && orgSettings != nil && orgSettings.Model != "" {
|
||||
return orgSettings.Model
|
||||
}
|
||||
}
|
||||
return setting.AI.DefaultModel
|
||||
}
|
||||
|
||||
// ResolveAPIKey resolves the API key using the cascade: org → system
|
||||
func ResolveAPIKey(ctx context.Context, orgID int64, provider string) string {
|
||||
// Try org-level key first
|
||||
if orgID > 0 {
|
||||
if orgSettings, err := GetOrgAISettings(ctx, orgID); err == nil && orgSettings != nil {
|
||||
if key, err := orgSettings.GetAPIKey(); err == nil && key != "" {
|
||||
return key
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to system-level key
|
||||
switch provider {
|
||||
case "claude":
|
||||
return setting.AI.ClaudeAPIKey
|
||||
case "openai":
|
||||
return setting.AI.OpenAIAPIKey
|
||||
case "gemini":
|
||||
return setting.AI.GeminiAPIKey
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ type BlogPost struct { //revive:disable-line:exported
|
||||
Status BlogPostStatus `xorm:"SMALLINT NOT NULL DEFAULT 0"`
|
||||
AllowComments bool `xorm:"NOT NULL DEFAULT true"`
|
||||
SubscriptionOnly bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ViewCount int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
PublishedUnix timeutil.TimeStamp `xorm:"INDEX"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
@@ -297,9 +298,10 @@ type TagCount struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// GetExploreTopTags returns the top N tags across all accessible published blog posts.
|
||||
// GetExploreTopTags returns the top N tags across recent accessible published blog posts.
|
||||
// Limits scan to most recent 500 posts for performance.
|
||||
func GetExploreTopTags(ctx context.Context, actor *user_model.User, limit int) ([]*TagCount, error) {
|
||||
// Fetch all tags from accessible posts
|
||||
// Fetch tags from recent accessible posts (limit scan for performance)
|
||||
type tagRow struct {
|
||||
Tags string `xorm:"tags"`
|
||||
}
|
||||
@@ -307,6 +309,8 @@ func GetExploreTopTags(ctx context.Context, actor *user_model.User, limit int) (
|
||||
err := exploreBlogBaseSess(ctx, actor).
|
||||
Cols("blog_post.tags").
|
||||
Where("blog_post.tags != ''").
|
||||
OrderBy("blog_post.published_unix DESC").
|
||||
Limit(500).
|
||||
Find(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -410,6 +414,20 @@ func CountPublishedBlogsByRepoID(ctx context.Context, repoID int64) (int64, erro
|
||||
return db.GetEngine(ctx).Where("repo_id = ? AND status >= ?", repoID, BlogPostPublic).Count(new(BlogPost))
|
||||
}
|
||||
|
||||
// HasSubscriptionOnlyBlogPosts returns true if the repo has any published subscription-only blog posts.
|
||||
func HasSubscriptionOnlyBlogPosts(ctx context.Context, repoID int64) (bool, error) {
|
||||
count, err := db.GetEngine(ctx).Where("repo_id = ? AND status >= ? AND subscription_only = ?", repoID, BlogPostPublic, true).Count(new(BlogPost))
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// IsPublishedBlogFeaturedImage checks if the given attachment ID is used as a
|
||||
// featured image by any published blog post.
|
||||
func IsPublishedBlogFeaturedImage(ctx context.Context, attachmentID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("featured_image_id = ? AND status >= ?", attachmentID, BlogPostPublic).
|
||||
Exist(new(BlogPost))
|
||||
}
|
||||
|
||||
// CreateBlogPost inserts a new blog post.
|
||||
func CreateBlogPost(ctx context.Context, p *BlogPost) error {
|
||||
_, err := db.GetEngine(ctx).Insert(p)
|
||||
@@ -428,6 +446,12 @@ func DeleteBlogPost(ctx context.Context, id int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// IncrementBlogPostViewCount atomically increments the view count for a blog post.
|
||||
func IncrementBlogPostViewCount(ctx context.Context, id int64) error {
|
||||
_, err := db.GetEngine(ctx).Exec("UPDATE blog_post SET view_count = view_count + 1 WHERE id = ?", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// CountPublishedBlogPostsByAuthorID returns the number of published/public blog posts by a user.
|
||||
func CountPublishedBlogPostsByAuthorID(ctx context.Context, authorID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("author_id = ? AND status >= ?", authorID, BlogPostPublic).Count(new(BlogPost))
|
||||
|
||||
@@ -438,6 +438,12 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(361, "Create wishlist_comment table", v1_26.CreateWishlistCommentTable),
|
||||
newMigration(362, "Create wishlist_comment_reaction table", v1_26.CreateWishlistCommentReactionTable),
|
||||
newMigration(363, "Add keep_packages_private to user", v1_26.AddKeepPackagesPrivateToUser),
|
||||
newMigration(364, "Add view_count to blog_post", v1_26.AddViewCountToBlogPost),
|
||||
newMigration(365, "Add public_app_integration to repository", v1_26.AddPublicAppIntegrationToRepository),
|
||||
newMigration(366, "Add page experiment tables for A/B testing", v1_26.AddPageExperimentTables),
|
||||
newMigration(367, "Add pages translation table for multi-language support", v1_26.AddPagesTranslationTable),
|
||||
newMigration(368, "Add owner_display_name to repository", v1_26.AddOwnerDisplayNameToRepository),
|
||||
newMigration(369, "Add public_release_downloads to repository", v1_26.AddPublicReleaseDownloadsToRepository),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
13
models/migrations/v1_26/v364.go
Normal file
13
models/migrations/v1_26/v364.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
func AddViewCountToBlogPost(x *xorm.Engine) error {
|
||||
type BlogPost struct {
|
||||
ViewCount int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
return x.Sync(new(BlogPost))
|
||||
}
|
||||
13
models/migrations/v1_26/v365.go
Normal file
13
models/migrations/v1_26/v365.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
func AddPublicAppIntegrationToRepository(x *xorm.Engine) error {
|
||||
type Repository struct {
|
||||
PublicAppIntegration bool `xorm:"NOT NULL DEFAULT true"`
|
||||
}
|
||||
return x.Sync(new(Repository))
|
||||
}
|
||||
54
models/migrations/v1_26/v366.go
Normal file
54
models/migrations/v1_26/v366.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"code.gitcaddy.com/server/v3/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddPageExperimentTables(x *xorm.Engine) error {
|
||||
type PageExperiment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
Name string `xorm:"VARCHAR(255) NOT NULL"`
|
||||
Status string `xorm:"VARCHAR(32) NOT NULL DEFAULT 'draft'"`
|
||||
CreatedByAI bool `xorm:"NOT NULL DEFAULT true"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
EndsUnix timeutil.TimeStamp `xorm:"DEFAULT 0"`
|
||||
}
|
||||
|
||||
type PageVariant struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
ExperimentID int64 `xorm:"INDEX NOT NULL"`
|
||||
Name string `xorm:"VARCHAR(255) NOT NULL"`
|
||||
IsControl bool `xorm:"NOT NULL DEFAULT false"`
|
||||
Weight int `xorm:"NOT NULL DEFAULT 50"`
|
||||
ConfigOverride string `xorm:"TEXT"`
|
||||
Impressions int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
Conversions int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
type PageEvent struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
VariantID int64 `xorm:"INDEX DEFAULT 0"`
|
||||
ExperimentID int64 `xorm:"INDEX DEFAULT 0"`
|
||||
VisitorID string `xorm:"VARCHAR(64) INDEX"`
|
||||
EventType string `xorm:"VARCHAR(32) NOT NULL"`
|
||||
EventData string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
if err := x.Sync(new(PageExperiment)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := x.Sync(new(PageVariant)); err != nil {
|
||||
return err
|
||||
}
|
||||
return x.Sync(new(PageEvent))
|
||||
}
|
||||
24
models/migrations/v1_26/v367.go
Normal file
24
models/migrations/v1_26/v367.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"code.gitcaddy.com/server/v3/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddPagesTranslationTable(x *xorm.Engine) error {
|
||||
type Translation struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
Lang string `xorm:"VARCHAR(10) NOT NULL"`
|
||||
ConfigJSON string `xorm:"TEXT"`
|
||||
AutoGenerated bool `xorm:"NOT NULL DEFAULT false"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Translation))
|
||||
}
|
||||
16
models/migrations/v1_26/v368.go
Normal file
16
models/migrations/v1_26/v368.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddOwnerDisplayNameToRepository(x *xorm.Engine) error {
|
||||
type Repository struct {
|
||||
OwnerDisplayName string `xorm:"VARCHAR(255)"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Repository))
|
||||
}
|
||||
16
models/migrations/v1_26/v369.go
Normal file
16
models/migrations/v1_26/v369.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddPublicReleaseDownloadsToRepository(x *xorm.Engine) error {
|
||||
type Repository struct {
|
||||
PublicReleaseDownloads bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Repository))
|
||||
}
|
||||
272
models/pages/experiment.go
Normal file
272
models/pages/experiment.go
Normal file
@@ -0,0 +1,272 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pages
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
"code.gitcaddy.com/server/v3/modules/timeutil"
|
||||
)
|
||||
|
||||
// ExperimentStatus represents the status of an A/B test experiment.
|
||||
type ExperimentStatus string
|
||||
|
||||
const (
|
||||
ExperimentStatusDraft ExperimentStatus = "draft"
|
||||
ExperimentStatusActive ExperimentStatus = "active"
|
||||
ExperimentStatusPaused ExperimentStatus = "paused"
|
||||
ExperimentStatusCompleted ExperimentStatus = "completed"
|
||||
ExperimentStatusApproved ExperimentStatus = "approved"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(PageExperiment))
|
||||
db.RegisterModel(new(PageVariant))
|
||||
db.RegisterModel(new(PageEvent))
|
||||
}
|
||||
|
||||
// PageExperiment tracks an A/B test on a landing page.
|
||||
type PageExperiment struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
Name string `xorm:"VARCHAR(255) NOT NULL"`
|
||||
Status ExperimentStatus `xorm:"VARCHAR(32) NOT NULL DEFAULT 'draft'"`
|
||||
CreatedByAI bool `xorm:"NOT NULL DEFAULT true"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
EndsUnix timeutil.TimeStamp `xorm:"DEFAULT 0"`
|
||||
|
||||
Variants []*PageVariant `xorm:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for PageExperiment.
|
||||
func (e *PageExperiment) TableName() string {
|
||||
return "page_experiment"
|
||||
}
|
||||
|
||||
// PageVariant is one arm of an A/B test experiment.
|
||||
type PageVariant struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
ExperimentID int64 `xorm:"INDEX NOT NULL"`
|
||||
Name string `xorm:"VARCHAR(255) NOT NULL"`
|
||||
IsControl bool `xorm:"NOT NULL DEFAULT false"`
|
||||
Weight int `xorm:"NOT NULL DEFAULT 50"`
|
||||
ConfigOverride string `xorm:"TEXT"`
|
||||
Impressions int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
Conversions int64 `xorm:"NOT NULL DEFAULT 0"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for PageVariant.
|
||||
func (v *PageVariant) TableName() string {
|
||||
return "page_variant"
|
||||
}
|
||||
|
||||
// ConversionRate returns the conversion rate for this variant.
|
||||
func (v *PageVariant) ConversionRate() float64 {
|
||||
if v.Impressions == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(v.Conversions) / float64(v.Impressions)
|
||||
}
|
||||
|
||||
// PageEvent tracks visitor interactions with a landing page.
|
||||
type PageEvent struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
VariantID int64 `xorm:"INDEX DEFAULT 0"`
|
||||
ExperimentID int64 `xorm:"INDEX DEFAULT 0"`
|
||||
VisitorID string `xorm:"VARCHAR(64) INDEX"`
|
||||
EventType string `xorm:"VARCHAR(32) NOT NULL"`
|
||||
EventData string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for PageEvent.
|
||||
func (e *PageEvent) TableName() string {
|
||||
return "page_event"
|
||||
}
|
||||
|
||||
// Valid event types
|
||||
const (
|
||||
EventTypeImpression = "impression"
|
||||
EventTypeCTAClick = "cta_click"
|
||||
EventTypeScrollDepth = "scroll_depth"
|
||||
EventTypeClick = "click"
|
||||
EventTypeStar = "star"
|
||||
EventTypeFork = "fork"
|
||||
EventTypeClone = "clone"
|
||||
)
|
||||
|
||||
// CreateExperiment creates a new experiment.
|
||||
func CreateExperiment(ctx context.Context, exp *PageExperiment) error {
|
||||
_, err := db.GetEngine(ctx).Insert(exp)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetExperimentByID returns an experiment by ID.
|
||||
func GetExperimentByID(ctx context.Context, id int64) (*PageExperiment, error) {
|
||||
exp := new(PageExperiment)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(exp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return exp, nil
|
||||
}
|
||||
|
||||
// GetExperimentsByRepoID returns all experiments for a repository.
|
||||
func GetExperimentsByRepoID(ctx context.Context, repoID int64) ([]*PageExperiment, error) {
|
||||
experiments := make([]*PageExperiment, 0, 10)
|
||||
return experiments, db.GetEngine(ctx).Where("repo_id = ?", repoID).
|
||||
Desc("created_unix").Find(&experiments)
|
||||
}
|
||||
|
||||
// GetActiveExperimentByRepoID returns the currently active experiment for a repo, if any.
|
||||
func GetActiveExperimentByRepoID(ctx context.Context, repoID int64) (*PageExperiment, error) {
|
||||
exp := new(PageExperiment)
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("repo_id = ? AND status = ?", repoID, ExperimentStatusActive).
|
||||
Get(exp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return exp, nil
|
||||
}
|
||||
|
||||
// GetAllActiveExperiments returns all active experiments across all repos.
|
||||
func GetAllActiveExperiments(ctx context.Context) ([]*PageExperiment, error) {
|
||||
experiments := make([]*PageExperiment, 0, 50)
|
||||
return experiments, db.GetEngine(ctx).
|
||||
Where("status = ?", ExperimentStatusActive).
|
||||
Find(&experiments)
|
||||
}
|
||||
|
||||
// UpdateExperiment updates an experiment.
|
||||
func UpdateExperiment(ctx context.Context, exp *PageExperiment) error {
|
||||
_, err := db.GetEngine(ctx).ID(exp.ID).AllCols().Update(exp)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateExperimentStatus updates just the status of an experiment.
|
||||
func UpdateExperimentStatus(ctx context.Context, id int64, status ExperimentStatus) error {
|
||||
_, err := db.GetEngine(ctx).ID(id).Cols("status").
|
||||
Update(&PageExperiment{Status: status})
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateVariant creates a new variant for an experiment.
|
||||
func CreateVariant(ctx context.Context, variant *PageVariant) error {
|
||||
_, err := db.GetEngine(ctx).Insert(variant)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetVariantByID returns a variant by ID.
|
||||
func GetVariantByID(ctx context.Context, id int64) (*PageVariant, error) {
|
||||
variant := new(PageVariant)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(variant)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return variant, nil
|
||||
}
|
||||
|
||||
// GetVariantsByExperimentID returns all variants for an experiment.
|
||||
func GetVariantsByExperimentID(ctx context.Context, experimentID int64) ([]*PageVariant, error) {
|
||||
variants := make([]*PageVariant, 0, 5)
|
||||
return variants, db.GetEngine(ctx).
|
||||
Where("experiment_id = ?", experimentID).
|
||||
Find(&variants)
|
||||
}
|
||||
|
||||
// IncrementVariantImpressions increments the impression counter for a variant.
|
||||
func IncrementVariantImpressions(ctx context.Context, variantID int64) error {
|
||||
_, err := db.GetEngine(ctx).Exec(
|
||||
"UPDATE `page_variant` SET impressions = impressions + 1 WHERE id = ?", variantID)
|
||||
return err
|
||||
}
|
||||
|
||||
// IncrementVariantConversions increments the conversion counter for a variant.
|
||||
func IncrementVariantConversions(ctx context.Context, variantID int64) error {
|
||||
_, err := db.GetEngine(ctx).Exec(
|
||||
"UPDATE `page_variant` SET conversions = conversions + 1 WHERE id = ?", variantID)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreatePageEvent records a visitor event.
|
||||
func CreatePageEvent(ctx context.Context, event *PageEvent) error {
|
||||
_, err := db.GetEngine(ctx).Insert(event)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetEventCountByVariant returns event counts grouped by event type for a variant.
|
||||
func GetEventCountByVariant(ctx context.Context, variantID int64) (map[string]int64, error) {
|
||||
type countResult struct {
|
||||
EventType string `xorm:"event_type"`
|
||||
Count int64 `xorm:"cnt"`
|
||||
}
|
||||
results := make([]countResult, 0)
|
||||
err := db.GetEngine(ctx).Table("page_event").
|
||||
Select("event_type, COUNT(*) as cnt").
|
||||
Where("variant_id = ?", variantID).
|
||||
GroupBy("event_type").
|
||||
Find(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
counts := make(map[string]int64, len(results))
|
||||
for _, r := range results {
|
||||
counts[r.EventType] = r.Count
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// GetEventCountsByExperiment returns event counts for all variants in an experiment.
|
||||
func GetEventCountsByExperiment(ctx context.Context, experimentID int64) (map[int64]map[string]int64, error) {
|
||||
type countResult struct {
|
||||
VariantID int64 `xorm:"variant_id"`
|
||||
EventType string `xorm:"event_type"`
|
||||
Count int64 `xorm:"cnt"`
|
||||
}
|
||||
results := make([]countResult, 0)
|
||||
err := db.GetEngine(ctx).Table("page_event").
|
||||
Select("variant_id, event_type, COUNT(*) as cnt").
|
||||
Where("experiment_id = ?", experimentID).
|
||||
GroupBy("variant_id, event_type").
|
||||
Find(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
counts := make(map[int64]map[string]int64)
|
||||
for _, r := range results {
|
||||
if counts[r.VariantID] == nil {
|
||||
counts[r.VariantID] = make(map[string]int64)
|
||||
}
|
||||
counts[r.VariantID][r.EventType] = r.Count
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// RecordRepoAction records a repo action (star, fork, clone) as a page event
|
||||
// if the repo has an active experiment.
|
||||
func RecordRepoAction(ctx context.Context, repoID int64, eventType string) {
|
||||
exp, err := GetActiveExperimentByRepoID(ctx, repoID)
|
||||
if err != nil || exp == nil {
|
||||
return
|
||||
}
|
||||
_ = CreatePageEvent(ctx, &PageEvent{
|
||||
RepoID: repoID,
|
||||
ExperimentID: exp.ID,
|
||||
EventType: eventType,
|
||||
})
|
||||
}
|
||||
70
models/pages/translation.go
Normal file
70
models/pages/translation.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pages
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
"code.gitcaddy.com/server/v3/modules/timeutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Translation))
|
||||
}
|
||||
|
||||
// Translation stores a language-specific translation overlay for a landing page.
|
||||
type Translation struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX NOT NULL"`
|
||||
Lang string `xorm:"VARCHAR(10) NOT NULL"`
|
||||
ConfigJSON string `xorm:"TEXT"`
|
||||
AutoGenerated bool `xorm:"NOT NULL DEFAULT false"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for Translation.
|
||||
func (t *Translation) TableName() string {
|
||||
return "pages_translation"
|
||||
}
|
||||
|
||||
// GetTranslationsByRepoID returns all translations for a repository.
|
||||
func GetTranslationsByRepoID(ctx context.Context, repoID int64) ([]*Translation, error) {
|
||||
translations := make([]*Translation, 0, 10)
|
||||
return translations, db.GetEngine(ctx).Where("repo_id = ?", repoID).
|
||||
Asc("lang").Find(&translations)
|
||||
}
|
||||
|
||||
// GetTranslation returns a specific language translation for a repository.
|
||||
func GetTranslation(ctx context.Context, repoID int64, lang string) (*Translation, error) {
|
||||
t := new(Translation)
|
||||
has, err := db.GetEngine(ctx).Where("repo_id = ? AND lang = ?", repoID, lang).Get(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// CreateTranslation creates a new translation.
|
||||
func CreateTranslation(ctx context.Context, t *Translation) error {
|
||||
_, err := db.GetEngine(ctx).Insert(t)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateTranslation updates an existing translation.
|
||||
func UpdateTranslation(ctx context.Context, t *Translation) error {
|
||||
_, err := db.GetEngine(ctx).ID(t.ID).Cols("config_json", "auto_generated").Update(t)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteTranslation deletes a translation by repo ID and language.
|
||||
func DeleteTranslation(ctx context.Context, repoID int64, lang string) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id = ? AND lang = ?", repoID, lang).
|
||||
Delete(new(Translation))
|
||||
return err
|
||||
}
|
||||
@@ -161,6 +161,7 @@ type Repository struct {
|
||||
Description string `xorm:"TEXT"`
|
||||
DisplayTitle string `xorm:"VARCHAR(255)"`
|
||||
GroupHeader string `xorm:"VARCHAR(255)"`
|
||||
OwnerDisplayName string `xorm:"VARCHAR(255)"`
|
||||
LicenseType string `xorm:"VARCHAR(50)"`
|
||||
Website string `xorm:"VARCHAR(2048)"`
|
||||
OriginalServiceType api.GitServiceType `xorm:"index"`
|
||||
@@ -221,6 +222,8 @@ type Repository struct {
|
||||
SubscriptionsEnabled bool `xorm:"NOT NULL DEFAULT false"`
|
||||
BlogEnabled bool `xorm:"NOT NULL DEFAULT false"`
|
||||
WishlistEnabled bool `xorm:"NOT NULL DEFAULT false"`
|
||||
PublicAppIntegration bool `xorm:"NOT NULL DEFAULT true"`
|
||||
PublicReleaseDownloads bool `xorm:"NOT NULL DEFAULT false"`
|
||||
ObjectFormatName string `xorm:"VARCHAR(6) NOT NULL DEFAULT 'sha1'"`
|
||||
|
||||
TrustModel TrustModelType
|
||||
@@ -467,6 +470,11 @@ func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit
|
||||
Type: tp,
|
||||
Config: new(ActionsConfig),
|
||||
}
|
||||
case unit.TypeAI:
|
||||
return &RepoUnit{
|
||||
Type: tp,
|
||||
Config: new(AIConfig),
|
||||
}
|
||||
case unit.TypeProjects:
|
||||
cfg := new(ProjectsConfig)
|
||||
cfg.ProjectsMode = ProjectsModeNone
|
||||
|
||||
@@ -219,6 +219,62 @@ func (cfg *ActionsConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
// AIConfig describes AI integration config
|
||||
type AIConfig struct {
|
||||
// Tier 1: Light AI operations
|
||||
AutoRespondToIssues bool `json:"auto_respond_issues"`
|
||||
AutoReviewPRs bool `json:"auto_review_prs"`
|
||||
AutoInspectWorkflows bool `json:"auto_inspect_workflows"`
|
||||
AutoTriageIssues bool `json:"auto_triage_issues"`
|
||||
|
||||
// Tier 2: Advanced agent operations
|
||||
AgentModeEnabled bool `json:"agent_mode_enabled"`
|
||||
AgentTriggerLabels []string `json:"agent_trigger_labels"`
|
||||
AgentMaxRunMinutes int `json:"agent_max_run_minutes"`
|
||||
|
||||
// Escalation
|
||||
EscalateToStaff bool `json:"escalate_to_staff"`
|
||||
EscalationLabel string `json:"escalation_label"`
|
||||
EscalationAssignTeam string `json:"escalation_assign_team"`
|
||||
|
||||
// Provider overrides (empty = inherit from org → system)
|
||||
PreferredProvider string `json:"preferred_provider"`
|
||||
PreferredModel string `json:"preferred_model"`
|
||||
|
||||
// Custom instructions
|
||||
SystemInstructions string `json:"system_instructions"`
|
||||
ReviewInstructions string `json:"review_instructions"`
|
||||
IssueInstructions string `json:"issue_instructions"`
|
||||
}
|
||||
|
||||
// FromDB fills up an AIConfig from serialized format.
|
||||
func (cfg *AIConfig) FromDB(bs []byte) error {
|
||||
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
|
||||
}
|
||||
|
||||
// ToDB exports an AIConfig to a serialized format.
|
||||
func (cfg *AIConfig) ToDB() ([]byte, error) {
|
||||
return json.Marshal(cfg)
|
||||
}
|
||||
|
||||
// IsOperationEnabled returns whether a given AI operation is enabled
|
||||
func (cfg *AIConfig) IsOperationEnabled(op string) bool {
|
||||
switch op {
|
||||
case "issue-response":
|
||||
return cfg.AutoRespondToIssues
|
||||
case "issue-triage":
|
||||
return cfg.AutoTriageIssues
|
||||
case "code-review":
|
||||
return cfg.AutoReviewPRs
|
||||
case "workflow-inspect":
|
||||
return cfg.AutoInspectWorkflows
|
||||
case "agent-fix":
|
||||
return cfg.AgentModeEnabled
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ProjectsMode represents the projects enabled for a repository
|
||||
type ProjectsMode string
|
||||
|
||||
@@ -281,6 +337,8 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) {
|
||||
r.Config = new(IssuesConfig)
|
||||
case unit.TypeActions:
|
||||
r.Config = new(ActionsConfig)
|
||||
case unit.TypeAI:
|
||||
r.Config = new(AIConfig)
|
||||
case unit.TypeProjects:
|
||||
r.Config = new(ProjectsConfig)
|
||||
case unit.TypeCode, unit.TypeReleases, unit.TypeWiki, unit.TypePackages:
|
||||
@@ -336,6 +394,11 @@ func (r *RepoUnit) ProjectsConfig() *ProjectsConfig {
|
||||
return r.Config.(*ProjectsConfig)
|
||||
}
|
||||
|
||||
// AIConfig returns config for unit.TypeAI
|
||||
func (r *RepoUnit) AIConfig() *AIConfig {
|
||||
return r.Config.(*AIConfig)
|
||||
}
|
||||
|
||||
func getUnitsByRepoID(ctx context.Context, repoID int64) (units []*RepoUnit, err error) {
|
||||
var tmpUnits []*RepoUnit
|
||||
if err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Find(&tmpUnits); err != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
pages_model "code.gitcaddy.com/server/v3/models/pages"
|
||||
user_model "code.gitcaddy.com/server/v3/models/user"
|
||||
"code.gitcaddy.com/server/v3/modules/timeutil"
|
||||
)
|
||||
@@ -25,7 +26,7 @@ func init() {
|
||||
|
||||
// StarRepo or unstar repository.
|
||||
func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star bool) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
staring := IsStaring(ctx, doer.ID, repo.ID)
|
||||
|
||||
if star {
|
||||
@@ -64,6 +65,10 @@ func StarRepo(ctx context.Context, doer *user_model.User, repo *Repository, star
|
||||
|
||||
return nil
|
||||
})
|
||||
if err == nil && star {
|
||||
pages_model.RecordRepoAction(ctx, repo.ID, pages_model.EventTypeStar)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// IsStaring checks if user has starred given repository.
|
||||
|
||||
@@ -33,6 +33,7 @@ const (
|
||||
TypeProjects // 8 Projects
|
||||
TypePackages // 9 Packages
|
||||
TypeActions // 10 Actions
|
||||
TypeAI // 11 AI
|
||||
|
||||
// FIXME: TEAM-UNIT-PERMISSION: the team unit "admin" permission's design is not right, when a new unit is added in the future,
|
||||
// admin team won't inherit the correct admin permission for the new unit, need to have a complete fix before adding any new unit.
|
||||
@@ -65,6 +66,7 @@ var (
|
||||
TypeProjects,
|
||||
TypePackages,
|
||||
TypeActions,
|
||||
TypeAI,
|
||||
}
|
||||
|
||||
// DefaultRepoUnits contains the default unit types
|
||||
@@ -110,6 +112,7 @@ var (
|
||||
NotAllowedDefaultRepoUnits = []Type{
|
||||
TypeExternalWiki,
|
||||
TypeExternalTracker,
|
||||
TypeAI,
|
||||
}
|
||||
|
||||
disabledRepoUnitsAtomic atomic.Pointer[[]Type] // the units that have been globally disabled
|
||||
@@ -328,6 +331,15 @@ var (
|
||||
perm.AccessModeOwner,
|
||||
}
|
||||
|
||||
UnitAI = Unit{
|
||||
TypeAI,
|
||||
"repo.ai",
|
||||
"/ai",
|
||||
"repo.ai.desc",
|
||||
8,
|
||||
perm.AccessModeOwner,
|
||||
}
|
||||
|
||||
// Units contains all the units
|
||||
Units = map[Type]Unit{
|
||||
TypeCode: UnitCode,
|
||||
@@ -340,6 +352,7 @@ var (
|
||||
TypeProjects: UnitProjects,
|
||||
TypePackages: UnitPackages,
|
||||
TypeActions: UnitActions,
|
||||
TypeAI: UnitAI,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -72,5 +72,38 @@ func GetSystemUserByName(name string) *User {
|
||||
if IsGiteaActionsUserName(name) {
|
||||
return NewActionsUser()
|
||||
}
|
||||
if IsAIUserName(name) {
|
||||
return NewAIUser()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
AIUserID int64 = -3
|
||||
AIUserName = "gitcaddy-ai"
|
||||
AIUserEmail = "ai@gitcaddy.com"
|
||||
)
|
||||
|
||||
func IsAIUserName(name string) bool {
|
||||
return strings.EqualFold(name, AIUserName)
|
||||
}
|
||||
|
||||
// NewAIUser creates and returns the system bot user for AI operations.
|
||||
func NewAIUser() *User {
|
||||
return &User{
|
||||
ID: AIUserID,
|
||||
Name: AIUserName,
|
||||
LowerName: strings.ToLower(AIUserName),
|
||||
IsActive: true,
|
||||
FullName: "GitCaddy AI",
|
||||
Email: AIUserEmail,
|
||||
KeepEmailPrivate: true,
|
||||
LoginName: AIUserName,
|
||||
Type: UserTypeBot,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) IsAI() bool {
|
||||
return u != nil && u.ID == AIUserID
|
||||
}
|
||||
|
||||
@@ -190,6 +190,43 @@ func (c *Client) SummarizeChanges(ctx context.Context, req *SummarizeChangesRequ
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GenerateIssueResponse requests an AI-generated response to an issue
|
||||
func (c *Client) GenerateIssueResponse(ctx context.Context, req *GenerateIssueResponseRequest) (*GenerateIssueResponseResponse, error) {
|
||||
if !IsEnabled() || !setting.AI.AllowAutoRespond {
|
||||
return nil, errors.New("AI auto-respond is not enabled")
|
||||
}
|
||||
|
||||
var resp GenerateIssueResponseResponse
|
||||
if err := c.doRequest(ctx, http.MethodPost, "/issues/respond", req, &resp); err != nil {
|
||||
log.Error("AI GenerateIssueResponse failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// InspectWorkflow sends a workflow for AI inspection
|
||||
func (c *Client) InspectWorkflow(ctx context.Context, req *InspectWorkflowRequest) (*InspectWorkflowResponse, error) {
|
||||
resp := &InspectWorkflowResponse{}
|
||||
if err := c.doRequest(ctx, "POST", "/api/v1/workflows/inspect", req, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ExecuteTask executes a generic AI task via the sidecar
|
||||
func (c *Client) ExecuteTask(ctx context.Context, req *ExecuteTaskRequest) (*ExecuteTaskResponse, error) {
|
||||
if !IsEnabled() {
|
||||
return nil, errors.New("AI service is not enabled")
|
||||
}
|
||||
|
||||
var resp ExecuteTaskResponse
|
||||
if err := c.doRequest(ctx, http.MethodPost, "/execute-task", req, &resp); err != nil {
|
||||
log.Error("AI ExecuteTask failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CheckHealth checks the health of the AI service
|
||||
func (c *Client) CheckHealth(ctx context.Context) (*HealthCheckResponse, error) {
|
||||
var resp HealthCheckResponse
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
|
||||
package ai
|
||||
|
||||
// ProviderConfig contains per-request AI provider configuration.
|
||||
// When sent to the AI sidecar, it overrides the sidecar's default provider/model/key.
|
||||
// Fields left empty fall back to the sidecar's defaults.
|
||||
type ProviderConfig struct {
|
||||
Provider string `json:"provider,omitempty"` // "claude", "openai", "gemini"
|
||||
Model string `json:"model,omitempty"`
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
}
|
||||
|
||||
// FileDiff represents a file diff for code review
|
||||
type FileDiff struct {
|
||||
Path string `json:"path"`
|
||||
@@ -54,14 +63,15 @@ type SecurityAnalysis struct {
|
||||
|
||||
// ReviewPullRequestRequest is the request for reviewing a pull request
|
||||
type ReviewPullRequestRequest struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
PullRequestID int64 `json:"pull_request_id"`
|
||||
BaseBranch string `json:"base_branch"`
|
||||
HeadBranch string `json:"head_branch"`
|
||||
Files []FileDiff `json:"files"`
|
||||
PRTitle string `json:"pr_title"`
|
||||
PRDescription string `json:"pr_description"`
|
||||
Options ReviewOptions `json:"options"`
|
||||
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
PullRequestID int64 `json:"pull_request_id"`
|
||||
BaseBranch string `json:"base_branch"`
|
||||
HeadBranch string `json:"head_branch"`
|
||||
Files []FileDiff `json:"files"`
|
||||
PRTitle string `json:"pr_title"`
|
||||
PRDescription string `json:"pr_description"`
|
||||
Options ReviewOptions `json:"options"`
|
||||
}
|
||||
|
||||
// ReviewPullRequestResponse is the response from reviewing a pull request
|
||||
@@ -76,12 +86,13 @@ type ReviewPullRequestResponse struct {
|
||||
|
||||
// TriageIssueRequest is the request for triaging an issue
|
||||
type TriageIssueRequest struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
IssueID int64 `json:"issue_id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
ExistingLabels []string `json:"existing_labels"`
|
||||
AvailableLabels []string `json:"available_labels"`
|
||||
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
IssueID int64 `json:"issue_id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
ExistingLabels []string `json:"existing_labels"`
|
||||
AvailableLabels []string `json:"available_labels"`
|
||||
}
|
||||
|
||||
// TriageIssueResponse is the response from triaging an issue
|
||||
@@ -97,10 +108,11 @@ type TriageIssueResponse struct {
|
||||
|
||||
// SuggestLabelsRequest is the request for suggesting labels
|
||||
type SuggestLabelsRequest struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
AvailableLabels []string `json:"available_labels"`
|
||||
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
AvailableLabels []string `json:"available_labels"`
|
||||
}
|
||||
|
||||
// LabelSuggestion represents a suggested label
|
||||
@@ -117,12 +129,13 @@ type SuggestLabelsResponse struct {
|
||||
|
||||
// ExplainCodeRequest is the request for explaining code
|
||||
type ExplainCodeRequest struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
FilePath string `json:"file_path"`
|
||||
Code string `json:"code"`
|
||||
StartLine int `json:"start_line"`
|
||||
EndLine int `json:"end_line"`
|
||||
Question string `json:"question,omitempty"`
|
||||
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
FilePath string `json:"file_path"`
|
||||
Code string `json:"code"`
|
||||
StartLine int `json:"start_line"`
|
||||
EndLine int `json:"end_line"`
|
||||
Question string `json:"question,omitempty"`
|
||||
}
|
||||
|
||||
// CodeReference represents a reference to related documentation
|
||||
@@ -140,12 +153,13 @@ type ExplainCodeResponse struct {
|
||||
|
||||
// GenerateDocumentationRequest is the request for generating documentation
|
||||
type GenerateDocumentationRequest struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
FilePath string `json:"file_path"`
|
||||
Code string `json:"code"`
|
||||
DocType string `json:"doc_type"` // function, class, module, api
|
||||
Language string `json:"language"`
|
||||
Style string `json:"style"` // jsdoc, docstring, xml, markdown
|
||||
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
FilePath string `json:"file_path"`
|
||||
Code string `json:"code"`
|
||||
DocType string `json:"doc_type"` // function, class, module, api
|
||||
Language string `json:"language"`
|
||||
Style string `json:"style"` // jsdoc, docstring, xml, markdown
|
||||
}
|
||||
|
||||
// DocumentationSection represents a section of documentation
|
||||
@@ -162,9 +176,10 @@ type GenerateDocumentationResponse struct {
|
||||
|
||||
// GenerateCommitMessageRequest is the request for generating a commit message
|
||||
type GenerateCommitMessageRequest struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Files []FileDiff `json:"files"`
|
||||
Style string `json:"style"` // conventional, descriptive, brief
|
||||
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Files []FileDiff `json:"files"`
|
||||
Style string `json:"style"` // conventional, descriptive, brief
|
||||
}
|
||||
|
||||
// GenerateCommitMessageResponse is the response from generating a commit message
|
||||
@@ -175,9 +190,10 @@ type GenerateCommitMessageResponse struct {
|
||||
|
||||
// SummarizeChangesRequest is the request for summarizing changes
|
||||
type SummarizeChangesRequest struct {
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Files []FileDiff `json:"files"`
|
||||
Context string `json:"context"`
|
||||
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Files []FileDiff `json:"files"`
|
||||
Context string `json:"context"`
|
||||
}
|
||||
|
||||
// SummarizeChangesResponse is the response from summarizing changes
|
||||
@@ -187,6 +203,77 @@ type SummarizeChangesResponse struct {
|
||||
ImpactAssessment string `json:"impact_assessment"`
|
||||
}
|
||||
|
||||
// IssueComment represents a comment on an issue for AI context
|
||||
type IssueComment struct {
|
||||
Author string `json:"author"`
|
||||
Body string `json:"body"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateIssueResponseRequest is the request for generating an AI response to an issue
|
||||
type GenerateIssueResponseRequest struct {
|
||||
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
IssueID int64 `json:"issue_id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Comments []IssueComment `json:"comments,omitempty"`
|
||||
ResponseType string `json:"response_type,omitempty"` // clarification, solution, acknowledgment
|
||||
CustomInstructions string `json:"custom_instructions,omitempty"`
|
||||
}
|
||||
|
||||
// GenerateIssueResponseResponse is the response from generating an issue response
|
||||
type GenerateIssueResponseResponse struct {
|
||||
Response string `json:"response"`
|
||||
FollowUpQuestions []string `json:"follow_up_questions,omitempty"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
}
|
||||
|
||||
// InspectWorkflowRequest is the request for inspecting a workflow file
|
||||
type InspectWorkflowRequest struct {
|
||||
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
FilePath string `json:"file_path"`
|
||||
Content string `json:"content"`
|
||||
RunnerLabels []string `json:"runner_labels,omitempty"`
|
||||
}
|
||||
|
||||
// WorkflowIssue represents an issue found in a workflow file
|
||||
type WorkflowIssue struct {
|
||||
Line int `json:"line"`
|
||||
Severity string `json:"severity"` // "error", "warning", "info"
|
||||
Message string `json:"message"`
|
||||
Fix string `json:"fix,omitempty"`
|
||||
}
|
||||
|
||||
// InspectWorkflowResponse is the response from inspecting a workflow file
|
||||
type InspectWorkflowResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
Issues []WorkflowIssue `json:"issues"`
|
||||
Suggestions []string `json:"suggestions"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
}
|
||||
|
||||
// ExecuteTaskRequest is the request for executing a generic AI task
|
||||
type ExecuteTaskRequest struct {
|
||||
ProviderConfig *ProviderConfig `json:"provider_config,omitempty"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Task string `json:"task"`
|
||||
Context map[string]string `json:"context"`
|
||||
AllowedTools []string `json:"allowed_tools,omitempty"`
|
||||
}
|
||||
|
||||
// ExecuteTaskResponse is the response from executing a generic AI task
|
||||
type ExecuteTaskResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Result string `json:"result"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// HealthCheckResponse is the response from a health check
|
||||
type HealthCheckResponse struct {
|
||||
Healthy bool `json:"healthy"`
|
||||
|
||||
@@ -170,6 +170,23 @@ const (
|
||||
WishlistVoteBudget ErrorCode = "WISHLIST_VOTE_BUDGET_EXCEEDED"
|
||||
)
|
||||
|
||||
// Pages errors (PAGES_)
|
||||
const (
|
||||
PagesNotConfigured ErrorCode = "PAGES_NOT_CONFIGURED"
|
||||
PagesNotEnabled ErrorCode = "PAGES_NOT_ENABLED"
|
||||
PagesInvalidTemplate ErrorCode = "PAGES_INVALID_TEMPLATE"
|
||||
)
|
||||
|
||||
// AI errors (AI_)
|
||||
const (
|
||||
AIDisabled ErrorCode = "AI_DISABLED"
|
||||
AIUnitNotEnabled ErrorCode = "AI_UNIT_NOT_ENABLED"
|
||||
AIOperationNotFound ErrorCode = "AI_OPERATION_NOT_FOUND"
|
||||
AIRateLimitExceeded ErrorCode = "AI_RATE_LIMIT_EXCEEDED"
|
||||
AIServiceError ErrorCode = "AI_SERVICE_ERROR"
|
||||
AIOperationDisabled ErrorCode = "AI_OPERATION_DISABLED"
|
||||
)
|
||||
|
||||
// errorInfo contains metadata about an error code
|
||||
type errorInfo struct {
|
||||
Message string
|
||||
@@ -299,6 +316,19 @@ var errorCatalog = map[ErrorCode]errorInfo{
|
||||
WishlistItemNotFound: {"Wishlist item not found", http.StatusNotFound},
|
||||
WishlistDisabled: {"Wishlist is disabled for this repository", http.StatusForbidden},
|
||||
WishlistVoteBudget: {"Vote budget exceeded for this repository", http.StatusConflict},
|
||||
|
||||
// Pages errors
|
||||
PagesNotConfigured: {"Landing page is not configured for this repository", http.StatusNotFound},
|
||||
PagesNotEnabled: {"Landing page is not enabled for this repository", http.StatusForbidden},
|
||||
PagesInvalidTemplate: {"Invalid landing page template name", http.StatusBadRequest},
|
||||
|
||||
// AI errors
|
||||
AIDisabled: {"AI features are disabled", http.StatusForbidden},
|
||||
AIUnitNotEnabled: {"AI unit is not enabled for this repository", http.StatusForbidden},
|
||||
AIOperationNotFound: {"AI operation not found", http.StatusNotFound},
|
||||
AIRateLimitExceeded: {"AI operation rate limit exceeded", http.StatusTooManyRequests},
|
||||
AIServiceError: {"AI service error", http.StatusBadGateway},
|
||||
AIOperationDisabled: {"This AI operation is not enabled", http.StatusForbidden},
|
||||
}
|
||||
|
||||
// Message returns the human-readable message for an error code
|
||||
|
||||
@@ -13,194 +13,360 @@ import (
|
||||
|
||||
// LandingConfig represents the parsed .gitea/landing.yaml configuration
|
||||
type LandingConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
PublicLanding bool `yaml:"public_landing"`
|
||||
Template string `yaml:"template"` // open-source-hero, minimalist-docs, saas-conversion, bold-marketing
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
PublicLanding bool `yaml:"public_landing" json:"public_landing"`
|
||||
Template string `yaml:"template" json:"template"` // open-source-hero, minimalist-docs, saas-conversion, bold-marketing
|
||||
|
||||
// Custom domain (optional)
|
||||
Domain string `yaml:"domain,omitempty"`
|
||||
Domain string `yaml:"domain,omitempty" json:"domain,omitempty"`
|
||||
|
||||
// Brand configuration
|
||||
Brand BrandConfig `yaml:"brand,omitempty"`
|
||||
Brand BrandConfig `yaml:"brand,omitempty" json:"brand,omitzero"`
|
||||
|
||||
// Hero section
|
||||
Hero HeroConfig `yaml:"hero,omitempty"`
|
||||
Hero HeroConfig `yaml:"hero,omitempty" json:"hero,omitzero"`
|
||||
|
||||
// Stats/metrics
|
||||
Stats []StatConfig `yaml:"stats,omitempty"`
|
||||
Stats []StatConfig `yaml:"stats,omitempty" json:"stats,omitempty"`
|
||||
|
||||
// Value propositions
|
||||
ValueProps []ValuePropConfig `yaml:"value_props,omitempty"`
|
||||
ValueProps []ValuePropConfig `yaml:"value_props,omitempty" json:"value_props,omitempty"`
|
||||
|
||||
// Features
|
||||
Features []FeatureConfig `yaml:"features,omitempty"`
|
||||
Features []FeatureConfig `yaml:"features,omitempty" json:"features,omitempty"`
|
||||
|
||||
// Social proof
|
||||
SocialProof SocialProofConfig `yaml:"social_proof,omitempty"`
|
||||
SocialProof SocialProofConfig `yaml:"social_proof,omitempty" json:"social_proof,omitzero"`
|
||||
|
||||
// Pricing (for saas-conversion template)
|
||||
Pricing PricingConfig `yaml:"pricing,omitempty"`
|
||||
Pricing PricingConfig `yaml:"pricing,omitempty" json:"pricing,omitzero"`
|
||||
|
||||
// CTA section
|
||||
CTASection CTASectionConfig `yaml:"cta_section,omitempty"`
|
||||
CTASection CTASectionConfig `yaml:"cta_section,omitempty" json:"cta_section,omitzero"`
|
||||
|
||||
// Blog section
|
||||
Blog BlogSectionConfig `yaml:"blog,omitempty" json:"blog,omitzero"`
|
||||
|
||||
// Gallery section
|
||||
Gallery GallerySectionConfig `yaml:"gallery,omitempty" json:"gallery,omitzero"`
|
||||
|
||||
// Comparison section
|
||||
Comparison ComparisonSectionConfig `yaml:"comparison,omitempty" json:"comparison,omitzero"`
|
||||
|
||||
// Navigation visibility
|
||||
Navigation NavigationConfig `yaml:"navigation,omitempty" json:"navigation,omitzero"`
|
||||
|
||||
// Footer
|
||||
Footer FooterConfig `yaml:"footer,omitempty"`
|
||||
Footer FooterConfig `yaml:"footer,omitempty" json:"footer,omitzero"`
|
||||
|
||||
// Theme customization
|
||||
Theme ThemeConfig `yaml:"theme,omitempty"`
|
||||
Theme ThemeConfig `yaml:"theme,omitempty" json:"theme,omitzero"`
|
||||
|
||||
// SEO & Social
|
||||
SEO SEOConfig `yaml:"seo,omitempty"`
|
||||
SEO SEOConfig `yaml:"seo,omitempty" json:"seo,omitzero"`
|
||||
|
||||
// Analytics
|
||||
Analytics AnalyticsConfig `yaml:"analytics,omitempty"`
|
||||
Analytics AnalyticsConfig `yaml:"analytics,omitempty" json:"analytics,omitzero"`
|
||||
|
||||
// Advanced settings
|
||||
Advanced AdvancedConfig `yaml:"advanced,omitempty"`
|
||||
Advanced AdvancedConfig `yaml:"advanced,omitempty" json:"advanced,omitzero"`
|
||||
|
||||
// A/B testing experiments
|
||||
Experiments ExperimentConfig `yaml:"experiments,omitempty" json:"experiments,omitzero"`
|
||||
|
||||
// Multi-language support
|
||||
I18n I18nConfig `yaml:"i18n,omitempty" json:"i18n,omitzero"`
|
||||
}
|
||||
|
||||
// BrandConfig represents brand/identity settings
|
||||
type BrandConfig struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
LogoURL string `yaml:"logo_url,omitempty"`
|
||||
Tagline string `yaml:"tagline,omitempty"`
|
||||
Name string `yaml:"name,omitempty" json:"name,omitempty"`
|
||||
LogoURL string `yaml:"logo_url,omitempty" json:"logo_url,omitempty"`
|
||||
UploadedLogo string `yaml:"uploaded_logo,omitempty" json:"uploaded_logo,omitempty"`
|
||||
LogoSource string `yaml:"logo_source,omitempty" json:"logo_source,omitempty"` // "url" (default), "repo", or "org" — selects avatar source
|
||||
Tagline string `yaml:"tagline,omitempty" json:"tagline,omitempty"`
|
||||
FaviconURL string `yaml:"favicon_url,omitempty" json:"favicon_url,omitempty"`
|
||||
UploadedFavicon string `yaml:"uploaded_favicon,omitempty" json:"uploaded_favicon,omitempty"`
|
||||
}
|
||||
|
||||
// ResolvedLogoURL returns the uploaded logo path or the external URL.
|
||||
func (b *BrandConfig) ResolvedLogoURL() string {
|
||||
if b.UploadedLogo != "" {
|
||||
return "/repo-avatars/" + b.UploadedLogo
|
||||
}
|
||||
return b.LogoURL
|
||||
}
|
||||
|
||||
// ResolvedFaviconURL returns the uploaded favicon path or the external URL.
|
||||
func (b *BrandConfig) ResolvedFaviconURL() string {
|
||||
if b.UploadedFavicon != "" {
|
||||
return "/repo-avatars/" + b.UploadedFavicon
|
||||
}
|
||||
return b.FaviconURL
|
||||
}
|
||||
|
||||
// HeroConfig represents hero section settings
|
||||
type HeroConfig struct {
|
||||
Headline string `yaml:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty"`
|
||||
PrimaryCTA CTAButton `yaml:"primary_cta,omitempty"`
|
||||
SecondaryCTA CTAButton `yaml:"secondary_cta,omitempty"`
|
||||
ImageURL string `yaml:"image_url,omitempty"`
|
||||
CodeExample string `yaml:"code_example,omitempty"`
|
||||
VideoURL string `yaml:"video_url,omitempty"`
|
||||
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
|
||||
PrimaryCTA CTAButton `yaml:"primary_cta,omitempty" json:"primary_cta,omitzero"`
|
||||
SecondaryCTA CTAButton `yaml:"secondary_cta,omitempty" json:"secondary_cta,omitzero"`
|
||||
ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"`
|
||||
UploadedImage string `yaml:"uploaded_image,omitempty" json:"uploaded_image,omitempty"` // filename in repo-avatars storage
|
||||
CodeExample string `yaml:"code_example,omitempty" json:"code_example,omitempty"`
|
||||
VideoURL string `yaml:"video_url,omitempty" json:"video_url,omitempty"`
|
||||
}
|
||||
|
||||
// ResolvedImageURL returns the effective hero image URL, preferring uploaded image over URL.
|
||||
func (h *HeroConfig) ResolvedImageURL() string {
|
||||
if h.UploadedImage != "" {
|
||||
return "/repo-avatars/" + h.UploadedImage
|
||||
}
|
||||
return h.ImageURL
|
||||
}
|
||||
|
||||
// CTAButton represents a call-to-action button
|
||||
type CTAButton struct {
|
||||
Label string `yaml:"label,omitempty"`
|
||||
URL string `yaml:"url,omitempty"`
|
||||
Variant string `yaml:"variant,omitempty"` // primary, secondary, outline, text
|
||||
Label string `yaml:"label,omitempty" json:"label,omitempty"`
|
||||
URL string `yaml:"url,omitempty" json:"url,omitempty"`
|
||||
Variant string `yaml:"variant,omitempty" json:"variant,omitempty"` // primary, secondary, outline, text
|
||||
}
|
||||
|
||||
// StatConfig represents a single stat/metric
|
||||
type StatConfig struct {
|
||||
Value string `yaml:"value,omitempty"`
|
||||
Label string `yaml:"label,omitempty"`
|
||||
Value string `yaml:"value,omitempty" json:"value,omitempty"`
|
||||
Label string `yaml:"label,omitempty" json:"label,omitempty"`
|
||||
}
|
||||
|
||||
// ValuePropConfig represents a value proposition
|
||||
type ValuePropConfig struct {
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Icon string `yaml:"icon,omitempty"`
|
||||
Title string `yaml:"title,omitempty" json:"title,omitempty"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"`
|
||||
}
|
||||
|
||||
// FeatureConfig represents a single feature item
|
||||
type FeatureConfig struct {
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Icon string `yaml:"icon,omitempty"`
|
||||
ImageURL string `yaml:"image_url,omitempty"`
|
||||
Title string `yaml:"title,omitempty" json:"title,omitempty"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"`
|
||||
ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"`
|
||||
}
|
||||
|
||||
// SocialProofConfig represents social proof section
|
||||
type SocialProofConfig struct {
|
||||
Logos []string `yaml:"logos,omitempty"`
|
||||
Testimonial TestimonialConfig `yaml:"testimonial,omitempty"`
|
||||
Testimonials []TestimonialConfig `yaml:"testimonials,omitempty"`
|
||||
Logos []string `yaml:"logos,omitempty" json:"logos,omitempty"`
|
||||
Testimonial TestimonialConfig `yaml:"testimonial,omitempty" json:"testimonial,omitzero"`
|
||||
Testimonials []TestimonialConfig `yaml:"testimonials,omitempty" json:"testimonials,omitempty"`
|
||||
}
|
||||
|
||||
// TestimonialConfig represents a testimonial
|
||||
type TestimonialConfig struct {
|
||||
Quote string `yaml:"quote,omitempty"`
|
||||
Author string `yaml:"author,omitempty"`
|
||||
Role string `yaml:"role,omitempty"`
|
||||
Avatar string `yaml:"avatar,omitempty"`
|
||||
Quote string `yaml:"quote,omitempty" json:"quote,omitempty"`
|
||||
Author string `yaml:"author,omitempty" json:"author,omitempty"`
|
||||
Role string `yaml:"role,omitempty" json:"role,omitempty"`
|
||||
Avatar string `yaml:"avatar,omitempty" json:"avatar,omitempty"`
|
||||
}
|
||||
|
||||
// PricingConfig represents pricing section
|
||||
type PricingConfig struct {
|
||||
Headline string `yaml:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty"`
|
||||
Plans []PricingPlanConfig `yaml:"plans,omitempty"`
|
||||
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
|
||||
Plans []PricingPlanConfig `yaml:"plans,omitempty" json:"plans,omitempty"`
|
||||
}
|
||||
|
||||
// PricingPlanConfig represents a pricing plan
|
||||
type PricingPlanConfig struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Price string `yaml:"price,omitempty"`
|
||||
Period string `yaml:"period,omitempty"`
|
||||
Features []string `yaml:"features,omitempty"`
|
||||
CTA string `yaml:"cta,omitempty"`
|
||||
Featured bool `yaml:"featured,omitempty"`
|
||||
Name string `yaml:"name,omitempty" json:"name,omitempty"`
|
||||
Price string `yaml:"price,omitempty" json:"price,omitempty"`
|
||||
Period string `yaml:"period,omitempty" json:"period,omitempty"`
|
||||
Features []string `yaml:"features,omitempty" json:"features,omitempty"`
|
||||
CTA string `yaml:"cta,omitempty" json:"cta,omitempty"`
|
||||
Featured bool `yaml:"featured,omitempty" json:"featured,omitempty"`
|
||||
}
|
||||
|
||||
// CTASectionConfig represents the final CTA section
|
||||
type CTASectionConfig struct {
|
||||
Headline string `yaml:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty"`
|
||||
Button CTAButton `yaml:"button,omitempty"`
|
||||
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
|
||||
Button CTAButton `yaml:"button,omitempty" json:"button,omitzero"`
|
||||
}
|
||||
|
||||
// BlogSectionConfig represents blog section settings on the landing page
|
||||
type BlogSectionConfig struct {
|
||||
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
||||
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
|
||||
MaxPosts int `yaml:"max_posts,omitempty" json:"max_posts,omitempty"` // default 3
|
||||
ShowExcerpt bool `yaml:"show_excerpt,omitempty" json:"show_excerpt,omitempty"` // show subtitle as excerpt
|
||||
CTAButton CTAButton `yaml:"cta_button,omitempty" json:"cta_button,omitzero"` // "View All Posts" link
|
||||
}
|
||||
|
||||
// GallerySectionConfig represents gallery section settings on the landing page
|
||||
type GallerySectionConfig struct {
|
||||
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
||||
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
|
||||
MaxImages int `yaml:"max_images,omitempty" json:"max_images,omitempty"` // default 6
|
||||
Columns int `yaml:"columns,omitempty" json:"columns,omitempty"` // grid columns, default 3
|
||||
}
|
||||
|
||||
// ComparisonSectionConfig represents a feature comparison matrix section
|
||||
type ComparisonSectionConfig struct {
|
||||
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
||||
Headline string `yaml:"headline,omitempty" json:"headline,omitempty"`
|
||||
Subheadline string `yaml:"subheadline,omitempty" json:"subheadline,omitempty"`
|
||||
Columns []ComparisonColumnConfig `yaml:"columns,omitempty" json:"columns,omitempty"`
|
||||
Groups []ComparisonGroupConfig `yaml:"groups,omitempty" json:"groups,omitempty"`
|
||||
}
|
||||
|
||||
// HasData returns true if the comparison section has columns and at least one feature
|
||||
func (c *ComparisonSectionConfig) HasData() bool {
|
||||
if len(c.Columns) == 0 || len(c.Groups) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, g := range c.Groups {
|
||||
if len(g.Features) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ComparisonColumnConfig represents a column header in the comparison table
|
||||
type ComparisonColumnConfig struct {
|
||||
Name string `yaml:"name,omitempty" json:"name,omitempty"`
|
||||
Highlight bool `yaml:"highlight,omitempty" json:"highlight,omitempty"`
|
||||
}
|
||||
|
||||
// ComparisonGroupConfig represents a group of features in the comparison table
|
||||
type ComparisonGroupConfig struct {
|
||||
Name string `yaml:"name,omitempty" json:"name,omitempty"`
|
||||
Features []ComparisonFeatureConfig `yaml:"features,omitempty" json:"features,omitempty"`
|
||||
}
|
||||
|
||||
// ComparisonFeatureConfig represents a single feature row in the comparison table
|
||||
type ComparisonFeatureConfig struct {
|
||||
Name string `yaml:"name,omitempty" json:"name,omitempty"`
|
||||
Values []string `yaml:"values,omitempty" json:"values,omitempty"` // "true"/"false" for check/x, anything else displayed as text
|
||||
}
|
||||
|
||||
// NavigationConfig controls which built-in navigation links appear in the header and footer
|
||||
type NavigationConfig struct {
|
||||
ShowDocs bool `yaml:"show_docs,omitempty" json:"show_docs,omitempty"`
|
||||
ShowAPI bool `yaml:"show_api,omitempty" json:"show_api,omitempty"`
|
||||
ShowRepository bool `yaml:"show_repository,omitempty" json:"show_repository,omitempty"`
|
||||
ShowReleases bool `yaml:"show_releases,omitempty" json:"show_releases,omitempty"`
|
||||
ShowIssues bool `yaml:"show_issues,omitempty" json:"show_issues,omitempty"`
|
||||
// Translatable labels for nav items and section headers (defaults to English)
|
||||
LabelValueProps string `yaml:"label_value_props,omitempty" json:"label_value_props,omitempty"`
|
||||
LabelFeatures string `yaml:"label_features,omitempty" json:"label_features,omitempty"`
|
||||
LabelPricing string `yaml:"label_pricing,omitempty" json:"label_pricing,omitempty"`
|
||||
LabelBlog string `yaml:"label_blog,omitempty" json:"label_blog,omitempty"`
|
||||
LabelGallery string `yaml:"label_gallery,omitempty" json:"label_gallery,omitempty"`
|
||||
LabelCompare string `yaml:"label_compare,omitempty" json:"label_compare,omitempty"`
|
||||
LabelDocs string `yaml:"label_docs,omitempty" json:"label_docs,omitempty"`
|
||||
LabelReleases string `yaml:"label_releases,omitempty" json:"label_releases,omitempty"`
|
||||
LabelAPI string `yaml:"label_api,omitempty" json:"label_api,omitempty"`
|
||||
LabelIssues string `yaml:"label_issues,omitempty" json:"label_issues,omitempty"`
|
||||
}
|
||||
|
||||
// FooterConfig represents footer settings
|
||||
type FooterConfig struct {
|
||||
Links []FooterLink `yaml:"links,omitempty"`
|
||||
Social []SocialLink `yaml:"social,omitempty"`
|
||||
Copyright string `yaml:"copyright,omitempty"`
|
||||
ShowPoweredBy bool `yaml:"show_powered_by,omitempty"`
|
||||
Links []FooterLink `yaml:"links,omitempty" json:"links,omitempty"`
|
||||
Social []SocialLink `yaml:"social,omitempty" json:"social,omitempty"`
|
||||
Copyright string `yaml:"copyright,omitempty" json:"copyright,omitempty"`
|
||||
ShowPoweredBy bool `yaml:"show_powered_by,omitempty" json:"show_powered_by,omitempty"`
|
||||
}
|
||||
|
||||
// FooterLink represents a single footer link
|
||||
type FooterLink struct {
|
||||
Label string `yaml:"label,omitempty"`
|
||||
URL string `yaml:"url,omitempty"`
|
||||
Label string `yaml:"label,omitempty" json:"label,omitempty"`
|
||||
URL string `yaml:"url,omitempty" json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// SocialLink represents a social media link
|
||||
type SocialLink struct {
|
||||
Platform string `yaml:"platform,omitempty"` // twitter, github, discord, linkedin, youtube
|
||||
URL string `yaml:"url,omitempty"`
|
||||
Platform string `yaml:"platform,omitempty" json:"platform,omitempty"` // bluesky, discord, facebook, github, instagram, linkedin, mastodon, reddit, rss, substack, threads, tiktok, twitch, twitter, youtube
|
||||
URL string `yaml:"url,omitempty" json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// ThemeConfig represents theme customization
|
||||
type ThemeConfig struct {
|
||||
PrimaryColor string `yaml:"primary_color,omitempty"`
|
||||
AccentColor string `yaml:"accent_color,omitempty"`
|
||||
Mode string `yaml:"mode,omitempty"` // light, dark, auto
|
||||
PrimaryColor string `yaml:"primary_color,omitempty" json:"primary_color,omitempty"`
|
||||
AccentColor string `yaml:"accent_color,omitempty" json:"accent_color,omitempty"`
|
||||
Mode string `yaml:"mode,omitempty" json:"mode,omitempty"` // light, dark, auto
|
||||
}
|
||||
|
||||
// SEOConfig represents SEO and social sharing settings
|
||||
type SEOConfig struct {
|
||||
Title string `yaml:"title,omitempty"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Keywords []string `yaml:"keywords,omitempty"`
|
||||
OGImage string `yaml:"og_image,omitempty"`
|
||||
TwitterCard string `yaml:"twitter_card,omitempty"`
|
||||
TwitterSite string `yaml:"twitter_site,omitempty"`
|
||||
Title string `yaml:"title,omitempty" json:"title,omitempty"`
|
||||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
Keywords []string `yaml:"keywords,omitempty" json:"keywords,omitempty"`
|
||||
OGImage string `yaml:"og_image,omitempty" json:"og_image,omitempty"`
|
||||
UseMediaKitOG bool `yaml:"use_media_kit_og,omitempty" json:"use_media_kit_og,omitempty"`
|
||||
TwitterCard string `yaml:"twitter_card,omitempty" json:"twitter_card,omitempty"`
|
||||
TwitterSite string `yaml:"twitter_site,omitempty" json:"twitter_site,omitempty"`
|
||||
}
|
||||
|
||||
// AnalyticsConfig represents analytics settings
|
||||
type AnalyticsConfig struct {
|
||||
Plausible string `yaml:"plausible,omitempty"`
|
||||
Umami UmamiConfig `yaml:"umami,omitempty"`
|
||||
GoogleAnalytics string `yaml:"google_analytics,omitempty"`
|
||||
Plausible string `yaml:"plausible,omitempty" json:"plausible,omitempty"`
|
||||
Umami UmamiConfig `yaml:"umami,omitempty" json:"umami,omitzero"`
|
||||
GoogleAnalytics string `yaml:"google_analytics,omitempty" json:"google_analytics,omitempty"`
|
||||
}
|
||||
|
||||
// UmamiConfig represents Umami analytics settings
|
||||
type UmamiConfig struct {
|
||||
WebsiteID string `yaml:"website_id,omitempty"`
|
||||
URL string `yaml:"url,omitempty"`
|
||||
WebsiteID string `yaml:"website_id,omitempty" json:"website_id,omitempty"`
|
||||
URL string `yaml:"url,omitempty" json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// ExperimentConfig represents A/B testing experiment settings
|
||||
type ExperimentConfig struct {
|
||||
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
||||
AutoOptimize bool `yaml:"auto_optimize,omitempty" json:"auto_optimize,omitempty"`
|
||||
MinImpressions int `yaml:"min_impressions,omitempty" json:"min_impressions,omitempty"`
|
||||
ApprovalRequired bool `yaml:"approval_required,omitempty" json:"approval_required,omitempty"`
|
||||
}
|
||||
|
||||
// I18nConfig represents multi-language settings for the landing page
|
||||
type I18nConfig struct {
|
||||
DefaultLang string `yaml:"default_lang,omitempty" json:"default_lang,omitempty"`
|
||||
Languages []string `yaml:"languages,omitempty" json:"languages,omitempty"`
|
||||
}
|
||||
|
||||
// LanguageDisplayNames returns a map of language codes to native display names
|
||||
func LanguageDisplayNames() map[string]string {
|
||||
return map[string]string{
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"de": "Deutsch",
|
||||
"fr": "Français",
|
||||
"ja": "日本語",
|
||||
"zh": "中文",
|
||||
"pt": "Português",
|
||||
"ru": "Русский",
|
||||
"ko": "한국어",
|
||||
"it": "Italiano",
|
||||
"hi": "हिन्दी",
|
||||
"ar": "العربية",
|
||||
"nl": "Nederlands",
|
||||
"pl": "Polski",
|
||||
"tr": "Türkçe",
|
||||
}
|
||||
}
|
||||
|
||||
// AdvancedConfig represents advanced settings
|
||||
type AdvancedConfig struct {
|
||||
CustomCSS string `yaml:"custom_css,omitempty"`
|
||||
CustomHead string `yaml:"custom_head,omitempty"`
|
||||
Redirects map[string]string `yaml:"redirects,omitempty"`
|
||||
PublicReleases bool `yaml:"public_releases,omitempty"`
|
||||
CustomCSS string `yaml:"custom_css,omitempty" json:"custom_css,omitempty"`
|
||||
CustomHead string `yaml:"custom_head,omitempty" json:"custom_head,omitempty"`
|
||||
Redirects map[string]string `yaml:"redirects,omitempty" json:"redirects,omitempty"`
|
||||
StaticRoutes []string `yaml:"static_routes,omitempty" json:"static_routes,omitempty"`
|
||||
PublicReleases bool `yaml:"public_releases,omitempty" json:"public_releases,omitempty"`
|
||||
HideMobileReleases bool `yaml:"hide_mobile_releases,omitempty" json:"hide_mobile_releases,omitempty"`
|
||||
GooglePlayID string `yaml:"google_play_id,omitempty" json:"google_play_id,omitempty"`
|
||||
AppStoreID string `yaml:"app_store_id,omitempty" json:"app_store_id,omitempty"`
|
||||
}
|
||||
|
||||
// ParseLandingConfig parses a landing.yaml file content
|
||||
@@ -258,6 +424,11 @@ func DefaultConfig() *LandingConfig {
|
||||
{Title: "Flexible", Description: "Adapts to your workflow, not the other way around.", Icon: "gear"},
|
||||
{Title: "Open Source", Description: "Free forever. Community driven.", Icon: "heart"},
|
||||
},
|
||||
Navigation: NavigationConfig{
|
||||
ShowDocs: true,
|
||||
ShowRepository: true,
|
||||
ShowReleases: true,
|
||||
},
|
||||
CTASection: CTASectionConfig{
|
||||
Headline: "Ready to get started?",
|
||||
Subheadline: "Join thousands of developers already using this project.",
|
||||
@@ -277,7 +448,7 @@ func DefaultConfig() *LandingConfig {
|
||||
|
||||
// ValidTemplates returns the list of valid template names
|
||||
func ValidTemplates() []string {
|
||||
return []string{"open-source-hero", "minimalist-docs", "saas-conversion", "bold-marketing"}
|
||||
return []string{"open-source-hero", "minimalist-docs", "saas-conversion", "bold-marketing", "documentation-first", "developer-tool", "visual-showcase", "cli-terminal", "architecture-deep-dive"}
|
||||
}
|
||||
|
||||
// IsValidTemplate checks if a template name is valid
|
||||
@@ -288,9 +459,76 @@ func IsValidTemplate(name string) bool {
|
||||
// TemplateDisplayNames returns a map of template names to display names
|
||||
func TemplateDisplayNames() map[string]string {
|
||||
return map[string]string{
|
||||
"open-source-hero": "Open Source Hero",
|
||||
"minimalist-docs": "Minimalist Docs",
|
||||
"saas-conversion": "SaaS Conversion",
|
||||
"bold-marketing": "Bold Marketing",
|
||||
"open-source-hero": "Open Source Product",
|
||||
"minimalist-docs": "Minimalist Product",
|
||||
"saas-conversion": "SaaS Product",
|
||||
"bold-marketing": "Bold Marketing Product",
|
||||
"documentation-first": "Documentation First",
|
||||
"developer-tool": "Developer Tool",
|
||||
"visual-showcase": "Visual Showcase",
|
||||
"cli-terminal": "CLI Terminal",
|
||||
"architecture-deep-dive": "Architecture Deep Dive",
|
||||
}
|
||||
}
|
||||
|
||||
// TemplateDefaultLabels returns the template-specific default section labels.
|
||||
// These are the creative names each template uses for its sections.
|
||||
func TemplateDefaultLabels(template string) NavigationConfig {
|
||||
switch template {
|
||||
case "architecture-deep-dive":
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Systems Analysis",
|
||||
LabelFeatures: "Technical Specifications",
|
||||
LabelPricing: "Resource Allocation",
|
||||
LabelBlog: "Dispatches",
|
||||
LabelGallery: "Visual Index",
|
||||
LabelCompare: "Compare",
|
||||
}
|
||||
case "bold-marketing":
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Why choose this",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Investment",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
}
|
||||
case "minimalist-docs":
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Why choose this",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Investment",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
}
|
||||
case "open-source-hero":
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Why choose us",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Pricing",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
}
|
||||
case "saas-conversion":
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Why",
|
||||
LabelFeatures: "Features",
|
||||
LabelPricing: "Pricing",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
}
|
||||
default:
|
||||
// developer-tool, documentation-first, visual-showcase, cli-terminal
|
||||
return NavigationConfig{
|
||||
LabelValueProps: "Why choose us",
|
||||
LabelFeatures: "Capabilities",
|
||||
LabelPricing: "Pricing",
|
||||
LabelBlog: "Blog",
|
||||
LabelGallery: "Gallery",
|
||||
LabelCompare: "Compare",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
modules/plugins/config.go
Normal file
96
modules/plugins/config.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitcaddy.com/server/v3/modules/log"
|
||||
"code.gitcaddy.com/server/v3/modules/setting"
|
||||
)
|
||||
|
||||
// ExternalPluginConfig holds configuration for a single external plugin
|
||||
type ExternalPluginConfig struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
// Managed mode: server launches the binary
|
||||
Binary string
|
||||
Args string
|
||||
// External mode: connect to already-running process
|
||||
Address string
|
||||
// Common
|
||||
SubscribedEvents []string
|
||||
HealthTimeout time.Duration
|
||||
}
|
||||
|
||||
// Config holds the global [plugins] configuration
|
||||
type Config struct {
|
||||
Enabled bool
|
||||
Path string
|
||||
HealthCheckInterval time.Duration
|
||||
ExternalPlugins map[string]*ExternalPluginConfig
|
||||
}
|
||||
|
||||
// LoadConfig loads plugin configuration from app.ini [plugins] and [plugins.*] sections
|
||||
func LoadConfig() *Config {
|
||||
cfg := &Config{
|
||||
ExternalPlugins: make(map[string]*ExternalPluginConfig),
|
||||
}
|
||||
|
||||
sec := setting.CfgProvider.Section("plugins")
|
||||
cfg.Enabled = sec.Key("ENABLED").MustBool(true)
|
||||
cfg.Path = sec.Key("PATH").MustString("data/plugins")
|
||||
cfg.HealthCheckInterval = sec.Key("HEALTH_CHECK_INTERVAL").MustDuration(30 * time.Second)
|
||||
|
||||
// Load [plugins.*] sections for external plugins
|
||||
for _, childSec := range sec.ChildSections() {
|
||||
name := strings.TrimPrefix(childSec.Name(), "plugins.")
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
pluginCfg := &ExternalPluginConfig{
|
||||
Name: name,
|
||||
Enabled: childSec.Key("ENABLED").MustBool(true),
|
||||
Binary: childSec.Key("BINARY").MustString(""),
|
||||
Args: childSec.Key("ARGS").MustString(""),
|
||||
Address: childSec.Key("ADDRESS").MustString(""),
|
||||
HealthTimeout: childSec.Key("HEALTH_TIMEOUT").MustDuration(5 * time.Second),
|
||||
}
|
||||
|
||||
// Parse subscribed events
|
||||
if eventsStr := childSec.Key("SUBSCRIBED_EVENTS").MustString(""); eventsStr != "" {
|
||||
pluginCfg.SubscribedEvents = splitAndTrim(eventsStr)
|
||||
}
|
||||
|
||||
// Validate: must have either binary or address
|
||||
if pluginCfg.Binary == "" && pluginCfg.Address == "" {
|
||||
log.Warn("Plugin %q has neither BINARY nor ADDRESS configured, skipping", name)
|
||||
continue
|
||||
}
|
||||
|
||||
cfg.ExternalPlugins[name] = pluginCfg
|
||||
log.Info("Loaded external plugin config: %s (managed=%v)", name, pluginCfg.IsManaged())
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// IsManaged returns true if the server manages the plugin's lifecycle (has a binary)
|
||||
func (c *ExternalPluginConfig) IsManaged() bool {
|
||||
return c.Binary != ""
|
||||
}
|
||||
|
||||
// splitAndTrim splits a comma-separated string and trims whitespace
|
||||
func splitAndTrim(s string) []string {
|
||||
var result []string
|
||||
for part := range strings.SplitSeq(s, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "" {
|
||||
result = append(result, part)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
359
modules/plugins/external.go
Normal file
359
modules/plugins/external.go
Normal file
@@ -0,0 +1,359 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"code.gitcaddy.com/server/v3/modules/graceful"
|
||||
"code.gitcaddy.com/server/v3/modules/log"
|
||||
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
|
||||
"code.gitcaddy.com/server/v3/modules/plugins/pluginv1/pluginv1connect"
|
||||
)
|
||||
|
||||
// PluginStatus represents the status of an external plugin
|
||||
type PluginStatus string
|
||||
|
||||
const (
|
||||
PluginStatusStarting PluginStatus = "starting"
|
||||
PluginStatusOnline PluginStatus = "online"
|
||||
PluginStatusOffline PluginStatus = "offline"
|
||||
PluginStatusError PluginStatus = "error"
|
||||
|
||||
// ProtocolVersion is the current plugin protocol version.
|
||||
// Increment this when new RPCs are added to PluginService.
|
||||
// The server uses this to avoid calling RPCs that older plugins don't implement.
|
||||
ProtocolVersion int32 = 1
|
||||
)
|
||||
|
||||
// ManagedPlugin tracks the state of an external plugin
|
||||
type ManagedPlugin struct {
|
||||
config *ExternalPluginConfig
|
||||
process *os.Process
|
||||
status PluginStatus
|
||||
lastSeen time.Time
|
||||
manifest *pluginv1.PluginManifest
|
||||
protocolVersion int32 // protocol version reported by the plugin (0 = pre-versioning, treated as 1)
|
||||
failCount int
|
||||
client pluginv1connect.PluginServiceClient
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// ExternalPluginManager manages external plugins (both managed and external mode)
|
||||
type ExternalPluginManager struct {
|
||||
mu sync.RWMutex
|
||||
plugins map[string]*ManagedPlugin
|
||||
config *Config
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
var globalExternalManager *ExternalPluginManager
|
||||
|
||||
// GetExternalManager returns the global external plugin manager
|
||||
func GetExternalManager() *ExternalPluginManager {
|
||||
return globalExternalManager
|
||||
}
|
||||
|
||||
// NewExternalPluginManager creates a new external plugin manager
|
||||
func NewExternalPluginManager(config *Config) *ExternalPluginManager {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m := &ExternalPluginManager{
|
||||
plugins: make(map[string]*ManagedPlugin),
|
||||
config: config,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
globalExternalManager = m
|
||||
return m
|
||||
}
|
||||
|
||||
// StartAll launches managed plugins and connects to external ones
|
||||
func (m *ExternalPluginManager) StartAll() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for name, cfg := range m.config.ExternalPlugins {
|
||||
if !cfg.Enabled {
|
||||
log.Info("External plugin %s is disabled, skipping", name)
|
||||
continue
|
||||
}
|
||||
|
||||
address := cfg.Address
|
||||
if address == "" {
|
||||
log.Error("External plugin %s has no address configured", name)
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
||||
address = "http://" + address
|
||||
}
|
||||
|
||||
mp := &ManagedPlugin{
|
||||
config: cfg,
|
||||
status: PluginStatusStarting,
|
||||
client: pluginv1connect.NewPluginServiceClient(
|
||||
newH2CClient(cfg.HealthTimeout),
|
||||
address,
|
||||
connect.WithGRPC(),
|
||||
),
|
||||
}
|
||||
m.plugins[name] = mp
|
||||
|
||||
if cfg.IsManaged() {
|
||||
if err := m.startManagedPlugin(mp); err != nil {
|
||||
log.Error("Failed to start managed plugin %s: %v", name, err)
|
||||
mp.status = PluginStatusError
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Try to initialize the plugin
|
||||
if err := m.initializePlugin(mp); err != nil {
|
||||
log.Error("Failed to initialize external plugin %s: %v", name, err)
|
||||
mp.status = PluginStatusError
|
||||
continue
|
||||
}
|
||||
|
||||
mp.status = PluginStatusOnline
|
||||
mp.lastSeen = time.Now()
|
||||
log.Info("External plugin %s is online (managed=%v)", name, cfg.IsManaged())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopAll gracefully shuts down all external plugins
|
||||
func (m *ExternalPluginManager) StopAll() {
|
||||
m.cancel()
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for name, mp := range m.plugins {
|
||||
log.Info("Shutting down external plugin: %s", name)
|
||||
|
||||
// Send shutdown request via Connect RPC
|
||||
m.shutdownPlugin(mp)
|
||||
|
||||
// Kill managed process
|
||||
if mp.process != nil {
|
||||
if err := mp.process.Signal(os.Interrupt); err != nil {
|
||||
log.Warn("Failed to send interrupt to plugin %s, killing: %v", name, err)
|
||||
_ = mp.process.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
mp.status = PluginStatusOffline
|
||||
}
|
||||
}
|
||||
|
||||
// GetPlugin returns an external plugin by name
|
||||
func (m *ExternalPluginManager) GetPlugin(name string) *ManagedPlugin {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.plugins[name]
|
||||
}
|
||||
|
||||
// AllPlugins returns all external plugins
|
||||
func (m *ExternalPluginManager) AllPlugins() map[string]*ManagedPlugin {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
result := make(map[string]*ManagedPlugin, len(m.plugins))
|
||||
maps.Copy(result, m.plugins)
|
||||
return result
|
||||
}
|
||||
|
||||
// OnEvent dispatches an event to all interested plugins (fire-and-forget with timeout)
|
||||
func (m *ExternalPluginManager) OnEvent(event *pluginv1.PluginEvent) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for name, mp := range m.plugins {
|
||||
mp.mu.RLock()
|
||||
if mp.status != PluginStatusOnline || mp.manifest == nil {
|
||||
mp.mu.RUnlock()
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this plugin is subscribed to this event
|
||||
subscribed := false
|
||||
for _, e := range mp.manifest.SubscribedEvents {
|
||||
if e == event.EventType || e == "*" {
|
||||
subscribed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
mp.mu.RUnlock()
|
||||
|
||||
if !subscribed {
|
||||
continue
|
||||
}
|
||||
|
||||
// Dispatch in background with timeout
|
||||
go func(pluginName string, p *ManagedPlugin) {
|
||||
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := p.client.OnEvent(ctx, connect.NewRequest(event))
|
||||
if err != nil {
|
||||
log.Error("Failed to dispatch event %s to plugin %s: %v", event.EventType, pluginName, err)
|
||||
return
|
||||
}
|
||||
if resp.Msg.Error != "" {
|
||||
log.Error("Plugin %s returned error for event %s: %s", pluginName, event.EventType, resp.Msg.Error)
|
||||
}
|
||||
}(name, mp)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleHTTP proxies an HTTP request to a plugin that declares the matching route
|
||||
func (m *ExternalPluginManager) HandleHTTP(method, path string, headers map[string]string, body []byte) (*pluginv1.HTTPResponse, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for name, mp := range m.plugins {
|
||||
mp.mu.RLock()
|
||||
if mp.status != PluginStatusOnline || mp.manifest == nil {
|
||||
mp.mu.RUnlock()
|
||||
continue
|
||||
}
|
||||
|
||||
for _, route := range mp.manifest.Routes {
|
||||
if route.Method == method && strings.HasPrefix(path, route.Path) {
|
||||
mp.mu.RUnlock()
|
||||
|
||||
ctx, cancel := context.WithTimeout(m.ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := mp.client.HandleHTTP(ctx, connect.NewRequest(&pluginv1.HTTPRequest{
|
||||
Method: method,
|
||||
Path: path,
|
||||
Headers: headers,
|
||||
Body: body,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin %s HandleHTTP failed: %w", name, err)
|
||||
}
|
||||
return resp.Msg, nil
|
||||
}
|
||||
}
|
||||
mp.mu.RUnlock()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no plugin handles %s %s", method, path)
|
||||
}
|
||||
|
||||
// Status returns the status of a plugin
|
||||
func (mp *ManagedPlugin) Status() PluginStatus {
|
||||
mp.mu.RLock()
|
||||
defer mp.mu.RUnlock()
|
||||
return mp.status
|
||||
}
|
||||
|
||||
// Manifest returns the plugin's manifest
|
||||
func (mp *ManagedPlugin) Manifest() *pluginv1.PluginManifest {
|
||||
mp.mu.RLock()
|
||||
defer mp.mu.RUnlock()
|
||||
return mp.manifest
|
||||
}
|
||||
|
||||
// SupportsProtocol returns true if the plugin supports the given protocol version.
|
||||
// Use this before calling RPCs added after protocol version 1.
|
||||
func (mp *ManagedPlugin) SupportsProtocol(version int32) bool {
|
||||
mp.mu.RLock()
|
||||
defer mp.mu.RUnlock()
|
||||
return mp.protocolVersion >= version
|
||||
}
|
||||
|
||||
// --- Internal methods ---
|
||||
|
||||
func (m *ExternalPluginManager) startManagedPlugin(mp *ManagedPlugin) error {
|
||||
args := strings.Fields(mp.config.Args)
|
||||
cmd := exec.Command(mp.config.Binary, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start binary %s: %w", mp.config.Binary, err)
|
||||
}
|
||||
|
||||
mp.process = cmd.Process
|
||||
|
||||
// Register with graceful manager for proper shutdown
|
||||
graceful.GetManager().RunAtShutdown(m.ctx, func() {
|
||||
if mp.process != nil {
|
||||
_ = mp.process.Signal(os.Interrupt)
|
||||
}
|
||||
})
|
||||
|
||||
// Wait a bit for the process to start
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ExternalPluginManager) initializePlugin(mp *ManagedPlugin) error {
|
||||
resp, err := mp.client.Initialize(m.ctx, connect.NewRequest(&pluginv1.InitializeRequest{
|
||||
ServerVersion: "3.0.0",
|
||||
Config: map[string]string{},
|
||||
ProtocolVersion: ProtocolVersion,
|
||||
}))
|
||||
if err != nil {
|
||||
return fmt.Errorf("plugin Initialize RPC failed: %w", err)
|
||||
}
|
||||
|
||||
if !resp.Msg.Success {
|
||||
return fmt.Errorf("plugin initialization failed: %s", resp.Msg.Error)
|
||||
}
|
||||
|
||||
mp.mu.Lock()
|
||||
mp.manifest = resp.Msg.Manifest
|
||||
mp.protocolVersion = resp.Msg.ProtocolVersion
|
||||
if mp.protocolVersion == 0 {
|
||||
mp.protocolVersion = 1 // pre-versioning plugins are treated as v1
|
||||
}
|
||||
mp.mu.Unlock()
|
||||
|
||||
log.Info("Plugin reports protocol version %d", mp.protocolVersion)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ExternalPluginManager) shutdownPlugin(mp *ManagedPlugin) {
|
||||
_, err := mp.client.Shutdown(m.ctx, connect.NewRequest(&pluginv1.ShutdownRequest{
|
||||
Reason: "server shutdown",
|
||||
}))
|
||||
if err != nil {
|
||||
log.Warn("Plugin shutdown call failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// newH2CClient creates an HTTP client that supports cleartext HTTP/2 (h2c)
|
||||
// for communicating with gRPC services without TLS.
|
||||
func newH2CClient(timeout time.Duration) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http2.Transport{
|
||||
AllowHTTP: true,
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, addr)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
136
modules/plugins/health.go
Normal file
136
modules/plugins/health.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
|
||||
"code.gitcaddy.com/server/v3/modules/graceful"
|
||||
"code.gitcaddy.com/server/v3/modules/log"
|
||||
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
|
||||
)
|
||||
|
||||
const (
|
||||
maxConsecutiveFailures = 3
|
||||
)
|
||||
|
||||
// StartHealthMonitoring begins periodic health checks for all external plugins.
|
||||
// It runs as a background goroutine managed by the graceful manager.
|
||||
func (m *ExternalPluginManager) StartHealthMonitoring() {
|
||||
interval := m.config.HealthCheckInterval
|
||||
if interval <= 0 {
|
||||
interval = 30 * time.Second
|
||||
}
|
||||
|
||||
graceful.GetManager().RunWithShutdownContext(func(ctx context.Context) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-m.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
m.checkAllPlugins(ctx)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *ExternalPluginManager) checkAllPlugins(ctx context.Context) {
|
||||
m.mu.RLock()
|
||||
plugins := make(map[string]*ManagedPlugin, len(m.plugins))
|
||||
maps.Copy(plugins, m.plugins)
|
||||
m.mu.RUnlock()
|
||||
|
||||
for name, mp := range plugins {
|
||||
if err := m.checkPlugin(ctx, name, mp); err != nil {
|
||||
log.Warn("Health check failed for plugin %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ExternalPluginManager) checkPlugin(ctx context.Context, name string, mp *ManagedPlugin) error {
|
||||
healthCtx, cancel := context.WithTimeout(ctx, mp.config.HealthTimeout)
|
||||
defer cancel()
|
||||
|
||||
resp, err := mp.client.HealthCheck(healthCtx, connect.NewRequest(&pluginv1.HealthCheckRequest{}))
|
||||
|
||||
mp.mu.Lock()
|
||||
defer mp.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
mp.failCount++
|
||||
if mp.failCount >= maxConsecutiveFailures {
|
||||
if mp.status != PluginStatusOffline {
|
||||
log.Error("Plugin %s is now offline after %d consecutive health check failures", name, mp.failCount)
|
||||
mp.status = PluginStatusOffline
|
||||
}
|
||||
|
||||
// Auto-restart managed plugins
|
||||
if mp.config.IsManaged() && mp.process != nil {
|
||||
log.Info("Attempting to restart managed plugin %s", name)
|
||||
go m.restartManagedPlugin(name, mp)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Health check succeeded
|
||||
if mp.status != PluginStatusOnline {
|
||||
log.Info("Plugin %s is back online", name)
|
||||
}
|
||||
mp.failCount = 0
|
||||
mp.status = PluginStatusOnline
|
||||
mp.lastSeen = time.Now()
|
||||
|
||||
if !resp.Msg.Healthy {
|
||||
log.Warn("Plugin %s reports unhealthy: %s", name, resp.Msg.Status)
|
||||
mp.status = PluginStatusError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ExternalPluginManager) restartManagedPlugin(name string, mp *ManagedPlugin) {
|
||||
// Kill the old process first
|
||||
if mp.process != nil {
|
||||
_ = mp.process.Kill()
|
||||
mp.process = nil
|
||||
}
|
||||
|
||||
mp.mu.Lock()
|
||||
mp.status = PluginStatusStarting
|
||||
mp.mu.Unlock()
|
||||
|
||||
if err := m.startManagedPlugin(mp); err != nil {
|
||||
log.Error("Failed to restart managed plugin %s: %v", name, err)
|
||||
mp.mu.Lock()
|
||||
mp.status = PluginStatusError
|
||||
mp.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.initializePlugin(mp); err != nil {
|
||||
log.Error("Failed to re-initialize managed plugin %s: %v", name, err)
|
||||
mp.mu.Lock()
|
||||
mp.status = PluginStatusError
|
||||
mp.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
mp.mu.Lock()
|
||||
mp.status = PluginStatusOnline
|
||||
mp.lastSeen = time.Now()
|
||||
mp.failCount = 0
|
||||
mp.mu.Unlock()
|
||||
|
||||
log.Info("Managed plugin %s restarted successfully", name)
|
||||
}
|
||||
1242
modules/plugins/pluginv1/plugin.pb.go
generated
Normal file
1242
modules/plugins/pluginv1/plugin.pb.go
generated
Normal file
File diff suppressed because it is too large
Load Diff
106
modules/plugins/pluginv1/plugin.proto
Normal file
106
modules/plugins/pluginv1/plugin.proto
Normal file
@@ -0,0 +1,106 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package plugin.v1;
|
||||
|
||||
option go_package = "code.gitcaddy.com/server/v3/modules/plugins/pluginv1;pluginv1";
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
// PluginService is the RPC interface that external plugins must implement.
|
||||
// The server calls these methods to manage the plugin's lifecycle and dispatch events.
|
||||
service PluginService {
|
||||
// Initialize is called when the server starts or the plugin is loaded
|
||||
rpc Initialize(InitializeRequest) returns (InitializeResponse);
|
||||
// Shutdown is called when the server is shutting down
|
||||
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
|
||||
// HealthCheck checks if the plugin is healthy
|
||||
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
// GetManifest returns the plugin's manifest describing its capabilities
|
||||
rpc GetManifest(GetManifestRequest) returns (PluginManifest);
|
||||
// OnEvent is called when an event the plugin is subscribed to occurs
|
||||
rpc OnEvent(PluginEvent) returns (EventResponse);
|
||||
// HandleHTTP proxies an HTTP request to the plugin
|
||||
rpc HandleHTTP(HTTPRequest) returns (HTTPResponse);
|
||||
}
|
||||
|
||||
message InitializeRequest {
|
||||
string server_version = 1;
|
||||
map<string, string> config = 2;
|
||||
// protocol_version is the plugin protocol version the server supports.
|
||||
// The current version is 1. Plugins should check this to know what RPCs
|
||||
// the server may call. A value of 0 means the server predates versioning.
|
||||
int32 protocol_version = 3;
|
||||
}
|
||||
|
||||
message InitializeResponse {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
PluginManifest manifest = 3;
|
||||
// protocol_version is the plugin protocol version the plugin supports.
|
||||
// The current version is 1. The server uses this to avoid calling RPCs
|
||||
// that the plugin doesn't implement. A value of 0 means the plugin
|
||||
// predates versioning and is treated as protocol version 1.
|
||||
int32 protocol_version = 4;
|
||||
}
|
||||
|
||||
message ShutdownRequest {
|
||||
string reason = 1;
|
||||
}
|
||||
|
||||
message ShutdownResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message HealthCheckRequest {}
|
||||
|
||||
message HealthCheckResponse {
|
||||
bool healthy = 1;
|
||||
string status = 2;
|
||||
map<string, string> details = 3;
|
||||
}
|
||||
|
||||
message GetManifestRequest {}
|
||||
|
||||
message PluginManifest {
|
||||
string name = 1;
|
||||
string version = 2;
|
||||
string description = 3;
|
||||
repeated string subscribed_events = 4;
|
||||
repeated PluginRoute routes = 5;
|
||||
repeated string required_permissions = 6;
|
||||
string license_tier = 7;
|
||||
}
|
||||
|
||||
message PluginRoute {
|
||||
string method = 1;
|
||||
string path = 2;
|
||||
string description = 3;
|
||||
}
|
||||
|
||||
message PluginEvent {
|
||||
string event_type = 1;
|
||||
google.protobuf.Struct payload = 2;
|
||||
google.protobuf.Timestamp timestamp = 3;
|
||||
int64 repo_id = 4;
|
||||
int64 org_id = 5;
|
||||
}
|
||||
|
||||
message EventResponse {
|
||||
bool handled = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
message HTTPRequest {
|
||||
string method = 1;
|
||||
string path = 2;
|
||||
map<string, string> headers = 3;
|
||||
bytes body = 4;
|
||||
map<string, string> query_params = 5;
|
||||
}
|
||||
|
||||
message HTTPResponse {
|
||||
int32 status_code = 1;
|
||||
map<string, string> headers = 2;
|
||||
bytes body = 3;
|
||||
}
|
||||
264
modules/plugins/pluginv1/pluginv1connect/plugin.connect.go
Normal file
264
modules/plugins/pluginv1/pluginv1connect/plugin.connect.go
Normal file
@@ -0,0 +1,264 @@
|
||||
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
|
||||
//
|
||||
// Source: modules/plugins/pluginv1/plugin.proto
|
||||
|
||||
package pluginv1connect
|
||||
|
||||
import (
|
||||
pluginv1 "code.gitcaddy.com/server/v3/modules/plugins/pluginv1"
|
||||
connect "connectrpc.com/connect"
|
||||
context "context"
|
||||
errors "errors"
|
||||
http "net/http"
|
||||
strings "strings"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file and the connect package are
|
||||
// compatible. If you get a compiler error that this constant is not defined, this code was
|
||||
// generated with a version of connect newer than the one compiled into your binary. You can fix the
|
||||
// problem by either regenerating this code with an older version of connect or updating the connect
|
||||
// version compiled into your binary.
|
||||
const _ = connect.IsAtLeastVersion1_13_0
|
||||
|
||||
const (
|
||||
// PluginServiceName is the fully-qualified name of the PluginService service.
|
||||
PluginServiceName = "plugin.v1.PluginService"
|
||||
)
|
||||
|
||||
// These constants are the fully-qualified names of the RPCs defined in this package. They're
|
||||
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
|
||||
//
|
||||
// Note that these are different from the fully-qualified method names used by
|
||||
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
|
||||
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
|
||||
// period.
|
||||
const (
|
||||
// PluginServiceInitializeProcedure is the fully-qualified name of the PluginService's Initialize
|
||||
// RPC.
|
||||
PluginServiceInitializeProcedure = "/plugin.v1.PluginService/Initialize"
|
||||
// PluginServiceShutdownProcedure is the fully-qualified name of the PluginService's Shutdown RPC.
|
||||
PluginServiceShutdownProcedure = "/plugin.v1.PluginService/Shutdown"
|
||||
// PluginServiceHealthCheckProcedure is the fully-qualified name of the PluginService's HealthCheck
|
||||
// RPC.
|
||||
PluginServiceHealthCheckProcedure = "/plugin.v1.PluginService/HealthCheck"
|
||||
// PluginServiceGetManifestProcedure is the fully-qualified name of the PluginService's GetManifest
|
||||
// RPC.
|
||||
PluginServiceGetManifestProcedure = "/plugin.v1.PluginService/GetManifest"
|
||||
// PluginServiceOnEventProcedure is the fully-qualified name of the PluginService's OnEvent RPC.
|
||||
PluginServiceOnEventProcedure = "/plugin.v1.PluginService/OnEvent"
|
||||
// PluginServiceHandleHTTPProcedure is the fully-qualified name of the PluginService's HandleHTTP
|
||||
// RPC.
|
||||
PluginServiceHandleHTTPProcedure = "/plugin.v1.PluginService/HandleHTTP"
|
||||
)
|
||||
|
||||
// PluginServiceClient is a client for the plugin.v1.PluginService service.
|
||||
type PluginServiceClient interface {
|
||||
// Initialize is called when the server starts or the plugin is loaded
|
||||
Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error)
|
||||
// Shutdown is called when the server is shutting down
|
||||
Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error)
|
||||
// HealthCheck checks if the plugin is healthy
|
||||
HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error)
|
||||
// GetManifest returns the plugin's manifest describing its capabilities
|
||||
GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error)
|
||||
// OnEvent is called when an event the plugin is subscribed to occurs
|
||||
OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error)
|
||||
// HandleHTTP proxies an HTTP request to the plugin
|
||||
HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error)
|
||||
}
|
||||
|
||||
// NewPluginServiceClient constructs a client for the plugin.v1.PluginService service. By default,
|
||||
// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and
|
||||
// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC()
|
||||
// or connect.WithGRPCWeb() options.
|
||||
//
|
||||
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
|
||||
// http://api.acme.com or https://acme.com/grpc).
|
||||
func NewPluginServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) PluginServiceClient {
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
pluginServiceMethods := pluginv1.File_modules_plugins_pluginv1_plugin_proto.Services().ByName("PluginService").Methods()
|
||||
return &pluginServiceClient{
|
||||
initialize: connect.NewClient[pluginv1.InitializeRequest, pluginv1.InitializeResponse](
|
||||
httpClient,
|
||||
baseURL+PluginServiceInitializeProcedure,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("Initialize")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
shutdown: connect.NewClient[pluginv1.ShutdownRequest, pluginv1.ShutdownResponse](
|
||||
httpClient,
|
||||
baseURL+PluginServiceShutdownProcedure,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("Shutdown")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
healthCheck: connect.NewClient[pluginv1.HealthCheckRequest, pluginv1.HealthCheckResponse](
|
||||
httpClient,
|
||||
baseURL+PluginServiceHealthCheckProcedure,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("HealthCheck")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
getManifest: connect.NewClient[pluginv1.GetManifestRequest, pluginv1.PluginManifest](
|
||||
httpClient,
|
||||
baseURL+PluginServiceGetManifestProcedure,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("GetManifest")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
onEvent: connect.NewClient[pluginv1.PluginEvent, pluginv1.EventResponse](
|
||||
httpClient,
|
||||
baseURL+PluginServiceOnEventProcedure,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("OnEvent")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
handleHTTP: connect.NewClient[pluginv1.HTTPRequest, pluginv1.HTTPResponse](
|
||||
httpClient,
|
||||
baseURL+PluginServiceHandleHTTPProcedure,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("HandleHTTP")),
|
||||
connect.WithClientOptions(opts...),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// pluginServiceClient implements PluginServiceClient.
|
||||
type pluginServiceClient struct {
|
||||
initialize *connect.Client[pluginv1.InitializeRequest, pluginv1.InitializeResponse]
|
||||
shutdown *connect.Client[pluginv1.ShutdownRequest, pluginv1.ShutdownResponse]
|
||||
healthCheck *connect.Client[pluginv1.HealthCheckRequest, pluginv1.HealthCheckResponse]
|
||||
getManifest *connect.Client[pluginv1.GetManifestRequest, pluginv1.PluginManifest]
|
||||
onEvent *connect.Client[pluginv1.PluginEvent, pluginv1.EventResponse]
|
||||
handleHTTP *connect.Client[pluginv1.HTTPRequest, pluginv1.HTTPResponse]
|
||||
}
|
||||
|
||||
// Initialize calls plugin.v1.PluginService.Initialize.
|
||||
func (c *pluginServiceClient) Initialize(ctx context.Context, req *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error) {
|
||||
return c.initialize.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// Shutdown calls plugin.v1.PluginService.Shutdown.
|
||||
func (c *pluginServiceClient) Shutdown(ctx context.Context, req *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error) {
|
||||
return c.shutdown.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// HealthCheck calls plugin.v1.PluginService.HealthCheck.
|
||||
func (c *pluginServiceClient) HealthCheck(ctx context.Context, req *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error) {
|
||||
return c.healthCheck.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// GetManifest calls plugin.v1.PluginService.GetManifest.
|
||||
func (c *pluginServiceClient) GetManifest(ctx context.Context, req *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error) {
|
||||
return c.getManifest.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// OnEvent calls plugin.v1.PluginService.OnEvent.
|
||||
func (c *pluginServiceClient) OnEvent(ctx context.Context, req *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error) {
|
||||
return c.onEvent.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// HandleHTTP calls plugin.v1.PluginService.HandleHTTP.
|
||||
func (c *pluginServiceClient) HandleHTTP(ctx context.Context, req *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error) {
|
||||
return c.handleHTTP.CallUnary(ctx, req)
|
||||
}
|
||||
|
||||
// PluginServiceHandler is an implementation of the plugin.v1.PluginService service.
|
||||
type PluginServiceHandler interface {
|
||||
// Initialize is called when the server starts or the plugin is loaded
|
||||
Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error)
|
||||
// Shutdown is called when the server is shutting down
|
||||
Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error)
|
||||
// HealthCheck checks if the plugin is healthy
|
||||
HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error)
|
||||
// GetManifest returns the plugin's manifest describing its capabilities
|
||||
GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error)
|
||||
// OnEvent is called when an event the plugin is subscribed to occurs
|
||||
OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error)
|
||||
// HandleHTTP proxies an HTTP request to the plugin
|
||||
HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error)
|
||||
}
|
||||
|
||||
// NewPluginServiceHandler builds an HTTP handler from the service implementation. It returns the
|
||||
// path on which to mount the handler and the handler itself.
|
||||
//
|
||||
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
|
||||
// and JSON codecs. They also support gzip compression.
|
||||
func NewPluginServiceHandler(svc PluginServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
|
||||
pluginServiceMethods := pluginv1.File_modules_plugins_pluginv1_plugin_proto.Services().ByName("PluginService").Methods()
|
||||
pluginServiceInitializeHandler := connect.NewUnaryHandler(
|
||||
PluginServiceInitializeProcedure,
|
||||
svc.Initialize,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("Initialize")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
pluginServiceShutdownHandler := connect.NewUnaryHandler(
|
||||
PluginServiceShutdownProcedure,
|
||||
svc.Shutdown,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("Shutdown")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
pluginServiceHealthCheckHandler := connect.NewUnaryHandler(
|
||||
PluginServiceHealthCheckProcedure,
|
||||
svc.HealthCheck,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("HealthCheck")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
pluginServiceGetManifestHandler := connect.NewUnaryHandler(
|
||||
PluginServiceGetManifestProcedure,
|
||||
svc.GetManifest,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("GetManifest")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
pluginServiceOnEventHandler := connect.NewUnaryHandler(
|
||||
PluginServiceOnEventProcedure,
|
||||
svc.OnEvent,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("OnEvent")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
pluginServiceHandleHTTPHandler := connect.NewUnaryHandler(
|
||||
PluginServiceHandleHTTPProcedure,
|
||||
svc.HandleHTTP,
|
||||
connect.WithSchema(pluginServiceMethods.ByName("HandleHTTP")),
|
||||
connect.WithHandlerOptions(opts...),
|
||||
)
|
||||
return "/plugin.v1.PluginService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case PluginServiceInitializeProcedure:
|
||||
pluginServiceInitializeHandler.ServeHTTP(w, r)
|
||||
case PluginServiceShutdownProcedure:
|
||||
pluginServiceShutdownHandler.ServeHTTP(w, r)
|
||||
case PluginServiceHealthCheckProcedure:
|
||||
pluginServiceHealthCheckHandler.ServeHTTP(w, r)
|
||||
case PluginServiceGetManifestProcedure:
|
||||
pluginServiceGetManifestHandler.ServeHTTP(w, r)
|
||||
case PluginServiceOnEventProcedure:
|
||||
pluginServiceOnEventHandler.ServeHTTP(w, r)
|
||||
case PluginServiceHandleHTTPProcedure:
|
||||
pluginServiceHandleHTTPHandler.ServeHTTP(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// UnimplementedPluginServiceHandler returns CodeUnimplemented from all methods.
|
||||
type UnimplementedPluginServiceHandler struct{}
|
||||
|
||||
func (UnimplementedPluginServiceHandler) Initialize(context.Context, *connect.Request[pluginv1.InitializeRequest]) (*connect.Response[pluginv1.InitializeResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.Initialize is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedPluginServiceHandler) Shutdown(context.Context, *connect.Request[pluginv1.ShutdownRequest]) (*connect.Response[pluginv1.ShutdownResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.Shutdown is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedPluginServiceHandler) HealthCheck(context.Context, *connect.Request[pluginv1.HealthCheckRequest]) (*connect.Response[pluginv1.HealthCheckResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.HealthCheck is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedPluginServiceHandler) GetManifest(context.Context, *connect.Request[pluginv1.GetManifestRequest]) (*connect.Response[pluginv1.PluginManifest], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.GetManifest is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedPluginServiceHandler) OnEvent(context.Context, *connect.Request[pluginv1.PluginEvent]) (*connect.Response[pluginv1.EventResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.OnEvent is not implemented"))
|
||||
}
|
||||
|
||||
func (UnimplementedPluginServiceHandler) HandleHTTP(context.Context, *connect.Request[pluginv1.HTTPRequest]) (*connect.Response[pluginv1.HTTPResponse], error) {
|
||||
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("plugin.v1.PluginService.HandleHTTP is not implemented"))
|
||||
}
|
||||
@@ -6,6 +6,8 @@ package secretscan
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
|
||||
"code.gitcaddy.com/server/v3/modules/git"
|
||||
"code.gitcaddy.com/server/v3/modules/git/gitcmd"
|
||||
"code.gitcaddy.com/server/v3/modules/json"
|
||||
"code.gitcaddy.com/server/v3/modules/log"
|
||||
"code.gitcaddy.com/server/v3/modules/setting"
|
||||
)
|
||||
@@ -46,6 +49,60 @@ type Scanner struct {
|
||||
ignoredFiles []string
|
||||
}
|
||||
|
||||
// IgnoreEntry represents an entry in a .gitsecrets-ignore file
|
||||
type IgnoreEntry struct {
|
||||
ContentHash string `json:"contentHash"`
|
||||
PatternID string `json:"patternId"`
|
||||
FilePath string `json:"filePath"`
|
||||
Reason string `json:"reason"`
|
||||
Confidence float64 `json:"confidence,omitempty"`
|
||||
AddedAt int64 `json:"addedAt"`
|
||||
}
|
||||
|
||||
// contentHash computes the same hash the GitSecrets addon uses: SHA-256 truncated to 16 hex chars
|
||||
func contentHash(text string) string {
|
||||
h := sha256.Sum256([]byte(text))
|
||||
return hex.EncodeToString(h[:])[:16]
|
||||
}
|
||||
|
||||
// parseIgnoreFile parses a .gitsecrets-ignore file and returns a map keyed by contentHash
|
||||
func parseIgnoreFile(data string) map[string]IgnoreEntry {
|
||||
entries := make(map[string]IgnoreEntry)
|
||||
for line := range strings.SplitSeq(data, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
var entry IgnoreEntry
|
||||
if err := json.Unmarshal([]byte(line), &entry); err != nil {
|
||||
continue
|
||||
}
|
||||
if entry.ContentHash != "" {
|
||||
entries[entry.ContentHash] = entry
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
// filterIgnored removes detected secrets that match entries in the .gitsecrets-ignore file.
|
||||
// Matching is done on contentHash + patternId.
|
||||
func filterIgnored(secrets []DetectedSecret, ignoreEntries map[string]IgnoreEntry) []DetectedSecret {
|
||||
if len(ignoreEntries) == 0 {
|
||||
return secrets
|
||||
}
|
||||
|
||||
var filtered []DetectedSecret
|
||||
for _, s := range secrets {
|
||||
hash := contentHash(s.MatchedText)
|
||||
if entry, ok := ignoreEntries[hash]; ok && entry.PatternID == s.PatternID {
|
||||
log.Debug("Secret scan: Skipping ignored secret (hash=%s, pattern=%s, reason=%s)", hash, s.PatternID, entry.Reason)
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, s)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// NewScanner creates a new secret scanner
|
||||
func NewScanner() *Scanner {
|
||||
return &Scanner{
|
||||
@@ -292,6 +349,21 @@ func (s *Scanner) ScanCommitRange(ctx context.Context, repo *git.Repository, old
|
||||
result.Secrets = append(result.Secrets, secrets...)
|
||||
}
|
||||
|
||||
// Load .gitsecrets-ignore from the commit being pushed
|
||||
commit, err := repo.GetCommit(newCommitID)
|
||||
if err == nil {
|
||||
if ignoreContent, err := commit.GetFileContent(".gitsecrets-ignore", 256*1024); err == nil {
|
||||
ignoreEntries := parseIgnoreFile(ignoreContent)
|
||||
if len(ignoreEntries) > 0 {
|
||||
before := len(result.Secrets)
|
||||
result.Secrets = filterIgnored(result.Secrets, ignoreEntries)
|
||||
if skipped := before - len(result.Secrets); skipped > 0 {
|
||||
log.Info("Secret scan: Skipped %d secret(s) via .gitsecrets-ignore in %s", skipped, repo.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.ScanDuration = time.Since(startTime)
|
||||
|
||||
// Determine if push should be blocked
|
||||
|
||||
@@ -11,31 +11,62 @@ import (
|
||||
|
||||
// AI settings for the GitCaddy AI service integration
|
||||
var AI = struct {
|
||||
Enabled bool
|
||||
ServiceURL string
|
||||
ServiceToken string
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
Enabled bool
|
||||
ServiceURL string
|
||||
ServiceToken string
|
||||
Timeout time.Duration
|
||||
MaxRetries int
|
||||
|
||||
// Provider/model defaults (fallback when org doesn't configure)
|
||||
DefaultProvider string
|
||||
DefaultModel string
|
||||
|
||||
// System API keys (used when org/repo doesn't provide their own)
|
||||
ClaudeAPIKey string
|
||||
OpenAIAPIKey string
|
||||
GeminiAPIKey string
|
||||
|
||||
// Rate limiting
|
||||
MaxOperationsPerHour int
|
||||
MaxTokensPerOperation int
|
||||
|
||||
// Feature gates (admin controls what's available)
|
||||
EnableCodeReview bool
|
||||
EnableIssueTriage bool
|
||||
EnableDocGen bool
|
||||
EnableExplainCode bool
|
||||
EnableChat bool
|
||||
MaxFileSizeKB int64
|
||||
MaxDiffLines int
|
||||
AllowAutoRespond bool
|
||||
AllowAutoReview bool
|
||||
AllowAgentMode bool
|
||||
|
||||
// Content limits
|
||||
MaxFileSizeKB int64
|
||||
MaxDiffLines int
|
||||
|
||||
// Bot user
|
||||
BotUserName string
|
||||
}{
|
||||
Enabled: false,
|
||||
ServiceURL: "localhost:50051",
|
||||
ServiceToken: "",
|
||||
Timeout: 30 * time.Second,
|
||||
MaxRetries: 3,
|
||||
EnableCodeReview: true,
|
||||
EnableIssueTriage: true,
|
||||
EnableDocGen: true,
|
||||
EnableExplainCode: true,
|
||||
EnableChat: true,
|
||||
MaxFileSizeKB: 500,
|
||||
MaxDiffLines: 5000,
|
||||
Enabled: false,
|
||||
ServiceURL: "localhost:50051",
|
||||
ServiceToken: "",
|
||||
Timeout: 30 * time.Second,
|
||||
MaxRetries: 3,
|
||||
DefaultProvider: "claude",
|
||||
DefaultModel: "claude-sonnet-4-20250514",
|
||||
MaxOperationsPerHour: 100,
|
||||
MaxTokensPerOperation: 8192,
|
||||
EnableCodeReview: true,
|
||||
EnableIssueTriage: true,
|
||||
EnableDocGen: true,
|
||||
EnableExplainCode: true,
|
||||
EnableChat: true,
|
||||
AllowAutoRespond: true,
|
||||
AllowAutoReview: true,
|
||||
AllowAgentMode: false,
|
||||
MaxFileSizeKB: 500,
|
||||
MaxDiffLines: 5000,
|
||||
BotUserName: "gitcaddy-ai",
|
||||
}
|
||||
|
||||
func loadAIFrom(rootCfg ConfigProvider) {
|
||||
@@ -45,14 +76,46 @@ func loadAIFrom(rootCfg ConfigProvider) {
|
||||
AI.ServiceToken = sec.Key("SERVICE_TOKEN").MustString("")
|
||||
AI.Timeout = sec.Key("TIMEOUT").MustDuration(30 * time.Second)
|
||||
AI.MaxRetries = sec.Key("MAX_RETRIES").MustInt(3)
|
||||
|
||||
// Provider/model
|
||||
AI.DefaultProvider = sec.Key("DEFAULT_PROVIDER").MustString("claude")
|
||||
AI.DefaultModel = sec.Key("DEFAULT_MODEL").MustString("claude-sonnet-4-20250514")
|
||||
|
||||
// Validate provider
|
||||
switch AI.DefaultProvider {
|
||||
case "claude", "openai", "gemini":
|
||||
// valid
|
||||
default:
|
||||
log.Error("[ai] DEFAULT_PROVIDER %q is not supported, falling back to claude", AI.DefaultProvider)
|
||||
AI.DefaultProvider = "claude"
|
||||
}
|
||||
|
||||
// System API keys
|
||||
AI.ClaudeAPIKey = sec.Key("CLAUDE_API_KEY").MustString("")
|
||||
AI.OpenAIAPIKey = sec.Key("OPENAI_API_KEY").MustString("")
|
||||
AI.GeminiAPIKey = sec.Key("GEMINI_API_KEY").MustString("")
|
||||
|
||||
// Rate limiting
|
||||
AI.MaxOperationsPerHour = sec.Key("MAX_OPERATIONS_PER_HOUR").MustInt(100)
|
||||
AI.MaxTokensPerOperation = sec.Key("MAX_TOKENS_PER_OPERATION").MustInt(8192)
|
||||
|
||||
// Feature gates
|
||||
AI.EnableCodeReview = sec.Key("ENABLE_CODE_REVIEW").MustBool(true)
|
||||
AI.EnableIssueTriage = sec.Key("ENABLE_ISSUE_TRIAGE").MustBool(true)
|
||||
AI.EnableDocGen = sec.Key("ENABLE_DOC_GEN").MustBool(true)
|
||||
AI.EnableExplainCode = sec.Key("ENABLE_EXPLAIN_CODE").MustBool(true)
|
||||
AI.EnableChat = sec.Key("ENABLE_CHAT").MustBool(true)
|
||||
AI.AllowAutoRespond = sec.Key("ALLOW_AUTO_RESPOND").MustBool(true)
|
||||
AI.AllowAutoReview = sec.Key("ALLOW_AUTO_REVIEW").MustBool(true)
|
||||
AI.AllowAgentMode = sec.Key("ALLOW_AGENT_MODE").MustBool(false)
|
||||
|
||||
// Content limits
|
||||
AI.MaxFileSizeKB = sec.Key("MAX_FILE_SIZE_KB").MustInt64(500)
|
||||
AI.MaxDiffLines = sec.Key("MAX_DIFF_LINES").MustInt(5000)
|
||||
|
||||
// Bot user
|
||||
AI.BotUserName = sec.Key("BOT_USER_NAME").MustString("gitcaddy-ai")
|
||||
|
||||
if AI.Enabled && AI.ServiceURL == "" {
|
||||
log.Error("AI is enabled but SERVICE_URL is not configured")
|
||||
AI.Enabled = false
|
||||
|
||||
@@ -69,6 +69,9 @@ type ThemeStruct struct {
|
||||
ExploreOrgDisplayFormat *config.Value[string]
|
||||
EnableBlogs *config.Value[bool]
|
||||
BlogsInTopNav *config.Value[bool]
|
||||
ShowFooterPoweredBy *config.Value[bool]
|
||||
ShowFooterLicenses *config.Value[bool]
|
||||
ShowFooterAPI *config.Value[bool]
|
||||
}
|
||||
|
||||
type ConfigStruct struct {
|
||||
@@ -109,6 +112,9 @@ func initDefaultConfig() {
|
||||
ExploreOrgDisplayFormat: config.ValueJSON[string]("theme.explore_org_display_format").WithDefault("list"),
|
||||
EnableBlogs: config.ValueJSON[bool]("theme.enable_blogs").WithDefault(false),
|
||||
BlogsInTopNav: config.ValueJSON[bool]("theme.blogs_in_top_nav").WithDefault(false),
|
||||
ShowFooterPoweredBy: config.ValueJSON[bool]("theme.show_footer_powered_by").WithFileConfig(config.CfgSecKey{Sec: "other", Key: "SHOW_FOOTER_POWERED_BY"}).WithDefault(true),
|
||||
ShowFooterLicenses: config.ValueJSON[bool]("theme.show_footer_licenses").WithDefault(true),
|
||||
ShowFooterAPI: config.ValueJSON[bool]("theme.show_footer_api").WithDefault(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ type CardData struct {
|
||||
LanguageColor string // hex color like "#3572A5"
|
||||
RepoAvatarURL string
|
||||
RepoFullName string
|
||||
BrandName string // replaces the GitCaddy logo when set (from repo Owner Display Name setting)
|
||||
SolidColor string // hex color for "solid" style
|
||||
BgImage image.Image // pre-loaded background image for "image" style
|
||||
UnsplashAuthor string // attribution text for Unsplash images
|
||||
@@ -264,12 +265,8 @@ func (r *Renderer) renderPresetCard(data CardData, theme Theme) ([]byte, error)
|
||||
}
|
||||
drawText(img, xCursor, metaY, data.RepoFullName, faces.meta, theme.SubtextColor)
|
||||
|
||||
// GitCaddy logo (bottom right)
|
||||
logoBounds := r.logo.Bounds()
|
||||
logoX := CardWidth - rightPad - logoBounds.Dx()
|
||||
logoY := CardHeight - 40 - logoBounds.Dy()
|
||||
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
|
||||
r.logo, logoBounds.Min, draw.Over)
|
||||
// Brand name or GitCaddy logo (bottom right)
|
||||
r.drawBrandOrLogo(img, data.BrandName, faces.meta, theme.SubtextColor, CardWidth-rightPad, CardHeight-40)
|
||||
|
||||
return encodePNG(img)
|
||||
}
|
||||
@@ -353,12 +350,8 @@ func (r *Renderer) renderSolidCard(data CardData) ([]byte, error) {
|
||||
repoW := measureText(data.RepoFullName, faces.meta)
|
||||
drawText(img, (w-repoW)/2, metaY, data.RepoFullName, faces.meta, theme.SubtextColor)
|
||||
|
||||
// GitCaddy logo (bottom right)
|
||||
logoBounds := r.logo.Bounds()
|
||||
logoX := w - pad - logoBounds.Dx()
|
||||
logoY := h - 40 - logoBounds.Dy()
|
||||
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
|
||||
r.logo, logoBounds.Min, draw.Over)
|
||||
// Brand name or GitCaddy logo (bottom right)
|
||||
r.drawBrandOrLogo(img, data.BrandName, faces.meta, theme.SubtextColor, w-pad, h-40)
|
||||
|
||||
// Unsplash attribution (lower-left of image area)
|
||||
if data.UnsplashAuthor != "" {
|
||||
@@ -458,16 +451,28 @@ func (r *Renderer) renderImageCard(data CardData) ([]byte, error) {
|
||||
yBottom -= 60
|
||||
}
|
||||
|
||||
// GitCaddy logo (bottom right)
|
||||
logoBounds := r.logo.Bounds()
|
||||
logoX := w - pad - logoBounds.Dx()
|
||||
logoY := h - pad + 10 - logoBounds.Dy()
|
||||
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
|
||||
r.logo, logoBounds.Min, draw.Over)
|
||||
// Brand name or GitCaddy logo (bottom right)
|
||||
subtextC := color.RGBA{R: 170, G: 170, B: 170, A: 255}
|
||||
r.drawBrandOrLogo(img, data.BrandName, faces.meta, subtextC, w-pad, h-pad+10)
|
||||
|
||||
return encodePNG(img)
|
||||
}
|
||||
|
||||
// drawBrandOrLogo draws either the custom brand name text or the default GitCaddy logo
|
||||
// at the bottom-right of the card. rightX is the right edge, baselineY is the text baseline.
|
||||
func (r *Renderer) drawBrandOrLogo(img *image.RGBA, brandName string, face font.Face, textColor color.RGBA, rightX, baselineY int) {
|
||||
if brandName != "" {
|
||||
textW := measureText(brandName, face)
|
||||
drawText(img, rightX-textW, baselineY, brandName, face, textColor)
|
||||
} else {
|
||||
logoBounds := r.logo.Bounds()
|
||||
logoX := rightX - logoBounds.Dx()
|
||||
logoY := baselineY - logoBounds.Dy()
|
||||
draw.Draw(img, image.Rect(logoX, logoY, logoX+logoBounds.Dx(), logoY+logoBounds.Dy()),
|
||||
r.logo, logoBounds.Min, draw.Over)
|
||||
}
|
||||
}
|
||||
|
||||
// fontFaces holds pre-created font faces for a single render.
|
||||
type fontFaces struct {
|
||||
title font.Face
|
||||
|
||||
@@ -205,3 +205,45 @@ type ActionRunnersResponse struct {
|
||||
Entries []*ActionRunner `json:"runners"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
// ActionWorkflowStatus represents the latest run status for a single workflow
|
||||
type ActionWorkflowStatus struct {
|
||||
WorkflowID string `json:"workflow_id"`
|
||||
WorkflowName string `json:"workflow_name"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion,omitempty"`
|
||||
RunID int64 `json:"run_id"`
|
||||
RunNumber int64 `json:"run_number"`
|
||||
Event string `json:"event"`
|
||||
HeadBranch string `json:"head_branch,omitempty"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
// swagger:strfmt date-time
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
// swagger:strfmt date-time
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
}
|
||||
|
||||
// ActionWorkflowStatusResponse returns the latest run status per workflow
|
||||
type ActionWorkflowStatusResponse struct {
|
||||
Workflows []*ActionWorkflowStatus `json:"workflows"`
|
||||
}
|
||||
|
||||
// ActionJobFailureDetail represents a failed job with its log excerpt
|
||||
type ActionJobFailureDetail struct {
|
||||
JobID int64 `json:"job_id"`
|
||||
JobName string `json:"job_name"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion,omitempty"`
|
||||
FailedSteps []string `json:"failed_steps"`
|
||||
Log string `json:"log"`
|
||||
}
|
||||
|
||||
// ActionRunFailureLog returns a structured failure summary for a workflow run
|
||||
type ActionRunFailureLog struct {
|
||||
RunID int64 `json:"run_id"`
|
||||
Status string `json:"status"`
|
||||
Conclusion string `json:"conclusion,omitempty"`
|
||||
WorkflowID string `json:"workflow_id"`
|
||||
WorkflowYAML string `json:"workflow_yaml,omitempty"`
|
||||
FailedJobs []*ActionJobFailureDetail `json:"failed_jobs"`
|
||||
}
|
||||
|
||||
123
modules/structs/repo_ai.go
Normal file
123
modules/structs/repo_ai.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
import "time"
|
||||
|
||||
// AISettingsV2 represents the AI settings for a repository
|
||||
type AISettingsV2 struct {
|
||||
// Tier 1: Light AI operations
|
||||
AutoRespondToIssues bool `json:"auto_respond_issues"`
|
||||
AutoReviewPRs bool `json:"auto_review_prs"`
|
||||
AutoInspectWorkflows bool `json:"auto_inspect_workflows"`
|
||||
AutoTriageIssues bool `json:"auto_triage_issues"`
|
||||
|
||||
// Tier 2: Advanced agent operations
|
||||
AgentModeEnabled bool `json:"agent_mode_enabled"`
|
||||
AgentTriggerLabels []string `json:"agent_trigger_labels"`
|
||||
AgentMaxRunMinutes int `json:"agent_max_run_minutes"`
|
||||
|
||||
// Escalation
|
||||
EscalateToStaff bool `json:"escalate_to_staff"`
|
||||
EscalationLabel string `json:"escalation_label"`
|
||||
EscalationAssignTeam string `json:"escalation_assign_team"`
|
||||
|
||||
// Provider overrides (empty = inherit from org → system)
|
||||
PreferredProvider string `json:"preferred_provider"`
|
||||
PreferredModel string `json:"preferred_model"`
|
||||
|
||||
// Custom instructions
|
||||
SystemInstructions string `json:"system_instructions"`
|
||||
ReviewInstructions string `json:"review_instructions"`
|
||||
IssueInstructions string `json:"issue_instructions"`
|
||||
|
||||
// Resolved values (read-only, computed from cascade)
|
||||
ResolvedProvider string `json:"resolved_provider,omitempty"`
|
||||
ResolvedModel string `json:"resolved_model,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateAISettingsOption represents the options for updating AI settings
|
||||
type UpdateAISettingsOption struct {
|
||||
AutoRespondToIssues *bool `json:"auto_respond_issues"`
|
||||
AutoReviewPRs *bool `json:"auto_review_prs"`
|
||||
AutoInspectWorkflows *bool `json:"auto_inspect_workflows"`
|
||||
AutoTriageIssues *bool `json:"auto_triage_issues"`
|
||||
AgentModeEnabled *bool `json:"agent_mode_enabled"`
|
||||
AgentTriggerLabels []string `json:"agent_trigger_labels"`
|
||||
AgentMaxRunMinutes *int `json:"agent_max_run_minutes"`
|
||||
EscalateToStaff *bool `json:"escalate_to_staff"`
|
||||
EscalationLabel *string `json:"escalation_label"`
|
||||
EscalationAssignTeam *string `json:"escalation_assign_team"`
|
||||
PreferredProvider *string `json:"preferred_provider"`
|
||||
PreferredModel *string `json:"preferred_model"`
|
||||
SystemInstructions *string `json:"system_instructions"`
|
||||
ReviewInstructions *string `json:"review_instructions"`
|
||||
IssueInstructions *string `json:"issue_instructions"`
|
||||
}
|
||||
|
||||
// OrgAISettingsV2 represents the AI settings for an organization
|
||||
type OrgAISettingsV2 struct {
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
HasAPIKey bool `json:"has_api_key"` // never expose actual key
|
||||
MaxOpsPerHour int `json:"max_ops_per_hour"`
|
||||
AllowedOps string `json:"allowed_ops"`
|
||||
AgentModeAllowed bool `json:"agent_mode_allowed"`
|
||||
}
|
||||
|
||||
// UpdateOrgAISettingsOption represents the options for updating org AI settings
|
||||
type UpdateOrgAISettingsOption struct {
|
||||
Provider *string `json:"provider"`
|
||||
Model *string `json:"model"`
|
||||
APIKey *string `json:"api_key"`
|
||||
MaxOpsPerHour *int `json:"max_ops_per_hour"`
|
||||
AllowedOps *string `json:"allowed_ops"`
|
||||
AgentModeAllowed *bool `json:"agent_mode_allowed"`
|
||||
}
|
||||
|
||||
// AIOperationV2 represents an AI operation log entry
|
||||
type AIOperationV2 struct {
|
||||
ID int64 `json:"id"`
|
||||
RepoID int64 `json:"repo_id"`
|
||||
Operation string `json:"operation"`
|
||||
Tier int `json:"tier"`
|
||||
TriggerEvent string `json:"trigger_event"`
|
||||
TriggerUserID int64 `json:"trigger_user_id"`
|
||||
TargetID int64 `json:"target_id"`
|
||||
TargetType string `json:"target_type"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
Status string `json:"status"`
|
||||
ResultCommentID int64 `json:"result_comment_id,omitempty"`
|
||||
ActionRunID int64 `json:"action_run_id,omitempty"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
DurationMs int64 `json:"duration_ms"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// AIOperationListV2 represents a paginated list of AI operations
|
||||
type AIOperationListV2 struct {
|
||||
Operations []*AIOperationV2 `json:"operations"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
}
|
||||
|
||||
// AIExplainRequest represents a request to explain code
|
||||
type AIExplainRequest struct {
|
||||
FilePath string `json:"file_path" binding:"Required"`
|
||||
StartLine int `json:"start_line"`
|
||||
EndLine int `json:"end_line"`
|
||||
Question string `json:"question"`
|
||||
}
|
||||
|
||||
// AIServiceStatusV2 represents the AI service health status
|
||||
type AIServiceStatusV2 struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Healthy bool `json:"healthy"`
|
||||
ServiceURL string `json:"service_url"`
|
||||
Version string `json:"version,omitempty"`
|
||||
ProviderStatus map[string]string `json:"provider_status,omitempty"`
|
||||
TotalOpsToday int64 `json:"total_ops_today"`
|
||||
}
|
||||
@@ -47,3 +47,231 @@ type PagesInfo struct {
|
||||
Config *PagesConfig `json:"config"`
|
||||
Domains []*PagesDomain `json:"domains,omitempty"`
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// v2 Landing Page Configuration API structs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// UpdatePagesConfigOption represents the full landing page config update.
|
||||
// For PATCH, only non-nil fields are applied. For PUT, all fields replace existing config.
|
||||
type UpdatePagesConfigOption struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
PublicLanding *bool `json:"public_landing"`
|
||||
Template *string `json:"template"`
|
||||
Brand *UpdatePagesBrandOption `json:"brand"`
|
||||
Hero *UpdatePagesHeroOption `json:"hero"`
|
||||
Stats *[]PagesStatOption `json:"stats"`
|
||||
ValueProps *[]PagesValuePropOption `json:"value_props"`
|
||||
Features *[]PagesFeatureOption `json:"features"`
|
||||
SocialProof *UpdatePagesSocialOption `json:"social_proof"`
|
||||
Pricing *UpdatePagesPricingOption `json:"pricing"`
|
||||
CTASection *UpdatePagesCTAOption `json:"cta_section"`
|
||||
Blog *UpdatePagesBlogOption `json:"blog"`
|
||||
Gallery *UpdatePagesGalleryOption `json:"gallery"`
|
||||
Comparison *UpdatePagesComparisonOption `json:"comparison"`
|
||||
Navigation *UpdatePagesNavOption `json:"navigation"`
|
||||
Footer *UpdatePagesFooterOption `json:"footer"`
|
||||
Theme *UpdatePagesThemeOption `json:"theme"`
|
||||
SEO *UpdatePagesSEOOption `json:"seo"`
|
||||
Advanced *UpdatePagesAdvancedOption `json:"advanced"`
|
||||
}
|
||||
|
||||
// UpdatePagesBrandOption represents brand section update
|
||||
type UpdatePagesBrandOption struct {
|
||||
Name *string `json:"name"`
|
||||
LogoURL *string `json:"logo_url"`
|
||||
Tagline *string `json:"tagline"`
|
||||
FaviconURL *string `json:"favicon_url"`
|
||||
}
|
||||
|
||||
// UpdatePagesHeroOption represents hero section update
|
||||
type UpdatePagesHeroOption struct {
|
||||
Headline *string `json:"headline"`
|
||||
Subheadline *string `json:"subheadline"`
|
||||
ImageURL *string `json:"image_url"`
|
||||
VideoURL *string `json:"video_url"`
|
||||
CodeExample *string `json:"code_example"`
|
||||
PrimaryCTA *PagesCTAButtonOption `json:"primary_cta"`
|
||||
SecondaryCTA *PagesCTAButtonOption `json:"secondary_cta"`
|
||||
}
|
||||
|
||||
// PagesCTAButtonOption represents a CTA button
|
||||
type PagesCTAButtonOption struct {
|
||||
Label *string `json:"label"`
|
||||
URL *string `json:"url"`
|
||||
Variant *string `json:"variant"`
|
||||
}
|
||||
|
||||
// PagesStatOption represents a stat item
|
||||
type PagesStatOption struct {
|
||||
Value string `json:"value"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// PagesValuePropOption represents a value proposition item
|
||||
type PagesValuePropOption struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
// PagesFeatureOption represents a feature item
|
||||
type PagesFeatureOption struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
ImageURL string `json:"image_url"`
|
||||
}
|
||||
|
||||
// UpdatePagesSocialOption represents social proof section update
|
||||
type UpdatePagesSocialOption struct {
|
||||
Logos *[]string `json:"logos"`
|
||||
Testimonials *[]PagesTestimonialOption `json:"testimonials"`
|
||||
}
|
||||
|
||||
// PagesTestimonialOption represents a testimonial item
|
||||
type PagesTestimonialOption struct {
|
||||
Quote string `json:"quote"`
|
||||
Author string `json:"author"`
|
||||
Role string `json:"role"`
|
||||
Avatar string `json:"avatar"`
|
||||
}
|
||||
|
||||
// UpdatePagesPricingOption represents pricing section update
|
||||
type UpdatePagesPricingOption struct {
|
||||
Headline *string `json:"headline"`
|
||||
Subheadline *string `json:"subheadline"`
|
||||
Plans *[]PagesPricingPlanOption `json:"plans"`
|
||||
}
|
||||
|
||||
// PagesPricingPlanOption represents a pricing plan item
|
||||
type PagesPricingPlanOption struct {
|
||||
Name string `json:"name"`
|
||||
Price string `json:"price"`
|
||||
Period string `json:"period"`
|
||||
Features []string `json:"features"`
|
||||
CTA string `json:"cta"`
|
||||
Featured bool `json:"featured"`
|
||||
}
|
||||
|
||||
// UpdatePagesCTAOption represents CTA section update
|
||||
type UpdatePagesCTAOption struct {
|
||||
Headline *string `json:"headline"`
|
||||
Subheadline *string `json:"subheadline"`
|
||||
Button *PagesCTAButtonOption `json:"button"`
|
||||
}
|
||||
|
||||
// UpdatePagesBlogOption represents blog section update
|
||||
type UpdatePagesBlogOption struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
Headline *string `json:"headline"`
|
||||
Subheadline *string `json:"subheadline"`
|
||||
MaxPosts *int `json:"max_posts"`
|
||||
}
|
||||
|
||||
// UpdatePagesGalleryOption represents gallery section update
|
||||
type UpdatePagesGalleryOption struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
Headline *string `json:"headline"`
|
||||
Subheadline *string `json:"subheadline"`
|
||||
MaxImages *int `json:"max_images"`
|
||||
Columns *int `json:"columns"`
|
||||
}
|
||||
|
||||
// UpdatePagesComparisonOption represents comparison section update
|
||||
type UpdatePagesComparisonOption struct {
|
||||
Enabled *bool `json:"enabled"`
|
||||
Headline *string `json:"headline"`
|
||||
Subheadline *string `json:"subheadline"`
|
||||
Columns *[]PagesComparisonColumnOption `json:"columns"`
|
||||
Groups *[]PagesComparisonGroupOption `json:"groups"`
|
||||
}
|
||||
|
||||
// PagesComparisonColumnOption represents a comparison column
|
||||
type PagesComparisonColumnOption struct {
|
||||
Name string `json:"name"`
|
||||
Highlight bool `json:"highlight"`
|
||||
}
|
||||
|
||||
// PagesComparisonGroupOption represents a comparison group
|
||||
type PagesComparisonGroupOption struct {
|
||||
Name string `json:"name"`
|
||||
Features []PagesComparisonFeatureOption `json:"features"`
|
||||
}
|
||||
|
||||
// PagesComparisonFeatureOption represents a comparison feature row
|
||||
type PagesComparisonFeatureOption struct {
|
||||
Name string `json:"name"`
|
||||
Values []string `json:"values"`
|
||||
}
|
||||
|
||||
// UpdatePagesNavOption represents navigation section update
|
||||
type UpdatePagesNavOption struct {
|
||||
ShowDocs *bool `json:"show_docs"`
|
||||
ShowAPI *bool `json:"show_api"`
|
||||
ShowRepository *bool `json:"show_repository"`
|
||||
ShowReleases *bool `json:"show_releases"`
|
||||
ShowIssues *bool `json:"show_issues"`
|
||||
}
|
||||
|
||||
// UpdatePagesFooterOption represents footer section update
|
||||
type UpdatePagesFooterOption struct {
|
||||
Copyright *string `json:"copyright"`
|
||||
ShowPoweredBy *bool `json:"show_powered_by"`
|
||||
Links *[]PagesFooterLinkOption `json:"links"`
|
||||
Social *[]PagesSocialLinkOption `json:"social"`
|
||||
CTASection *UpdatePagesCTAOption `json:"cta_section"`
|
||||
}
|
||||
|
||||
// PagesFooterLinkOption represents a footer link
|
||||
type PagesFooterLinkOption struct {
|
||||
Label string `json:"label"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// PagesSocialLinkOption represents a social link
|
||||
type PagesSocialLinkOption struct {
|
||||
Platform string `json:"platform"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// UpdatePagesThemeOption represents theme section update
|
||||
type UpdatePagesThemeOption struct {
|
||||
PrimaryColor *string `json:"primary_color"`
|
||||
AccentColor *string `json:"accent_color"`
|
||||
Mode *string `json:"mode"`
|
||||
}
|
||||
|
||||
// UpdatePagesSEOOption represents SEO section update
|
||||
type UpdatePagesSEOOption struct {
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Keywords *[]string `json:"keywords"`
|
||||
OGImage *string `json:"og_image"`
|
||||
UseMediaKitOG *bool `json:"use_media_kit_og"`
|
||||
TwitterCard *string `json:"twitter_card"`
|
||||
TwitterSite *string `json:"twitter_site"`
|
||||
}
|
||||
|
||||
// UpdatePagesAdvancedOption represents advanced settings update
|
||||
type UpdatePagesAdvancedOption struct {
|
||||
CustomCSS *string `json:"custom_css"`
|
||||
CustomHead *string `json:"custom_head"`
|
||||
StaticRoutes *[]string `json:"static_routes"`
|
||||
PublicReleases *bool `json:"public_releases"`
|
||||
HideMobileReleases *bool `json:"hide_mobile_releases"`
|
||||
GooglePlayID *string `json:"google_play_id"`
|
||||
AppStoreID *string `json:"app_store_id"`
|
||||
}
|
||||
|
||||
// UpdatePagesContentOption bundles content-page sections for PUT /config/content
|
||||
type UpdatePagesContentOption struct {
|
||||
Blog *UpdatePagesBlogOption `json:"blog"`
|
||||
Gallery *UpdatePagesGalleryOption `json:"gallery"`
|
||||
ComparisonEnabled *bool `json:"comparison_enabled"`
|
||||
Stats *[]PagesStatOption `json:"stats"`
|
||||
ValueProps *[]PagesValuePropOption `json:"value_props"`
|
||||
Features *[]PagesFeatureOption `json:"features"`
|
||||
Navigation *UpdatePagesNavOption `json:"navigation"`
|
||||
Advanced *UpdatePagesAdvancedOption `json:"advanced"`
|
||||
}
|
||||
|
||||
@@ -127,9 +127,6 @@ func NewFuncMap() template.FuncMap {
|
||||
"ShowFooterTemplateLoadTime": func() bool {
|
||||
return setting.Other.ShowFooterTemplateLoadTime
|
||||
},
|
||||
"ShowFooterPoweredBy": func() bool {
|
||||
return setting.Other.ShowFooterPoweredBy
|
||||
},
|
||||
"AllowedReactions": func() []string {
|
||||
return setting.UI.Reactions
|
||||
},
|
||||
|
||||
@@ -413,6 +413,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -660,6 +661,9 @@
|
||||
"repo.settings.display_title": "Display Title",
|
||||
"repo.settings.display_title_placeholder": "Optional display title for this repository",
|
||||
"repo.settings.display_title_help": "A custom title shown prominently on the repository page. Leave empty to use the repository name.",
|
||||
"repo.settings.owner_display_name": "Owner Name",
|
||||
"repo.settings.owner_display_name_placeholder": "e.g., John Smith, Acme Corp",
|
||||
"repo.settings.owner_display_name_help": "A display name for the repository owner. Used in licenses, social cards, and other public-facing contexts instead of the username.",
|
||||
"repo.settings.license": "License",
|
||||
"repo.settings.license_type": "License Type",
|
||||
"repo.settings.license_none": "No license selected",
|
||||
@@ -688,11 +692,41 @@
|
||||
"repo.settings.pages.saved": "Settings saved successfully",
|
||||
"repo.settings.pages.brand_name": "Brand Name",
|
||||
"repo.settings.pages.brand_name_help": "The name displayed on your landing page",
|
||||
"repo.settings.pages.brand_logo": "Logo",
|
||||
"repo.settings.pages.brand_logo_url": "Logo URL",
|
||||
"repo.settings.pages.brand_logo_url_help": "URL to your logo image (SVG or PNG)",
|
||||
"repo.settings.pages.brand_upload_logo": "Upload Logo",
|
||||
"repo.settings.pages.brand_delete_logo": "Delete Logo",
|
||||
"repo.settings.pages.brand_logo_uploaded": "Logo uploaded successfully.",
|
||||
"repo.settings.pages.brand_logo_deleted": "Logo deleted.",
|
||||
"repo.settings.pages.brand_upload_btn": "Upload",
|
||||
"repo.settings.pages.brand_upload_help": "Upload a JPG, PNG, WebP, or GIF image (max 5 MB). Uploaded images take priority over the URL field.",
|
||||
"repo.settings.pages.brand_upload_error": "Failed to upload image.",
|
||||
"repo.settings.pages.brand_image_too_large": "Image is too large. Maximum size is 5 MB.",
|
||||
"repo.settings.pages.brand_not_an_image": "The uploaded file is not a valid image.",
|
||||
"repo.settings.pages.brand_or": "or use a URL",
|
||||
"repo.settings.pages.brand_favicon": "Favicon",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL to a custom favicon for your landing page (ICO, PNG, or SVG). Leave blank to use the default.",
|
||||
"repo.settings.pages.brand_upload_favicon": "Upload Favicon",
|
||||
"repo.settings.pages.brand_delete_favicon": "Delete Favicon",
|
||||
"repo.settings.pages.brand_favicon_uploaded": "Favicon uploaded successfully.",
|
||||
"repo.settings.pages.brand_favicon_deleted": "Favicon deleted.",
|
||||
"repo.settings.pages.brand_favicon_upload_help": "Upload an ICO, PNG, WebP, or GIF favicon (max 5 MB). Uploaded favicons take priority over the URL field.",
|
||||
"repo.settings.pages.brand_tagline": "Tagline",
|
||||
"repo.settings.pages.headline": "Headline",
|
||||
"repo.settings.pages.subheadline": "Subheadline",
|
||||
"repo.settings.pages.hero_image": "Hero Image",
|
||||
"repo.settings.pages.hero_upload": "Upload Image",
|
||||
"repo.settings.pages.hero_upload_btn": "Upload",
|
||||
"repo.settings.pages.hero_upload_help": "Upload a JPG, PNG, WebP, or GIF image (max 5 MB). Uploaded images take priority over the URL field.",
|
||||
"repo.settings.pages.hero_upload_error": "Failed to upload image.",
|
||||
"repo.settings.pages.hero_image_too_large": "Image is too large. Maximum size is 5 MB.",
|
||||
"repo.settings.pages.hero_not_an_image": "The uploaded file is not a valid image.",
|
||||
"repo.settings.pages.hero_image_uploaded": "Hero image uploaded successfully.",
|
||||
"repo.settings.pages.hero_image_deleted": "Hero image deleted.",
|
||||
"repo.settings.pages.hero_delete_image": "Delete Image",
|
||||
"repo.settings.pages.hero_or": "or use a URL",
|
||||
"repo.settings.pages.image_url": "Hero Image URL",
|
||||
"repo.settings.pages.video_url": "Demo Video URL",
|
||||
"repo.settings.pages.code_example": "Code Example",
|
||||
@@ -703,6 +737,23 @@
|
||||
"repo.settings.pages.stats": "Stats",
|
||||
"repo.settings.pages.value_props": "Value Propositions",
|
||||
"repo.settings.pages.features": "Features",
|
||||
"repo.settings.pages.gallery_section": "Gallery Section",
|
||||
"repo.settings.pages.gallery_enabled_desc": "Show a gallery of images from your repository's .gallery folder on the landing page",
|
||||
"repo.settings.pages.gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.gallery_max_images": "Maximum Images to Show",
|
||||
"repo.settings.pages.gallery_columns": "Grid Columns",
|
||||
"repo.settings.pages.gallery_help_link": "Upload and manage gallery images in Settings > Gallery.",
|
||||
"repo.settings.pages.comparison_section": "Comparison Section",
|
||||
"repo.settings.pages.comparison_enabled_desc": "Show a feature comparison table on the landing page",
|
||||
"repo.settings.pages.comparison_headline": "Headline",
|
||||
"repo.settings.pages.comparison_subheadline": "Subheadline",
|
||||
"repo.settings.pages.comparison_columns": "Comparison Columns",
|
||||
"repo.settings.pages.comparison_columns_help": "Define three columns for your comparison table (e.g., your product vs. two competitors, or three tiers).",
|
||||
"repo.settings.pages.comparison_highlight": "Highlight",
|
||||
"repo.settings.pages.comparison_features": "Features & Groups",
|
||||
"repo.settings.pages.comparison_features_help": "Organize features into optional groups. For each cell, enter \"true\" for a checkmark, \"false\" for an X, or any text.",
|
||||
"repo.settings.pages.comparison_group_name": "Group Name",
|
||||
"repo.settings.pages.company_logos": "Company Logos",
|
||||
"repo.settings.pages.testimonials": "Testimonials",
|
||||
"repo.settings.pages.pricing_headline": "Pricing Headline",
|
||||
@@ -724,7 +775,11 @@
|
||||
"repo.settings.pages.seo_title": "SEO Title",
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.seo_keywords.placeholder": "Add a keyword...",
|
||||
"repo.settings.pages.seo_keywords.add": "Add",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.use_media_kit_og": "Use Media Kit social card",
|
||||
"repo.settings.pages.use_media_kit_og_help": "Use the social card from your Media Kit settings as the Open Graph image instead of a custom URL.",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3758,6 +3758,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -3956,6 +3957,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "Zobrazit Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Zobrazit zpr\u00e1vu \"Powered by GitCaddy Server\" v z\u00e1pat\u00ed",
|
||||
"admin.config.show_footer_licenses": "Zobrazit odkaz na licence",
|
||||
"admin.config.show_footer_licenses_desc": "Zobrazit odkaz na licence v z\u00e1pat\u00ed",
|
||||
"admin.config.show_footer_api": "Zobrazit odkaz na API",
|
||||
"admin.config.show_footer_api_desc": "Zobrazit odkaz na API (Swagger) v z\u00e1pat\u00ed",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -4004,6 +4011,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Ve\u0159ejn\u00e1 vyd\u00e1n\u00ed",
|
||||
"repo.settings.pages.public_releases_desc": "Umo\u017enit neautentifikovan\u00fdm u\u017eivatel\u016fm stahovat vyd\u00e1n\u00ed. U\u017eite\u010dn\u00e9 pro vstupn\u00ed str\u00e1nky soukrom\u00fdch repozit\u00e1\u0159\u016f.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL favikony",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL vlastn\u00ed favikony pro va\u0161i c\u00edlovou str\u00e1nku (ICO, PNG nebo SVG). Ponechte pr\u00e1zdn\u00e9 pro pou\u017eit\u00ed v\u00fdchoz\u00ed.",
|
||||
"repo.settings.pages.navigation": "Naviga\u010dn\u00ed odkazy",
|
||||
"repo.settings.pages.navigation_desc": "Ovl\u00e1dejte, kter\u00e9 vestav\u011bn\u00e9 odkazy se zobraz\u00ed v navigaci z\u00e1hlav\u00ed a z\u00e1pat\u00ed.",
|
||||
"repo.settings.pages.nav_show_docs": "Zobrazit odkaz na Docs (odkaz na wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Zobrazit odkaz na API (odkaz na Swagger dokumentaci)",
|
||||
"repo.settings.pages.nav_show_repository": "Zobrazit odkaz na repozit\u00e1\u0159 (tla\u010d\u00edtko Zobrazit zdrojov\u00fd k\u00f3d)",
|
||||
"repo.settings.pages.nav_show_releases": "Zobrazit odkaz na vyd\u00e1n\u00ed",
|
||||
"repo.settings.pages.nav_show_issues": "Zobrazit odkaz na probl\u00e9my",
|
||||
"repo.settings.pages.blog_section": "Sekce blogu",
|
||||
"repo.settings.pages.blog_enabled_desc": "Zobrazit ned\u00e1vn\u00e9 p\u0159\u00edsp\u011bvky blogu na c\u00edlov\u00e9 str\u00e1nce",
|
||||
"repo.settings.pages.blog_headline": "Nadpis blogu",
|
||||
"repo.settings.pages.blog_subheadline": "Podnadpis blogu",
|
||||
"repo.settings.pages.blog_max_posts": "Maxim\u00e1ln\u00ed po\u010det p\u0159\u00edsp\u011bvk\u016f k zobrazen\u00ed",
|
||||
"repo.settings.pages.ai_generate": "AI gener\u00e1tor obsahu",
|
||||
"repo.settings.pages.ai_generate_desc": "Automaticky generujte obsah c\u00edlov\u00e9 str\u00e1nky (nadpis, funkce, statistiky, CTA) z README a metadat va\u0161eho repozit\u00e1\u0159e pomoc\u00ed AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Generovat obsah pomoc\u00ed AI",
|
||||
"repo.settings.pages.ai_generate_success": "Obsah c\u00edlov\u00e9 str\u00e1nky byl \u00fasp\u011b\u0161n\u011b vygenerov\u00e1n. Zkontrolujte a p\u0159izp\u016fsobte ho v ostatn\u00edch kart\u00e1ch.",
|
||||
"repo.settings.pages.ai_generate_failed": "Generov\u00e1n\u00ed obsahu AI selhalo. Zkuste to pozd\u011bji znovu nebo nakonfigurujte obsah ru\u010dn\u011b.",
|
||||
"repo.settings.pages.languages": "Jazyky",
|
||||
"repo.settings.pages.default_lang": "V\u00fdchoz\u00ed jazyk",
|
||||
"repo.settings.pages.default_lang_help": "Hlavn\u00ed jazyk obsahu va\u0161\u00ed c\u00edlov\u00e9 str\u00e1nky",
|
||||
"repo.settings.pages.enabled_languages": "Povolen\u00e9 jazyky",
|
||||
"repo.settings.pages.enabled_languages_help": "Vyberte, kter\u00e9 jazyky m\u00e1 va\u0161e c\u00edlov\u00e1 str\u00e1nka podporovat. N\u00e1v\u0161t\u011bvn\u00edci uvid\u00ed v navigaci p\u0159ep\u00edna\u010d jazyk\u016f.",
|
||||
"repo.settings.pages.save_languages": "Ulo\u017eit jazykov\u00e9 nastaven\u00ed",
|
||||
"repo.settings.pages.languages_saved": "Jazykov\u00e9 nastaven\u00ed \u00fasp\u011b\u0161n\u011b ulo\u017eeno.",
|
||||
"repo.settings.pages.translations": "P\u0159eklady",
|
||||
"repo.settings.pages.ai_translate": "AI p\u0159eklad",
|
||||
"repo.settings.pages.ai_translate_success": "P\u0159eklad byl \u00fasp\u011b\u0161n\u011b vygenerov\u00e1n AI. Zkontrolujte a upravte dle pot\u0159eby.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Smazat",
|
||||
"repo.settings.pages.save_translation": "Ulo\u017eit p\u0159eklad",
|
||||
"repo.settings.pages.translation_saved": "P\u0159eklad \u00fasp\u011b\u0161n\u011b ulo\u017een.",
|
||||
"repo.settings.pages.translation_deleted": "P\u0159eklad smaz\u00e1n.",
|
||||
"repo.settings.pages.translation_empty": "Nebyl poskytnut \u017e\u00e1dn\u00fd obsah p\u0159ekladu.",
|
||||
"repo.settings.pages.trans_headline": "Nadpis",
|
||||
"repo.settings.pages.trans_subheadline": "Podnadpis",
|
||||
"repo.settings.pages.trans_primary_cta": "\u0160t\u00edtek hlavn\u00edho CTA",
|
||||
"repo.settings.pages.trans_secondary_cta": "\u0160t\u00edtek vedlej\u0161\u00edho CTA",
|
||||
"repo.settings.pages.trans_cta_headline": "Nadpis sekce CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Podnadpis sekce CTA",
|
||||
"repo.settings.pages.trans_cta_button": "\u0160t\u00edtek tla\u010d\u00edtka CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3924,6 +3924,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Pakete bereinigen",
|
||||
"admin.dashboard.cleanup_actions": "Aktionen bereinigen",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Abgelaufene Uploads bereinigen",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Löschen alter Aktionen gestartet",
|
||||
"admin.dashboard.gc_lfs": "LFS-Garbage-Collection",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -4162,6 +4163,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Anzeigeformat für Organisationen",
|
||||
"admin.config.explore_org_format_list": "Liste",
|
||||
"admin.config.explore_org_format_tiles": "Kacheln",
|
||||
"admin.config.show_footer_powered_by": "Powered By anzeigen",
|
||||
"admin.config.show_footer_powered_by_desc": "Die Nachricht \"Powered by GitCaddy Server\" in der Fu\u00dfzeile anzeigen",
|
||||
"admin.config.show_footer_licenses": "Lizenz-Link anzeigen",
|
||||
"admin.config.show_footer_licenses_desc": "Den Lizenz-Link in der Fu\u00dfzeile anzeigen",
|
||||
"admin.config.show_footer_api": "API-Link anzeigen",
|
||||
"admin.config.show_footer_api_desc": "Den API (Swagger)-Link in der Fu\u00dfzeile anzeigen",
|
||||
"repo.settings.pages.general": "Allgemein",
|
||||
"repo.settings.pages.brand": "Marke",
|
||||
"repo.settings.pages.hero": "Hero-Bereich",
|
||||
@@ -4210,6 +4217,89 @@
|
||||
"repo.settings.pages.seo_description": "SEO-Beschreibung",
|
||||
"repo.settings.pages.seo_keywords": "SEO-Schlüsselwörter",
|
||||
"repo.settings.pages.og_image": "Open-Graph-Bild",
|
||||
"repo.settings.pages.public_releases": "\u00d6ffentliche Releases",
|
||||
"repo.settings.pages.public_releases_desc": "Nicht authentifizierten Benutzern das Herunterladen von Releases erlauben. N\u00fctzlich f\u00fcr Landingpages privater Repositories.",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon-URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL zu einem benutzerdefinierten Favicon f\u00fcr Ihre Landingpage (ICO, PNG oder SVG). Leer lassen f\u00fcr das Standard-Favicon.",
|
||||
"repo.settings.pages.navigation": "Navigationslinks",
|
||||
"repo.settings.pages.navigation_desc": "Steuern Sie, welche integrierten Links in der Kopf- und Fu\u00dfzeilennavigation angezeigt werden.",
|
||||
"repo.settings.pages.nav_show_docs": "Docs-Link anzeigen (verlinkt zum Wiki)",
|
||||
"repo.settings.pages.nav_show_api": "API-Link anzeigen (verlinkt zur Swagger-Dokumentation)",
|
||||
"repo.settings.pages.nav_show_repository": "Repository-Link anzeigen (Quellcode-Button)",
|
||||
"repo.settings.pages.nav_show_releases": "Releases-Link anzeigen",
|
||||
"repo.settings.pages.nav_show_issues": "Issues-Link anzeigen",
|
||||
"repo.settings.pages.blog_section": "Blog-Bereich",
|
||||
"repo.settings.pages.blog_enabled_desc": "Aktuelle Blog-Beitr\u00e4ge auf der Landingpage anzeigen",
|
||||
"repo.settings.pages.blog_headline": "Blog-\u00dcberschrift",
|
||||
"repo.settings.pages.blog_subheadline": "Blog-Unter\u00fcberschrift",
|
||||
"repo.settings.pages.blog_max_posts": "Maximale Anzahl angezeigter Beitr\u00e4ge",
|
||||
"repo.settings.pages.ai_generate": "KI-Inhaltsgenerator",
|
||||
"repo.settings.pages.ai_generate_desc": "Landingpage-Inhalte (\u00dcberschrift, Funktionen, Statistiken, CTAs) automatisch aus der README und den Metadaten Ihres Repositories mit KI generieren.",
|
||||
"repo.settings.pages.ai_generate_button": "Inhalte mit KI generieren",
|
||||
"repo.settings.pages.ai_generate_success": "Landingpage-Inhalte wurden erfolgreich generiert. \u00dcberpr\u00fcfen und passen Sie sie in den anderen Tabs an.",
|
||||
"repo.settings.pages.ai_generate_failed": "KI-Generierung fehlgeschlagen. Bitte versuchen Sie es sp\u00e4ter erneut oder konfigurieren Sie die Inhalte manuell.",
|
||||
"repo.settings.pages.languages": "Sprachen",
|
||||
"repo.settings.pages.default_lang": "Standardsprache",
|
||||
"repo.settings.pages.default_lang_help": "Die Hauptsprache Ihrer Landingpage-Inhalte",
|
||||
"repo.settings.pages.enabled_languages": "Aktivierte Sprachen",
|
||||
"repo.settings.pages.enabled_languages_help": "W\u00e4hlen Sie, welche Sprachen Ihre Landingpage unterst\u00fctzen soll. Besucher sehen einen Sprachwechsler in der Navigation.",
|
||||
"repo.settings.pages.save_languages": "Spracheinstellungen speichern",
|
||||
"repo.settings.pages.languages_saved": "Spracheinstellungen erfolgreich gespeichert.",
|
||||
"repo.settings.pages.translations": "\u00dcbersetzungen",
|
||||
"repo.settings.pages.ai_translate": "KI-\u00dcbersetzung",
|
||||
"repo.settings.pages.ai_translate_success": "\u00dcbersetzung wurde erfolgreich von der KI generiert. \u00dcberpr\u00fcfen und bearbeiten Sie sie nach Bedarf.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "L\u00f6schen",
|
||||
"repo.settings.pages.save_translation": "\u00dcbersetzung speichern",
|
||||
"repo.settings.pages.translation_saved": "\u00dcbersetzung erfolgreich gespeichert.",
|
||||
"repo.settings.pages.translation_deleted": "\u00dcbersetzung gel\u00f6scht.",
|
||||
"repo.settings.pages.translation_empty": "Kein \u00dcbersetzungsinhalt angegeben.",
|
||||
"repo.settings.pages.trans_headline": "\u00dcberschrift",
|
||||
"repo.settings.pages.trans_subheadline": "Unter\u00fcberschrift",
|
||||
"repo.settings.pages.trans_primary_cta": "Prim\u00e4re CTA-Beschriftung",
|
||||
"repo.settings.pages.trans_secondary_cta": "Sekund\u00e4re CTA-Beschriftung",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA-Abschnitt \u00dcberschrift",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA-Abschnitt Unter\u00fcberschrift",
|
||||
"repo.settings.pages.trans_cta_button": "CTA-Button-Beschriftung",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault-Plugin nicht installiert",
|
||||
"repo.vault.plugin_not_installed_desc": "Das Vault-Plugin ist auf diesem Server nicht verfügbar",
|
||||
"repo.vault.secret_limit_reached": "Geheimnis-Limit erreicht",
|
||||
|
||||
@@ -3459,6 +3459,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -3649,6 +3650,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2 \"Powered by GitCaddy Server\" \u03c3\u03c4\u03bf \u03c5\u03c0\u03bf\u03c3\u03ad\u03bb\u03b9\u03b4\u03bf",
|
||||
"admin.config.show_footer_licenses": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5 \u03b1\u03b4\u03b5\u03b9\u03ce\u03bd",
|
||||
"admin.config.show_footer_licenses_desc": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5 \u03b1\u03b4\u03b5\u03b9\u03ce\u03bd \u03c3\u03c4\u03bf \u03c5\u03c0\u03bf\u03c3\u03ad\u03bb\u03b9\u03b4\u03bf",
|
||||
"admin.config.show_footer_api": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5 API",
|
||||
"admin.config.show_footer_api_desc": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5 API (Swagger) \u03c3\u03c4\u03bf \u03c5\u03c0\u03bf\u03c3\u03ad\u03bb\u03b9\u03b4\u03bf",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -3697,6 +3704,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "\u0394\u03b7\u03bc\u03cc\u03c3\u03b9\u03b5\u03c2 \u03b5\u03ba\u03b4\u03cc\u03c3\u03b5\u03b9\u03c2",
|
||||
"repo.settings.pages.public_releases_desc": "\u0395\u03c0\u03b9\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03c3\u03b5 \u03bc\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03bf\u03c5\u03bd \u03b5\u03ba\u03b4\u03cc\u03c3\u03b5\u03b9\u03c2. \u03a7\u03c1\u03ae\u03c3\u03b9\u03bc\u03bf \u03b3\u03b9\u03b1 \u03c3\u03b5\u03bb\u03af\u03b4\u03b5\u03c2 \u03c0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03ce\u03bd \u03b1\u03c0\u03bf\u03b8\u03b5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL Favicon",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL ενός προσαρμοσμένου favicon για τη σελίδα προορισμού σας (ICO, PNG ή SVG). Αφήστε κενό για χρήση του προεπιλεγμένου.",
|
||||
"repo.settings.pages.navigation": "Σύνδεσμοι πλοήγησης",
|
||||
"repo.settings.pages.navigation_desc": "Ελέγξτε ποιοι ενσωματωμένοι σύνδεσμοι εμφανίζονται στην πλοήγηση κεφαλίδας και υποσέλιδου.",
|
||||
"repo.settings.pages.nav_show_docs": "Εμφάνιση συνδέσμου Docs (σύνδεση στο wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Εμφάνιση συνδέσμου API (σύνδεση στην τεκμηρίωση Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "Εμφάνιση συνδέσμου αποθετηρίου (κουμπί Προβολή πηγαίου κώδικα)",
|
||||
"repo.settings.pages.nav_show_releases": "Εμφάνιση συνδέσμου εκδόσεων",
|
||||
"repo.settings.pages.nav_show_issues": "Εμφάνιση συνδέσμου ζητημάτων",
|
||||
"repo.settings.pages.blog_section": "Ενότητα ιστολογίου",
|
||||
"repo.settings.pages.blog_enabled_desc": "Εμφάνιση πρόσφατων αναρτήσεων ιστολογίου στη σελίδα προορισμού",
|
||||
"repo.settings.pages.blog_headline": "Τίτλος ιστολογίου",
|
||||
"repo.settings.pages.blog_subheadline": "Υπότιτλος ιστολογίου",
|
||||
"repo.settings.pages.blog_max_posts": "Μέγιστος αριθμός αναρτήσεων προς εμφάνιση",
|
||||
"repo.settings.pages.ai_generate": "Δημιουργός περιεχομένου AI",
|
||||
"repo.settings.pages.ai_generate_desc": "Δημιουργήστε αυτόματα περιεχόμενο σελίδας προορισμού (τίτλο, χαρακτηριστικά, στατιστικά, CTA) από το README και τα μεταδεδομένα του αποθετηρίου σας με AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Δημιουργία περιεχομένου με AI",
|
||||
"repo.settings.pages.ai_generate_success": "Το περιεχόμενο της σελίδας προορισμού δημιουργήθηκε επιτυχώς. Ελέγξτε και προσαρμόστε στις άλλες καρτέλες.",
|
||||
"repo.settings.pages.ai_generate_failed": "Η δημιουργία περιεχομένου AI απέτυχε. Δοκιμάστε ξανά αργότερα ή διαμορφώστε το περιεχόμενο χειροκίνητα.",
|
||||
"repo.settings.pages.languages": "Γλώσσες",
|
||||
"repo.settings.pages.default_lang": "Προεπιλεγμένη γλώσσα",
|
||||
"repo.settings.pages.default_lang_help": "Η κύρια γλώσσα του περιεχομένου της σελίδας προορισμού σας",
|
||||
"repo.settings.pages.enabled_languages": "Ενεργοποιημένες γλώσσες",
|
||||
"repo.settings.pages.enabled_languages_help": "Επιλέξτε ποιες γλώσσες πρέπει να υποστηρίζει η σελίδα προορισμού σας. Οι επισκέπτες θα δουν έναν εναλλάκτη γλώσσας στην πλοήγηση.",
|
||||
"repo.settings.pages.save_languages": "Αποθήκευση ρυθμίσεων γλώσσας",
|
||||
"repo.settings.pages.languages_saved": "Οι ρυθμίσεις γλώσσας αποθηκεύτηκαν επιτυχώς.",
|
||||
"repo.settings.pages.translations": "Μεταφράσεις",
|
||||
"repo.settings.pages.ai_translate": "Μετάφραση AI",
|
||||
"repo.settings.pages.ai_translate_success": "Η μετάφραση δημιουργήθηκε επιτυχώς από το AI. Ελέγξτε και επεξεργαστείτε όπως χρειάζεται.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Διαγραφή",
|
||||
"repo.settings.pages.save_translation": "Αποθήκευση μετάφρασης",
|
||||
"repo.settings.pages.translation_saved": "Η μετάφραση αποθηκεύτηκε επιτυχώς.",
|
||||
"repo.settings.pages.translation_deleted": "Η μετάφραση διαγράφηκε.",
|
||||
"repo.settings.pages.translation_empty": "Δεν παρέχεται περιεχόμενο μετάφρασης.",
|
||||
"repo.settings.pages.trans_headline": "Τίτλος",
|
||||
"repo.settings.pages.trans_subheadline": "Υπότιτλος",
|
||||
"repo.settings.pages.trans_primary_cta": "Ετικέτα κύριου CTA",
|
||||
"repo.settings.pages.trans_secondary_cta": "Ετικέτα δευτερεύοντος CTA",
|
||||
"repo.settings.pages.trans_cta_headline": "Τίτλος ενότητας CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Υπότιτλος ενότητας CTA",
|
||||
"repo.settings.pages.trans_cta_button": "Ετικέτα κουμπιού CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -2040,6 +2040,7 @@
|
||||
"repo.blog.subscription_required_desc": "This post is available exclusively to subscribers. Subscribe to unlock the full content.",
|
||||
"repo.blog.subscribe_to_read": "Subscribe to Read",
|
||||
"repo.blog.reactions.admin_hint": "Thumbs down counts are only visible to repo admins.",
|
||||
"repo.blog.views": "views",
|
||||
"repo.blog.comments": "Comments",
|
||||
"repo.blog.comments.empty": "No comments yet. Be the first to share your thoughts!",
|
||||
"repo.blog.comments.disabled": "Comments have been disabled for this post.",
|
||||
@@ -2114,6 +2115,38 @@
|
||||
"repo.wishlist.item_closed": "Wishlist item has been closed.",
|
||||
"repo.wishlist.item_reopened": "Wishlist item has been reopened.",
|
||||
"repo.wishlist.cannot_vote_closed": "Cannot vote on a closed item.",
|
||||
"repo.settings.ai": "AI",
|
||||
"repo.settings.ai.globally_disabled": "AI features are disabled by the system administrator.",
|
||||
"repo.settings.ai.enable": "Enable AI",
|
||||
"repo.settings.ai.enable_desc": "Enable AI-powered operations for this repository (code review, issue triage, auto-respond, etc.)",
|
||||
"repo.settings.ai.not_enabled": "AI is not enabled for this repository. Enable it first.",
|
||||
"repo.settings.ai.disabled_by_admin": "(disabled by admin)",
|
||||
"repo.settings.ai.tier1": "Tier 1: Light AI Operations",
|
||||
"repo.settings.ai.auto_respond_issues": "Automatically respond to new issues with helpful suggestions",
|
||||
"repo.settings.ai.auto_review_prs": "Automatically review pull requests for code quality and security",
|
||||
"repo.settings.ai.auto_triage_issues": "Automatically triage and label new issues",
|
||||
"repo.settings.ai.auto_inspect_workflows": "Automatically inspect workflow changes for issues",
|
||||
"repo.settings.ai.tier2": "Tier 2: Agent Mode",
|
||||
"repo.settings.ai.agent_mode": "Enable agent mode (AI can modify code, create branches, and submit PRs)",
|
||||
"repo.settings.ai.agent_trigger_labels": "Trigger Labels",
|
||||
"repo.settings.ai.agent_trigger_labels_desc": "Comma-separated list of labels that trigger agent mode when added to an issue (e.g. ai-fix, ai-implement)",
|
||||
"repo.settings.ai.agent_max_run_minutes": "Max Run Time (minutes)",
|
||||
"repo.settings.ai.escalation": "Escalation",
|
||||
"repo.settings.ai.escalate_to_staff": "Escalate to staff when AI confidence is low or agent fails",
|
||||
"repo.settings.ai.escalation_label": "Escalation Label",
|
||||
"repo.settings.ai.escalation_assign_team": "Assign to Team",
|
||||
"repo.settings.ai.provider": "Provider & Model",
|
||||
"repo.settings.ai.preferred_provider": "Preferred Provider",
|
||||
"repo.settings.ai.preferred_model": "Preferred Model",
|
||||
"repo.settings.ai.inherit_default": "Inherit from org/system default",
|
||||
"repo.settings.ai.resolved_provider": "Currently using",
|
||||
"repo.settings.ai.instructions": "Custom Instructions",
|
||||
"repo.settings.ai.system_instructions": "System Instructions",
|
||||
"repo.settings.ai.system_instructions_desc": "General instructions for all AI operations on this repository",
|
||||
"repo.settings.ai.review_instructions": "Code Review Instructions",
|
||||
"repo.settings.ai.review_instructions_desc": "Specific instructions for AI code reviews (focus areas, coding standards, etc.)",
|
||||
"repo.settings.ai.issue_instructions": "Issue Response Instructions",
|
||||
"repo.settings.ai.issue_instructions_desc": "Specific instructions for AI issue responses (tone, common answers, project context, etc.)",
|
||||
"repo.settings.wishlist": "Wishlist",
|
||||
"repo.settings.wishlist.enable": "Enable Wishlist",
|
||||
"repo.settings.wishlist.enable_help": "When enabled, a Wishlist tab appears on the repository where users can submit and vote on feature requests.",
|
||||
@@ -2313,6 +2346,7 @@
|
||||
"repo.settings.pulls.default_delete_branch_after_merge": "Delete pull request branch after merge by default",
|
||||
"repo.settings.pulls.default_allow_edits_from_maintainers": "Allow edits from maintainers by default",
|
||||
"repo.settings.releases_desc": "Enable Repository Releases",
|
||||
"repo.settings.public_release_downloads_desc": "Allow direct release downloads without authentication (for Limited visibility repos)",
|
||||
"repo.settings.packages_desc": "Enable Repository Packages Registry",
|
||||
"repo.settings.projects_desc": "Enable Projects",
|
||||
"repo.settings.projects_mode_desc": "Projects Mode (which kinds of projects to show)",
|
||||
@@ -3017,6 +3051,20 @@
|
||||
"org.settings.license_file_found": "Found existing license file in .profile: %s",
|
||||
"org.settings.license_overwrite_warning": "This will overwrite the existing %s file in your .profile repository.",
|
||||
"org.settings.license_create_confirm": "This will create a LICENSE.md file in your .profile repository.",
|
||||
"org.settings.ai": "AI",
|
||||
"org.settings.ai.provider": "AI Provider & Configuration",
|
||||
"org.settings.ai.provider_label": "Provider",
|
||||
"org.settings.ai.model_label": "Model",
|
||||
"org.settings.ai.api_key": "API Key",
|
||||
"org.settings.ai.api_key_configured": "An API key is currently configured. Enter a new value to change it.",
|
||||
"org.settings.ai.rate_limits": "Rate Limits",
|
||||
"org.settings.ai.max_ops_per_hour": "Max Operations Per Hour",
|
||||
"org.settings.ai.max_ops_per_hour_desc": "Set to 0 to use the system default. This limit applies per-repository within the organization.",
|
||||
"org.settings.ai.allowed_ops": "Allowed Operations",
|
||||
"org.settings.ai.allowed_ops_placeholder": "code-review, issue-triage, issue-response",
|
||||
"org.settings.ai.allowed_ops_desc": "Comma-separated list of allowed operation types. Leave empty to allow all operations.",
|
||||
"org.settings.ai.advanced": "Advanced",
|
||||
"org.settings.ai.agent_mode_allowed": "Allow Agent Mode for repositories in this organization",
|
||||
"org.settings.homepage_pinning": "Homepage Visibility",
|
||||
"org.settings.pin_to_homepage": "Pin this organization to the homepage",
|
||||
"org.settings.pin_to_homepage_help": "When enabled, this organization will be featured on the public homepage. Only administrators can change this setting.",
|
||||
@@ -3164,6 +3212,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.server_uptime": "Server Uptime",
|
||||
"admin.dashboard.current_goroutine": "Current Goroutines",
|
||||
"admin.dashboard.current_memory_usage": "Current Memory Usage",
|
||||
@@ -3321,6 +3370,17 @@
|
||||
"admin.packages.bulk.global.partial": "Enabled global access for %d package(s), %d failed (may already exist as global)",
|
||||
"admin.packages.bulk.automatch.success": "Auto-matched %d package(s) to repositories",
|
||||
"admin.packages.bulk.automatch.none": "No matching repositories found for selected packages",
|
||||
"admin.packages.bulk.make_private": "Make Private",
|
||||
"admin.packages.bulk.make_public": "Make Public",
|
||||
"admin.packages.bulk.private.enabled": "Made %d package(s) private",
|
||||
"admin.packages.bulk.private.disabled": "Made %d package(s) public",
|
||||
"admin.packages.visibility": "Visibility",
|
||||
"admin.packages.visibility.private": "Private",
|
||||
"admin.packages.visibility.public": "Public",
|
||||
"admin.packages.bulk.delete": "Delete Selected",
|
||||
"admin.packages.bulk.delete.confirm": "Are you sure you want to delete the selected packages and all their versions? This action cannot be undone.",
|
||||
"admin.packages.bulk.delete.success": "Deleted %d package(s) with %d version(s)",
|
||||
"admin.packages.bulk.delete.none": "No packages were deleted",
|
||||
"admin.packages.automatch.button": "Find matching repository",
|
||||
"admin.packages.automatch.match": "Match",
|
||||
"admin.packages.automatch.success": "Package linked to matching repository",
|
||||
@@ -3725,6 +3785,14 @@
|
||||
"packages.no_metadata": "No metadata.",
|
||||
"packages.empty.documentation": "For more information on the package registry, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the documentation</a>.",
|
||||
"packages.empty.repo": "Did you upload a package, but it's not shown here? Go to <a href=\"%[1]s\">package settings</a> and link it to this repo.",
|
||||
"packages.visibility.public": "Public Packages",
|
||||
"packages.visibility.private": "Private Packages",
|
||||
"packages.bulk.actions": "Bulk Actions",
|
||||
"packages.bulk.make_private": "Make Private",
|
||||
"packages.bulk.make_public": "Make Public",
|
||||
"packages.bulk.selected": "Selected:",
|
||||
"packages.bulk.select_all": "Select all",
|
||||
"packages.bulk.no_selection": "Please select at least one package.",
|
||||
"packages.registry.documentation": "For more information on the %s registry, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the documentation</a>.",
|
||||
"packages.filter.type": "Type",
|
||||
"packages.filter.type.all": "All",
|
||||
@@ -4178,6 +4246,12 @@
|
||||
"admin.config.enable_blogs_desc": "Enable the Blogs feature across the platform. Repos can publish blog posts visible under Explore > Blogs.",
|
||||
"admin.config.blogs_in_top_nav": "Blogs in Top Navigation",
|
||||
"admin.config.blogs_in_top_nav_desc": "Show a Blogs link in the site header navigation bar next to Explore",
|
||||
"admin.config.show_footer_powered_by": "Show Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Show the \"Powered by GitCaddy Server\" message in the footer",
|
||||
"admin.config.show_footer_licenses": "Show Licenses Link",
|
||||
"admin.config.show_footer_licenses_desc": "Show the Licenses link in the footer",
|
||||
"admin.config.show_footer_api": "Show API Link",
|
||||
"admin.config.show_footer_api_desc": "Show the API (Swagger) link in the footer",
|
||||
"admin.config.custom_home_title": "Homepage Title",
|
||||
"admin.config.custom_home_title_placeholder": "Leave empty to use app name",
|
||||
"admin.config.custom_home_title_help": "Custom title displayed on the homepage. Leave empty to use the default app name.",
|
||||
@@ -4213,6 +4287,9 @@
|
||||
"repo.settings.group_header": "Group Header",
|
||||
"repo.settings.group_header_placeholder": "e.g., Core Services, Libraries, Tools",
|
||||
"repo.settings.group_header_help": "Optional header for grouping this repository on the organization page",
|
||||
"repo.settings.owner_display_name": "Owner Name",
|
||||
"repo.settings.owner_display_name_placeholder": "e.g., John Smith, Acme Corp",
|
||||
"repo.settings.owner_display_name_help": "A display name for the repository owner. Used in licenses, social cards, and other public-facing contexts instead of the username.",
|
||||
"repo.settings.media_kit": "Media Kit",
|
||||
"repo.settings.media_kit.style": "Social Card Style",
|
||||
"repo.settings.media_kit.style_help": "Choose the visual style for your repository's share card image used in link previews.",
|
||||
@@ -4293,10 +4370,21 @@
|
||||
"repo.settings.subscriptions.no_clients": "No subscribers yet.",
|
||||
"repo.subscribe.title": "Subscribe to %s",
|
||||
"repo.subscribe.description": "This repository requires a subscription to access the source code.",
|
||||
"repo.subscribe.description_with_blogs": "This repository requires a subscription to access the source code and premium blog content.",
|
||||
"repo.subscribe.buy": "Subscribe",
|
||||
"repo.subscribe.payment_required": "A subscription is required to view this repository's source code.",
|
||||
"repo.subscribe.button": "Subscribe for Access",
|
||||
"repo.subscribe.active": "You have an active subscription",
|
||||
"repo.subscribe.monthly": "Monthly",
|
||||
"repo.subscribe.yearly": "Yearly",
|
||||
"repo.subscribe.lifetime": "Lifetime",
|
||||
"repo.subscribe.per_month": "month",
|
||||
"repo.subscribe.per_year": "year",
|
||||
"repo.subscribe.one_time": "one-time payment",
|
||||
"repo.subscribe.choose": "Choose Plan",
|
||||
"repo.subscribe.pay_with_stripe": "Pay with Card",
|
||||
"repo.subscribe.no_products": "No subscription plans are currently available for this repository.",
|
||||
"repo.subscribe.success": "Thank you! Your subscription is now active.",
|
||||
"repo.cross_promoted": "Also Check Out",
|
||||
"repo.settings.license": "License",
|
||||
"repo.settings.license_type": "License Type",
|
||||
@@ -4323,6 +4411,7 @@
|
||||
"repo.settings.license_apply": "Apply",
|
||||
"repo.settings.license_overwrite_warning": "This will overwrite the existing %s file.",
|
||||
"repo.settings.license_create_confirm": "This will create a LICENSE.md file in the repository.",
|
||||
"repo.settings.license_copyright_holder": "Copyright holder",
|
||||
"repo.settings.gallery": "Gallery",
|
||||
"repo.settings.gallery_help": "Upload images to showcase your project. Images are stored in the .gallery folder.",
|
||||
"repo.settings.gallery_upload": "Upload Images",
|
||||
@@ -4348,6 +4437,9 @@
|
||||
"repo.settings.hidden_folders.already_hidden": "This folder is already hidden.",
|
||||
"repo.settings.dotfiles": "Dotfiles",
|
||||
"repo.settings.dotfiles.hide_desc": "Hide files and folders starting with \".\" from the code browser for non-admin users",
|
||||
"repo.settings.app_integration": "App Integration",
|
||||
"repo.settings.app_integration.enable": "Allow anonymous issue reporting and update checks",
|
||||
"repo.settings.app_integration.enable_help": "When enabled, the desktop app can submit issues and check for updates without authentication. Disable this for full access control on private repositories.",
|
||||
"repo.gallery": "Gallery",
|
||||
"api": "API",
|
||||
"admin.config.api_header_url": "API Header Link",
|
||||
@@ -4368,11 +4460,33 @@
|
||||
"repo.settings.pages.saved": "Settings saved successfully",
|
||||
"repo.settings.pages.brand_name": "Brand Name",
|
||||
"repo.settings.pages.brand_name_help": "The name displayed on your landing page",
|
||||
"repo.settings.pages.brand_logo": "Logo",
|
||||
"repo.settings.pages.brand_logo_url": "Logo URL",
|
||||
"repo.settings.pages.brand_logo_url_help": "URL to your logo image (SVG or PNG)",
|
||||
"repo.settings.pages.brand_upload_logo": "Upload Logo",
|
||||
"repo.settings.pages.brand_delete_logo": "Delete Logo",
|
||||
"repo.settings.pages.brand_logo_uploaded": "Logo uploaded successfully.",
|
||||
"repo.settings.pages.brand_logo_deleted": "Logo deleted.",
|
||||
"repo.settings.pages.brand_upload_btn": "Upload",
|
||||
"repo.settings.pages.brand_upload_help": "Upload a JPG, PNG, WebP, or GIF image (max 5 MB). Uploaded images take priority over the URL field.",
|
||||
"repo.settings.pages.brand_upload_error": "Failed to upload image.",
|
||||
"repo.settings.pages.brand_image_too_large": "Image is too large. Maximum size is 5 MB.",
|
||||
"repo.settings.pages.brand_not_an_image": "The uploaded file is not a valid image.",
|
||||
"repo.settings.pages.brand_or": "or use a URL",
|
||||
"repo.settings.pages.brand_tagline": "Tagline",
|
||||
"repo.settings.pages.headline": "Headline",
|
||||
"repo.settings.pages.subheadline": "Subheadline",
|
||||
"repo.settings.pages.hero_image": "Hero Image",
|
||||
"repo.settings.pages.hero_upload": "Upload Image",
|
||||
"repo.settings.pages.hero_upload_btn": "Upload",
|
||||
"repo.settings.pages.hero_upload_help": "Upload a JPG, PNG, WebP, or GIF image (max 5 MB). Uploaded images take priority over the URL field.",
|
||||
"repo.settings.pages.hero_upload_error": "Failed to upload image.",
|
||||
"repo.settings.pages.hero_image_too_large": "Image is too large. Maximum size is 5 MB.",
|
||||
"repo.settings.pages.hero_not_an_image": "The uploaded file is not a valid image.",
|
||||
"repo.settings.pages.hero_image_uploaded": "Hero image uploaded successfully.",
|
||||
"repo.settings.pages.hero_image_deleted": "Hero image deleted.",
|
||||
"repo.settings.pages.hero_delete_image": "Delete Image",
|
||||
"repo.settings.pages.hero_or": "or use a URL",
|
||||
"repo.settings.pages.image_url": "Hero Image URL",
|
||||
"repo.settings.pages.video_url": "Demo Video URL",
|
||||
"repo.settings.pages.code_example": "Code Example",
|
||||
@@ -4380,6 +4494,13 @@
|
||||
"repo.settings.pages.secondary_cta": "Secondary Call to Action",
|
||||
"repo.settings.pages.cta_label": "Button Label",
|
||||
"repo.settings.pages.cta_url": "Button URL",
|
||||
"repo.settings.pages.public_releases": "Public Releases",
|
||||
"repo.settings.pages.public_releases_desc": "Allow unauthenticated users to download releases. Useful for landing pages on private repositories.",
|
||||
"repo.settings.pages.hide_mobile_releases_desc": "Hide mobile platform releases (Android, iOS) from the landing page. Use this when mobile releases are distributed via app stores.",
|
||||
"repo.settings.pages.google_play_id": "Google Play Store ID",
|
||||
"repo.settings.pages.google_play_id_desc": "Package name from the Play Store URL (e.g. com.example.app)",
|
||||
"repo.settings.pages.app_store_id": "Apple App Store ID",
|
||||
"repo.settings.pages.app_store_id_desc": "App ID from the App Store URL (e.g. id123456789)",
|
||||
"repo.settings.pages.stats": "Stats",
|
||||
"repo.settings.pages.value_props": "Value Propositions",
|
||||
"repo.settings.pages.features": "Features",
|
||||
@@ -4404,7 +4525,133 @@
|
||||
"repo.settings.pages.seo_title": "SEO Title",
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.seo_keywords.placeholder": "Add a keyword...",
|
||||
"repo.settings.pages.seo_keywords.add": "Add",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.use_media_kit_og": "Use Media Kit social card",
|
||||
"repo.settings.pages.use_media_kit_og_help": "Use the social card from your Media Kit settings as the Open Graph image instead of a custom URL.",
|
||||
"repo.settings.pages.brand_favicon": "Favicon",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL to a custom favicon for your landing page (ICO, PNG, or SVG). Leave blank to use the default.",
|
||||
"repo.settings.pages.brand_upload_favicon": "Upload Favicon",
|
||||
"repo.settings.pages.brand_delete_favicon": "Delete Favicon",
|
||||
"repo.settings.pages.brand_favicon_uploaded": "Favicon uploaded successfully.",
|
||||
"repo.settings.pages.brand_favicon_deleted": "Favicon deleted.",
|
||||
"repo.settings.pages.brand_favicon_upload_help": "Upload an ICO, PNG, WebP, or GIF favicon (max 5 MB). Uploaded favicons take priority over the URL field.",
|
||||
"repo.settings.pages.navigation": "Navigation Links",
|
||||
"repo.settings.pages.navigation_desc": "Control which built-in links appear in the header and footer navigation.",
|
||||
"repo.settings.pages.nav_show_docs": "Show Docs link (links to wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Show API link (links to Swagger docs)",
|
||||
"repo.settings.pages.nav_show_repository": "Show Repository link (View Source button)",
|
||||
"repo.settings.pages.nav_show_releases": "Show Releases link",
|
||||
"repo.settings.pages.nav_show_issues": "Show Issues link",
|
||||
"repo.settings.pages.blog_section": "Blog Section",
|
||||
"repo.settings.pages.blog_enabled_desc": "Show recent blog posts on the landing page",
|
||||
"repo.settings.pages.blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.blog_max_posts": "Maximum Posts to Show",
|
||||
"repo.settings.pages.gallery_section": "Gallery Section",
|
||||
"repo.settings.pages.gallery_enabled_desc": "Show a gallery of images from your repository's .gallery folder on the landing page",
|
||||
"repo.settings.pages.gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.gallery_max_images": "Maximum Images to Show",
|
||||
"repo.settings.pages.gallery_columns": "Grid Columns",
|
||||
"repo.settings.pages.gallery_help_link": "Upload and manage gallery images in Settings > Gallery.",
|
||||
"repo.settings.pages.comparison": "Comparison",
|
||||
"repo.settings.pages.comparison_section": "Comparison Section",
|
||||
"repo.settings.pages.comparison_enabled_desc": "Show a feature comparison table on the landing page",
|
||||
"repo.settings.pages.comparison_headline": "Headline",
|
||||
"repo.settings.pages.comparison_subheadline": "Subheadline",
|
||||
"repo.settings.pages.comparison_columns": "Comparison Columns",
|
||||
"repo.settings.pages.comparison_columns_help": "Define three columns for your comparison table (e.g., your product vs. two competitors, or three tiers).",
|
||||
"repo.settings.pages.comparison_highlight": "Highlight",
|
||||
"repo.settings.pages.comparison_features": "Features & Groups",
|
||||
"repo.settings.pages.comparison_features_help": "Organize features into optional groups. For each cell, enter \"true\" for a checkmark, \"false\" for an X, or any text.",
|
||||
"repo.settings.pages.comparison_group_name": "Group Name",
|
||||
"repo.settings.pages.comparison_help_link": "Configure comparison columns and features in Settings > Landing Pages > Comparison.",
|
||||
"repo.settings.pages.ai_generate": "AI Content Generator",
|
||||
"repo.settings.pages.ai_generate_desc": "Automatically generate landing page content (headline, features, stats, CTAs) from your repository's README and metadata using AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Generate Content with AI",
|
||||
"repo.settings.pages.ai_generating": "Generating content with AI\u2026 This may take a moment.",
|
||||
"repo.settings.pages.ai_generate_success": "Landing page content has been generated successfully. Review and customize it in the other tabs.",
|
||||
"repo.settings.pages.ai_generate_failed": "Failed to generate content with AI. Please try again later or configure the content manually.",
|
||||
"repo.settings.pages.languages": "Languages",
|
||||
"repo.settings.pages.default_lang": "Default Language",
|
||||
"repo.settings.pages.default_lang_help": "The primary language of your landing page content",
|
||||
"repo.settings.pages.enabled_languages": "Enabled Languages",
|
||||
"repo.settings.pages.enabled_languages_help": "Select which languages your landing page should support. Visitors will see a language switcher in the navigation.",
|
||||
"repo.settings.pages.save_languages": "Save Language Settings",
|
||||
"repo.settings.pages.languages_saved": "Language settings saved successfully.",
|
||||
"repo.settings.pages.translations": "Translations",
|
||||
"repo.settings.pages.ai_translate": "AI Translate",
|
||||
"repo.settings.pages.ai_translate_success": "Translation has been generated successfully by AI. Review and edit as needed.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.ai_translating": "Translating with AI\u2026 This may take a moment.",
|
||||
"repo.settings.pages.delete_translation": "Delete",
|
||||
"repo.settings.pages.save_translation": "Save Translation",
|
||||
"repo.settings.pages.translation_saved": "Translation saved successfully.",
|
||||
"repo.settings.pages.translation_deleted": "Translation deleted.",
|
||||
"repo.settings.pages.translation_empty": "No translation content provided.",
|
||||
"repo.settings.pages.trans_headline": "Headline",
|
||||
"repo.settings.pages.trans_subheadline": "Subheadline",
|
||||
"repo.settings.pages.trans_primary_cta": "Primary CTA Label",
|
||||
"repo.settings.pages.trans_secondary_cta": "Secondary CTA Label",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA Section Headline",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA Section Subheadline",
|
||||
"repo.settings.pages.trans_cta_button": "CTA Button Label",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_section_navigation": "Navigation Labels",
|
||||
"repo.settings.pages.trans_nav_label": "Label",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.settings.pages.advanced": "Advanced",
|
||||
"repo.settings.pages.static_routes": "Static Routes",
|
||||
"repo.settings.pages.static_routes_desc": "Serve files directly from your repository at specific URL paths instead of rendering the landing page. Use exact paths (/badge.svg) or glob patterns (/schema/*).",
|
||||
"repo.settings.pages.add_route": "Add Route",
|
||||
"repo.settings.pages.redirects": "Redirects",
|
||||
"repo.settings.pages.redirects_desc": "Redirect specific paths to other URLs. The source path must start with /.",
|
||||
"repo.settings.pages.add_redirect": "Add Redirect",
|
||||
"repo.settings.pages.custom_code": "Custom Code",
|
||||
"repo.settings.pages.custom_css": "Custom CSS",
|
||||
"repo.settings.pages.custom_head": "Custom Head HTML",
|
||||
"repo.settings.pages.app_stores": "App Stores",
|
||||
"repo.settings.pages.hide_mobile_releases": "Hide Mobile Releases",
|
||||
"repo.vault": "Vault",
|
||||
"repo.vault.secrets": "Secrets",
|
||||
"repo.vault.new_secret": "New Secret",
|
||||
@@ -4539,6 +4786,16 @@
|
||||
"actions.runners.waiting_jobs": "Waiting Jobs",
|
||||
"actions.runners.back_to_runners": "Back to Runners",
|
||||
"actions.runners.no_waiting_jobs": "No jobs waiting for this label",
|
||||
"admin.ai": "AI Status",
|
||||
"admin.ai.title": "AI Service Status",
|
||||
"admin.ai.sidecar_status": "Sidecar Status",
|
||||
"admin.ai.config": "Configuration",
|
||||
"admin.ai.stats": "Statistics",
|
||||
"admin.ai.recent_operations": "Recent Operations",
|
||||
"admin.ai.total_operations": "Total Operations",
|
||||
"admin.ai.operations_24h": "Operations (24h)",
|
||||
"admin.ai.success_rate": "Success Rate",
|
||||
"admin.ai.tokens_used": "Tokens Used",
|
||||
"admin.ai_learning": "AI Learning",
|
||||
"admin.ai_learning.edit": "Edit Pattern",
|
||||
"admin.ai_learning.total_patterns": "Total Patterns",
|
||||
@@ -4751,6 +5008,13 @@
|
||||
"vault.config_error_title": "Vault Not Configured",
|
||||
"vault.config_error_message": "The vault encryption key has not been configured. Secrets cannot be encrypted or decrypted.",
|
||||
"vault.config_error_fix": "Add MASTER_KEY to the [vault] section in app.ini or set the GITCADDY_VAULT_KEY environment variable.",
|
||||
"vault.fallback_key_warning_title": "Vault Using Fallback Encryption Key",
|
||||
"vault.fallback_key_warning_message": "The vault is currently using Gitea's SECRET_KEY for encryption because no dedicated vault key has been configured. If the SECRET_KEY is ever changed or lost, all vault secrets will become permanently unreadable.",
|
||||
"vault.fallback_key_warning_fix": "To fix this, copy the current SECRET_KEY value and set it as MASTER_KEY in the [vault] section of app.ini, or set the GITCADDY_VAULT_KEY environment variable. This ensures vault encryption remains stable even if the SECRET_KEY changes.",
|
||||
"vault.decryption_error_title": "Vault Decryption Failed",
|
||||
"vault.decryption_error_message": "Unable to decrypt vault secrets. The encryption key may have been changed or is incorrect.",
|
||||
"vault.decryption_error_fix": "Verify that the MASTER_KEY in the [vault] section of app.ini (or the GITCADDY_VAULT_KEY environment variable) matches the key that was used when the secrets were originally created.",
|
||||
"vault.encryption_error_message": "Unable to encrypt the secret value. The vault encryption key may not be configured correctly.",
|
||||
"vault.type_file": "File",
|
||||
"vault.compare": "Compare",
|
||||
"vault.compare_version": "Compare this version",
|
||||
|
||||
@@ -3564,6 +3564,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Limpiar paquetes",
|
||||
"admin.dashboard.cleanup_actions": "Limpiar acciones",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Limpiar sesiones de subida expiradas",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Eliminación de acciones antiguas iniciada",
|
||||
"admin.dashboard.gc_lfs": "GC de LFS",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -3754,6 +3755,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Formato de visualización para organizaciones",
|
||||
"admin.config.explore_org_format_list": "Lista",
|
||||
"admin.config.explore_org_format_tiles": "Mosaicos",
|
||||
"admin.config.show_footer_powered_by": "Mostrar Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Mostrar el mensaje \"Powered by GitCaddy Server\" en el pie de p\u00e1gina",
|
||||
"admin.config.show_footer_licenses": "Mostrar enlace de licencias",
|
||||
"admin.config.show_footer_licenses_desc": "Mostrar el enlace de licencias en el pie de p\u00e1gina",
|
||||
"admin.config.show_footer_api": "Mostrar enlace de API",
|
||||
"admin.config.show_footer_api_desc": "Mostrar el enlace de API (Swagger) en el pie de p\u00e1gina",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -3802,6 +3809,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Versiones p\u00fablicas",
|
||||
"repo.settings.pages.public_releases_desc": "Permitir a usuarios no autenticados descargar versiones. \u00datil para p\u00e1ginas de destino de repositorios privados.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL del Favicon",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL de un favicon personalizado para tu p\u00e1gina de destino (ICO, PNG o SVG). D\u00e9jalo en blanco para usar el predeterminado.",
|
||||
"repo.settings.pages.navigation": "Enlaces de navegaci\u00f3n",
|
||||
"repo.settings.pages.navigation_desc": "Controla qu\u00e9 enlaces integrados aparecen en la navegaci\u00f3n del encabezado y pie de p\u00e1gina.",
|
||||
"repo.settings.pages.nav_show_docs": "Mostrar enlace de Docs (enlaza al wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Mostrar enlace de API (enlaza a la documentaci\u00f3n Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "Mostrar enlace del Repositorio (bot\u00f3n Ver c\u00f3digo fuente)",
|
||||
"repo.settings.pages.nav_show_releases": "Mostrar enlace de Releases",
|
||||
"repo.settings.pages.nav_show_issues": "Mostrar enlace de Issues",
|
||||
"repo.settings.pages.blog_section": "Secci\u00f3n de Blog",
|
||||
"repo.settings.pages.blog_enabled_desc": "Mostrar publicaciones recientes del blog en la p\u00e1gina de destino",
|
||||
"repo.settings.pages.blog_headline": "T\u00edtulo del Blog",
|
||||
"repo.settings.pages.blog_subheadline": "Subt\u00edtulo del Blog",
|
||||
"repo.settings.pages.blog_max_posts": "N\u00famero m\u00e1ximo de publicaciones a mostrar",
|
||||
"repo.settings.pages.ai_generate": "Generador de contenido con IA",
|
||||
"repo.settings.pages.ai_generate_desc": "Genera autom\u00e1ticamente el contenido de la p\u00e1gina de destino (t\u00edtulo, caracter\u00edsticas, estad\u00edsticas, CTAs) a partir del README y los metadatos de tu repositorio usando IA.",
|
||||
"repo.settings.pages.ai_generate_button": "Generar contenido con IA",
|
||||
"repo.settings.pages.ai_generate_success": "El contenido de la p\u00e1gina de destino se ha generado correctamente. Rev\u00edsalo y personal\u00edzalo en las otras pesta\u00f1as.",
|
||||
"repo.settings.pages.ai_generate_failed": "Error al generar contenido con IA. Int\u00e9ntalo de nuevo m\u00e1s tarde o configura el contenido manualmente.",
|
||||
"repo.settings.pages.languages": "Idiomas",
|
||||
"repo.settings.pages.default_lang": "Idioma predeterminado",
|
||||
"repo.settings.pages.default_lang_help": "El idioma principal del contenido de tu p\u00e1gina de destino",
|
||||
"repo.settings.pages.enabled_languages": "Idiomas habilitados",
|
||||
"repo.settings.pages.enabled_languages_help": "Selecciona qu\u00e9 idiomas debe admitir tu p\u00e1gina de destino. Los visitantes ver\u00e1n un selector de idioma en la navegaci\u00f3n.",
|
||||
"repo.settings.pages.save_languages": "Guardar configuraci\u00f3n de idiomas",
|
||||
"repo.settings.pages.languages_saved": "Configuraci\u00f3n de idiomas guardada correctamente.",
|
||||
"repo.settings.pages.translations": "Traducciones",
|
||||
"repo.settings.pages.ai_translate": "Traducir con IA",
|
||||
"repo.settings.pages.ai_translate_success": "La traducci\u00f3n se ha generado correctamente con IA. Rev\u00edsala y ed\u00edtala seg\u00fan sea necesario.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Eliminar",
|
||||
"repo.settings.pages.save_translation": "Guardar traducci\u00f3n",
|
||||
"repo.settings.pages.translation_saved": "Traducci\u00f3n guardada correctamente.",
|
||||
"repo.settings.pages.translation_deleted": "Traducci\u00f3n eliminada.",
|
||||
"repo.settings.pages.translation_empty": "No se proporcion\u00f3 contenido de traducci\u00f3n.",
|
||||
"repo.settings.pages.trans_headline": "T\u00edtulo",
|
||||
"repo.settings.pages.trans_subheadline": "Subt\u00edtulo",
|
||||
"repo.settings.pages.trans_primary_cta": "Etiqueta del CTA principal",
|
||||
"repo.settings.pages.trans_secondary_cta": "Etiqueta del CTA secundario",
|
||||
"repo.settings.pages.trans_cta_headline": "T\u00edtulo de la secci\u00f3n CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Subt\u00edtulo de la secci\u00f3n CTA",
|
||||
"repo.settings.pages.trans_cta_button": "Etiqueta del bot\u00f3n CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -2658,6 +2658,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -2903,6 +2904,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "\u0627\u0646\u062a\u0634\u0627\u0631\u0647\u0627\u06cc \u0639\u0645\u0648\u0645\u06cc",
|
||||
"repo.settings.pages.public_releases_desc": "\u0628\u0647 \u06a9\u0627\u0631\u0628\u0631\u0627\u0646 \u0627\u062d\u0631\u0627\u0632 \u0647\u0648\u06cc\u062a \u0646\u0634\u062f\u0647 \u0627\u062c\u0627\u0632\u0647 \u062f\u0627\u0646\u0644\u0648\u062f \u0627\u0646\u062a\u0634\u0627\u0631\u0647\u0627 \u0631\u0627 \u0628\u062f\u0647\u06cc\u062f. \u0628\u0631\u0627\u06cc \u0635\u0641\u062d\u0627\u062a \u0641\u0631\u0648\u062f \u0645\u062e\u0627\u0632\u0646 \u062e\u0635\u0648\u0635\u06cc \u0645\u0641\u06cc\u062f \u0627\u0633\u062a.",
|
||||
"repo.settings.pages.brand_favicon_url": "آدرس فاوآیکون",
|
||||
"repo.settings.pages.brand_favicon_url_help": "آدرس فاوآیکون سفارشی برای صفحه فرود شما (ICO، PNG یا SVG). برای استفاده از پیشفرض خالی بگذارید.",
|
||||
"repo.settings.pages.navigation": "پیوندهای ناوبری",
|
||||
"repo.settings.pages.navigation_desc": "کنترل کنید کدام پیوندهای داخلی در ناوبری سربرگ و پاورقی نمایش داده شوند.",
|
||||
"repo.settings.pages.nav_show_docs": "نمایش پیوند مستندات (پیوند به ویکی)",
|
||||
"repo.settings.pages.nav_show_api": "نمایش پیوند API (پیوند به مستندات Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "نمایش پیوند مخزن (دکمه مشاهده کد منبع)",
|
||||
"repo.settings.pages.nav_show_releases": "نمایش پیوند انتشارها",
|
||||
"repo.settings.pages.nav_show_issues": "نمایش پیوند مسائل",
|
||||
"repo.settings.pages.blog_section": "بخش وبلاگ",
|
||||
"repo.settings.pages.blog_enabled_desc": "نمایش پستهای اخیر وبلاگ در صفحه فرود",
|
||||
"repo.settings.pages.blog_headline": "عنوان وبلاگ",
|
||||
"repo.settings.pages.blog_subheadline": "زیرعنوان وبلاگ",
|
||||
"repo.settings.pages.blog_max_posts": "حداکثر تعداد پستها برای نمایش",
|
||||
"repo.settings.pages.ai_generate": "تولید محتوای هوش مصنوعی",
|
||||
"repo.settings.pages.ai_generate_desc": "تولید خودکار محتوای صفحه فرود (عنوان، ویژگیها، آمار، فراخوانها) از README و فرادادههای مخزن شما با هوش مصنوعی.",
|
||||
"repo.settings.pages.ai_generate_button": "تولید محتوا با هوش مصنوعی",
|
||||
"repo.settings.pages.ai_generate_success": "محتوای صفحه فرود با موفقیت تولید شد. در تبهای دیگر بررسی و سفارشیسازی کنید.",
|
||||
"repo.settings.pages.ai_generate_failed": "تولید محتوا با هوش مصنوعی ناموفق بود. لطفاً بعداً دوباره امتحان کنید یا محتوا را بهصورت دستی پیکربندی کنید.",
|
||||
"repo.settings.pages.languages": "زبانها",
|
||||
"repo.settings.pages.default_lang": "زبان پیشفرض",
|
||||
"repo.settings.pages.default_lang_help": "زبان اصلی محتوای صفحه فرود شما",
|
||||
"repo.settings.pages.enabled_languages": "زبانهای فعال",
|
||||
"repo.settings.pages.enabled_languages_help": "زبانهایی را انتخاب کنید که صفحه فرود شما باید پشتیبانی کند. بازدیدکنندگان یک انتخابگر زبان در ناوبری خواهند دید.",
|
||||
"repo.settings.pages.save_languages": "ذخیره تنظیمات زبان",
|
||||
"repo.settings.pages.languages_saved": "تنظیمات زبان با موفقیت ذخیره شد.",
|
||||
"repo.settings.pages.translations": "ترجمهها",
|
||||
"repo.settings.pages.ai_translate": "ترجمه هوش مصنوعی",
|
||||
"repo.settings.pages.ai_translate_success": "ترجمه با موفقیت توسط هوش مصنوعی تولید شد. در صورت نیاز بررسی و ویرایش کنید.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "حذف",
|
||||
"repo.settings.pages.save_translation": "ذخیره ترجمه",
|
||||
"repo.settings.pages.translation_saved": "ترجمه با موفقیت ذخیره شد.",
|
||||
"repo.settings.pages.translation_deleted": "ترجمه حذف شد.",
|
||||
"repo.settings.pages.translation_empty": "محتوای ترجمه ارائه نشده است.",
|
||||
"repo.settings.pages.trans_headline": "عنوان",
|
||||
"repo.settings.pages.trans_subheadline": "زیرعنوان",
|
||||
"repo.settings.pages.trans_primary_cta": "برچسب فراخوان اصلی",
|
||||
"repo.settings.pages.trans_secondary_cta": "برچسب فراخوان ثانویه",
|
||||
"repo.settings.pages.trans_cta_headline": "عنوان بخش فراخوان",
|
||||
"repo.settings.pages.trans_cta_subheadline": "زیرعنوان بخش فراخوان",
|
||||
"repo.settings.pages.trans_cta_button": "برچسب دکمه فراخوان",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
@@ -3211,6 +3295,12 @@
|
||||
"secrets.read_only": "فقط خواندنی",
|
||||
"admin.config.enable_explore_packages": "فعالسازی کاوش بستهها",
|
||||
"admin.config.enable_explore_packages_desc": "نمایش زبانه بستهها در منوی کاوش برای مرور بستههای عمومی و سراسری",
|
||||
"admin.config.show_footer_powered_by": "\u0646\u0645\u0627\u06cc\u0634 Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "\u0646\u0645\u0627\u06cc\u0634 \u067e\u06cc\u0627\u0645 \"Powered by GitCaddy Server\" \u062f\u0631 \u067e\u0627\u0648\u0631\u0642\u06cc",
|
||||
"admin.config.show_footer_licenses": "\u0646\u0645\u0627\u06cc\u0634 \u0644\u06cc\u0646\u06a9 \u0645\u062c\u0648\u0632\u0647\u0627",
|
||||
"admin.config.show_footer_licenses_desc": "\u0646\u0645\u0627\u06cc\u0634 \u0644\u06cc\u0646\u06a9 \u0645\u062c\u0648\u0632\u0647\u0627 \u062f\u0631 \u067e\u0627\u0648\u0631\u0642\u06cc",
|
||||
"admin.config.show_footer_api": "\u0646\u0645\u0627\u06cc\u0634 \u0644\u06cc\u0646\u06a9 API",
|
||||
"admin.config.show_footer_api_desc": "\u0646\u0645\u0627\u06cc\u0634 \u0644\u06cc\u0646\u06a9 API (Swagger) \u062f\u0631 \u067e\u0627\u0648\u0631\u0642\u06cc",
|
||||
"actions.runners.waiting_jobs": "کارهای در انتظار",
|
||||
"actions.runners.back_to_runners": "بازگشت به اجراکنندهها",
|
||||
"actions.runners.no_waiting_jobs": "هیچ کاری برای این برچسب در انتظار نیست"
|
||||
|
||||
@@ -1920,6 +1920,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -2147,6 +2148,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "N\u00e4yt\u00e4 Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "N\u00e4yt\u00e4 \"Powered by GitCaddy Server\" -viesti alatunnisteessa",
|
||||
"admin.config.show_footer_licenses": "N\u00e4yt\u00e4 lisenssit-linkki",
|
||||
"admin.config.show_footer_licenses_desc": "N\u00e4yt\u00e4 lisenssit-linkki alatunnisteessa",
|
||||
"admin.config.show_footer_api": "N\u00e4yt\u00e4 API-linkki",
|
||||
"admin.config.show_footer_api_desc": "N\u00e4yt\u00e4 API (Swagger) -linkki alatunnisteessa",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -2195,6 +2202,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Julkiset julkaisut",
|
||||
"repo.settings.pages.public_releases_desc": "Salli todentamattomien k\u00e4ytt\u00e4jien ladata julkaisuja. Hy\u00f6dyllinen yksityisten arkistojen laskeutumissivuilla.",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon-URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "Mukautetun faviconin URL aloitussivullesi (ICO, PNG tai SVG). J\u00e4t\u00e4 tyhj\u00e4ksi k\u00e4ytt\u00e4\u00e4ksesi oletusta.",
|
||||
"repo.settings.pages.navigation": "Navigointilinkit",
|
||||
"repo.settings.pages.navigation_desc": "Hallitse, mitk\u00e4 sis\u00e4\u00e4nrakennetut linkit n\u00e4kyv\u00e4t yl\u00e4- ja alatunnisteen navigoinnissa.",
|
||||
"repo.settings.pages.nav_show_docs": "N\u00e4yt\u00e4 Docs-linkki (linkitt\u00e4\u00e4 wikiin)",
|
||||
"repo.settings.pages.nav_show_api": "N\u00e4yt\u00e4 API-linkki (linkitt\u00e4\u00e4 Swagger-dokumentaatioon)",
|
||||
"repo.settings.pages.nav_show_repository": "N\u00e4yt\u00e4 tietovarastolinkki (N\u00e4yt\u00e4 l\u00e4hdekoodi -painike)",
|
||||
"repo.settings.pages.nav_show_releases": "N\u00e4yt\u00e4 julkaisulinkki",
|
||||
"repo.settings.pages.nav_show_issues": "N\u00e4yt\u00e4 ongelmalinkki",
|
||||
"repo.settings.pages.blog_section": "Blogiosio",
|
||||
"repo.settings.pages.blog_enabled_desc": "N\u00e4yt\u00e4 viimeisimm\u00e4t blogikirjoitukset aloitussivulla",
|
||||
"repo.settings.pages.blog_headline": "Blogin otsikko",
|
||||
"repo.settings.pages.blog_subheadline": "Blogin alaotsikko",
|
||||
"repo.settings.pages.blog_max_posts": "N\u00e4ytett\u00e4vien kirjoitusten enimm\u00e4ism\u00e4\u00e4r\u00e4",
|
||||
"repo.settings.pages.ai_generate": "AI-sis\u00e4ll\u00f6ntuottaja",
|
||||
"repo.settings.pages.ai_generate_desc": "Luo automaattisesti aloitussivun sis\u00e4lt\u00f6 (otsikko, ominaisuudet, tilastot, toimintakehotukset) tietovaraston README-tiedostosta ja metatiedoista AI:n avulla.",
|
||||
"repo.settings.pages.ai_generate_button": "Luo sis\u00e4lt\u00f6\u00e4 AI:lla",
|
||||
"repo.settings.pages.ai_generate_success": "Aloitussivun sis\u00e4lt\u00f6 on luotu onnistuneesti. Tarkista ja mukauta muissa v\u00e4lilehdiss\u00e4.",
|
||||
"repo.settings.pages.ai_generate_failed": "AI-sis\u00e4ll\u00f6n luonti ep\u00e4onnistui. Yrit\u00e4 my\u00f6hemmin uudelleen tai m\u00e4\u00e4rit\u00e4 sis\u00e4lt\u00f6 manuaalisesti.",
|
||||
"repo.settings.pages.languages": "Kielet",
|
||||
"repo.settings.pages.default_lang": "Oletuskieli",
|
||||
"repo.settings.pages.default_lang_help": "Aloitussivun sis\u00e4ll\u00f6n p\u00e4\u00e4kieli",
|
||||
"repo.settings.pages.enabled_languages": "K\u00e4yt\u00f6ss\u00e4 olevat kielet",
|
||||
"repo.settings.pages.enabled_languages_help": "Valitse, mit\u00e4 kieli\u00e4 aloitussivusi tukee. Vierailijat n\u00e4kev\u00e4t kielenvaihtajan navigoinnissa.",
|
||||
"repo.settings.pages.save_languages": "Tallenna kieliasetukset",
|
||||
"repo.settings.pages.languages_saved": "Kieliasetukset tallennettu onnistuneesti.",
|
||||
"repo.settings.pages.translations": "K\u00e4\u00e4nn\u00f6kset",
|
||||
"repo.settings.pages.ai_translate": "AI-k\u00e4\u00e4nn\u00f6s",
|
||||
"repo.settings.pages.ai_translate_success": "K\u00e4\u00e4nn\u00f6s on luotu onnistuneesti AI:n avulla. Tarkista ja muokkaa tarvittaessa.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Poista",
|
||||
"repo.settings.pages.save_translation": "Tallenna k\u00e4\u00e4nn\u00f6s",
|
||||
"repo.settings.pages.translation_saved": "K\u00e4\u00e4nn\u00f6s tallennettu onnistuneesti.",
|
||||
"repo.settings.pages.translation_deleted": "K\u00e4\u00e4nn\u00f6s poistettu.",
|
||||
"repo.settings.pages.translation_empty": "K\u00e4\u00e4nn\u00f6ssis\u00e4lt\u00f6\u00e4 ei annettu.",
|
||||
"repo.settings.pages.trans_headline": "Otsikko",
|
||||
"repo.settings.pages.trans_subheadline": "Alaotsikko",
|
||||
"repo.settings.pages.trans_primary_cta": "Ensisijaisen CTA:n teksti",
|
||||
"repo.settings.pages.trans_secondary_cta": "Toissijaisen CTA:n teksti",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA-osion otsikko",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA-osion alaotsikko",
|
||||
"repo.settings.pages.trans_cta_button": "CTA-painikkeen teksti",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3960,6 +3960,7 @@
|
||||
"org.settings.pin_to_homepage_help": "When enabled, this organization will be featured on the public homepage. Only administrators can change this setting.",
|
||||
"admin.dashboard.resync_all_hooks": "Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...)",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.users.2fa": "2FA",
|
||||
"admin.packages.version": "Version",
|
||||
"admin.auths.port": "Port",
|
||||
@@ -4100,6 +4101,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "Afficher Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Afficher le message \"Powered by GitCaddy Server\" dans le pied de page",
|
||||
"admin.config.show_footer_licenses": "Afficher le lien des licences",
|
||||
"admin.config.show_footer_licenses_desc": "Afficher le lien des licences dans le pied de page",
|
||||
"admin.config.show_footer_api": "Afficher le lien API",
|
||||
"admin.config.show_footer_api_desc": "Afficher le lien API (Swagger) dans le pied de page",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -4148,6 +4155,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Versions publiques",
|
||||
"repo.settings.pages.public_releases_desc": "Autoriser les utilisateurs non authentifi\u00e9s \u00e0 t\u00e9l\u00e9charger les versions. Utile pour les pages d'accueil des d\u00e9p\u00f4ts priv\u00e9s.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL du Favicon",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL d'un favicon personnalis\u00e9 pour votre page d'atterrissage (ICO, PNG ou SVG). Laissez vide pour utiliser le favicon par d\u00e9faut.",
|
||||
"repo.settings.pages.navigation": "Liens de navigation",
|
||||
"repo.settings.pages.navigation_desc": "Contr\u00f4lez quels liens int\u00e9gr\u00e9s apparaissent dans la navigation de l'en-t\u00eate et du pied de page.",
|
||||
"repo.settings.pages.nav_show_docs": "Afficher le lien Docs (lien vers le wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Afficher le lien API (lien vers la documentation Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "Afficher le lien du d\u00e9p\u00f4t (bouton Voir le code source)",
|
||||
"repo.settings.pages.nav_show_releases": "Afficher le lien Releases",
|
||||
"repo.settings.pages.nav_show_issues": "Afficher le lien Issues",
|
||||
"repo.settings.pages.blog_section": "Section Blog",
|
||||
"repo.settings.pages.blog_enabled_desc": "Afficher les articles de blog r\u00e9cents sur la page d'atterrissage",
|
||||
"repo.settings.pages.blog_headline": "Titre du Blog",
|
||||
"repo.settings.pages.blog_subheadline": "Sous-titre du Blog",
|
||||
"repo.settings.pages.blog_max_posts": "Nombre maximum d'articles \u00e0 afficher",
|
||||
"repo.settings.pages.ai_generate": "G\u00e9n\u00e9rateur de contenu IA",
|
||||
"repo.settings.pages.ai_generate_desc": "G\u00e9n\u00e9rez automatiquement le contenu de la page d'atterrissage (titre, fonctionnalit\u00e9s, statistiques, CTAs) \u00e0 partir du README et des m\u00e9tadonn\u00e9es de votre d\u00e9p\u00f4t gr\u00e2ce \u00e0 l'IA.",
|
||||
"repo.settings.pages.ai_generate_button": "G\u00e9n\u00e9rer le contenu avec l'IA",
|
||||
"repo.settings.pages.ai_generate_success": "Le contenu de la page d'atterrissage a \u00e9t\u00e9 g\u00e9n\u00e9r\u00e9 avec succ\u00e8s. V\u00e9rifiez et personnalisez-le dans les autres onglets.",
|
||||
"repo.settings.pages.ai_generate_failed": "\u00c9chec de la g\u00e9n\u00e9ration du contenu par l'IA. Veuillez r\u00e9essayer plus tard ou configurer le contenu manuellement.",
|
||||
"repo.settings.pages.languages": "Langues",
|
||||
"repo.settings.pages.default_lang": "Langue par d\u00e9faut",
|
||||
"repo.settings.pages.default_lang_help": "La langue principale du contenu de votre page d'atterrissage",
|
||||
"repo.settings.pages.enabled_languages": "Langues activ\u00e9es",
|
||||
"repo.settings.pages.enabled_languages_help": "S\u00e9lectionnez les langues que votre page d'atterrissage doit prendre en charge. Les visiteurs verront un s\u00e9lecteur de langue dans la navigation.",
|
||||
"repo.settings.pages.save_languages": "Enregistrer les param\u00e8tres de langue",
|
||||
"repo.settings.pages.languages_saved": "Param\u00e8tres de langue enregistr\u00e9s avec succ\u00e8s.",
|
||||
"repo.settings.pages.translations": "Traductions",
|
||||
"repo.settings.pages.ai_translate": "Traduction IA",
|
||||
"repo.settings.pages.ai_translate_success": "La traduction a \u00e9t\u00e9 g\u00e9n\u00e9r\u00e9e avec succ\u00e8s par l'IA. V\u00e9rifiez et modifiez si n\u00e9cessaire.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Supprimer",
|
||||
"repo.settings.pages.save_translation": "Enregistrer la traduction",
|
||||
"repo.settings.pages.translation_saved": "Traduction enregistr\u00e9e avec succ\u00e8s.",
|
||||
"repo.settings.pages.translation_deleted": "Traduction supprim\u00e9e.",
|
||||
"repo.settings.pages.translation_empty": "Aucun contenu de traduction fourni.",
|
||||
"repo.settings.pages.trans_headline": "Titre",
|
||||
"repo.settings.pages.trans_subheadline": "Sous-titre",
|
||||
"repo.settings.pages.trans_primary_cta": "Libell\u00e9 du CTA principal",
|
||||
"repo.settings.pages.trans_secondary_cta": "Libell\u00e9 du CTA secondaire",
|
||||
"repo.settings.pages.trans_cta_headline": "Titre de la section CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Sous-titre de la section CTA",
|
||||
"repo.settings.pages.trans_cta_button": "Libell\u00e9 du bouton CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3902,6 +3902,7 @@
|
||||
"org.settings.pin_to_homepage_help": "When enabled, this organization will be featured on the public homepage. Only administrators can change this setting.",
|
||||
"admin.dashboard.cron.process": "Cron: %[1]s",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.users.2fa": "2FA",
|
||||
"admin.auths.ssh_keys_are_verified": "SSH keys in LDAP are considered as verified",
|
||||
"admin.config.db_ssl_mode": "SSL",
|
||||
@@ -4029,6 +4030,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "Taispe\u00e1in Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Taispe\u00e1in an teachtaireacht \"Powered by GitCaddy Server\" sa bhunt\u00e1sc",
|
||||
"admin.config.show_footer_licenses": "Taispe\u00e1in nasc cead\u00fanas",
|
||||
"admin.config.show_footer_licenses_desc": "Taispe\u00e1in an nasc cead\u00fanas sa bhunt\u00e1sc",
|
||||
"admin.config.show_footer_api": "Taispe\u00e1in nasc API",
|
||||
"admin.config.show_footer_api_desc": "Taispe\u00e1in an nasc API (Swagger) sa bhunt\u00e1sc",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -4077,6 +4084,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Eisi\u00faint\u00ed poibl\u00ed",
|
||||
"repo.settings.pages.public_releases_desc": "Ceadaigh d'\u00fas\u00e1ideoir\u00ed neamhfh\u00edordheimhnithe eisi\u00faint\u00ed a \u00edosl\u00f3d\u00e1il. \u00das\u00e1ideach do leathanaigh tuirlingthe st\u00f3rtha pr\u00edobh\u00e1ideacha.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL Favicon",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL d'fhavicon saincheaptha do do leathanach tuirlingthe (ICO, PNG nó SVG). Fág folamh chun an ceann réamhshocraithe a úsáid.",
|
||||
"repo.settings.pages.navigation": "Naisc nascleanúna",
|
||||
"repo.settings.pages.navigation_desc": "Rialaigh cé na naisc ionsuite a thaispeántar sa nascleanúint ceanntásc agus buntásc.",
|
||||
"repo.settings.pages.nav_show_docs": "Taispeáin nasc Docs (nascanna le wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Taispeáin nasc API (nascanna le doiciméadú Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "Taispeáin nasc stóras (cnaipe Féach ar an gcód foinse)",
|
||||
"repo.settings.pages.nav_show_releases": "Taispeáin nasc eisiúintí",
|
||||
"repo.settings.pages.nav_show_issues": "Taispeáin nasc saincheisteanna",
|
||||
"repo.settings.pages.blog_section": "Rannóg bhlag",
|
||||
"repo.settings.pages.blog_enabled_desc": "Taispeáin postálacha blaga le déanaí ar an leathanach tuirlingthe",
|
||||
"repo.settings.pages.blog_headline": "Ceannlíne an bhlag",
|
||||
"repo.settings.pages.blog_subheadline": "Fo-cheannlíne an bhlag",
|
||||
"repo.settings.pages.blog_max_posts": "Uasmhéid postálacha le taispeáint",
|
||||
"repo.settings.pages.ai_generate": "Gineadóir ábhair AI",
|
||||
"repo.settings.pages.ai_generate_desc": "Gin ábhar leathanaigh tuirlingthe go huathoibríoch (ceannlíne, gnéithe, staitisticí, CTAanna) ó README agus meiteashonraí do stóras le AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Gin ábhar le AI",
|
||||
"repo.settings.pages.ai_generate_success": "Gineadh ábhar an leathanaigh tuirlingthe go rathúil. Athbhreithnigh agus saincheap sna cluaisíní eile.",
|
||||
"repo.settings.pages.ai_generate_failed": "Theip ar ghiniúint ábhair AI. Bain triail as arís níos déanaí nó cumraigh an t-ábhar de láimh.",
|
||||
"repo.settings.pages.languages": "Teangacha",
|
||||
"repo.settings.pages.default_lang": "Teanga réamhshocraithe",
|
||||
"repo.settings.pages.default_lang_help": "Príomhtheanga ábhair do leathanaigh tuirlingthe",
|
||||
"repo.settings.pages.enabled_languages": "Teangacha cumasaithe",
|
||||
"repo.settings.pages.enabled_languages_help": "Roghnaigh na teangacha ba cheart do do leathanach tuirlingthe tacú leo. Feicfidh cuairteoirí aistritheoir teanga sa nascleanúint.",
|
||||
"repo.settings.pages.save_languages": "Sábháil socruithe teanga",
|
||||
"repo.settings.pages.languages_saved": "Sábháladh na socruithe teanga go rathúil.",
|
||||
"repo.settings.pages.translations": "Aistriúcháin",
|
||||
"repo.settings.pages.ai_translate": "Aistriúchán AI",
|
||||
"repo.settings.pages.ai_translate_success": "Ghin AI an t-aistriúchán go rathúil. Athbhreithnigh agus cuir in eagar de réir mar is gá.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Scrios",
|
||||
"repo.settings.pages.save_translation": "Sábháil aistriúchán",
|
||||
"repo.settings.pages.translation_saved": "Sábháladh an t-aistriúchán go rathúil.",
|
||||
"repo.settings.pages.translation_deleted": "Scriosadh an t-aistriúchán.",
|
||||
"repo.settings.pages.translation_empty": "Níor soláthraíodh aon ábhar aistriúcháin.",
|
||||
"repo.settings.pages.trans_headline": "Ceannlíne",
|
||||
"repo.settings.pages.trans_subheadline": "Fo-cheannlíne",
|
||||
"repo.settings.pages.trans_primary_cta": "Lipéad príomh-CTA",
|
||||
"repo.settings.pages.trans_secondary_cta": "Lipéad tánaisteach CTA",
|
||||
"repo.settings.pages.trans_cta_headline": "Ceannlíne rannóg CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Fo-cheannlíne rannóg CTA",
|
||||
"repo.settings.pages.trans_cta_button": "Lipéad cnaipe CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -2955,6 +2955,7 @@
|
||||
"admin.dashboard.cleanup_packages": "पैकेज साफ करें",
|
||||
"admin.dashboard.cleanup_actions": "कार्य साफ करें",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "समाप्त अपलोड साफ करें",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.server_uptime": "Server Uptime",
|
||||
"admin.dashboard.current_goroutine": "Current Goroutines",
|
||||
"admin.dashboard.current_memory_usage": "Current Memory Usage",
|
||||
@@ -3935,6 +3936,12 @@
|
||||
"admin.config.explore_org_display_format_help": "संगठनों के लिए प्रदर्शन प्रारूप",
|
||||
"admin.config.explore_org_format_list": "सूची",
|
||||
"admin.config.explore_org_format_tiles": "टाइल्स",
|
||||
"admin.config.show_footer_powered_by": "Powered By \u0926\u093f\u0916\u093e\u090f\u0902",
|
||||
"admin.config.show_footer_powered_by_desc": "\u092b\u093c\u0941\u091f\u0930 \u092e\u0947\u0902 \"Powered by GitCaddy Server\" \u0938\u0902\u0926\u0947\u0936 \u0926\u093f\u0916\u093e\u090f\u0902",
|
||||
"admin.config.show_footer_licenses": "\u0932\u093e\u0907\u0938\u0947\u0902\u0938 \u0932\u093f\u0902\u0915 \u0926\u093f\u0916\u093e\u090f\u0902",
|
||||
"admin.config.show_footer_licenses_desc": "\u092b\u093c\u0941\u091f\u0930 \u092e\u0947\u0902 \u0932\u093e\u0907\u0938\u0947\u0902\u0938 \u0932\u093f\u0902\u0915 \u0926\u093f\u0916\u093e\u090f\u0902",
|
||||
"admin.config.show_footer_api": "API \u0932\u093f\u0902\u0915 \u0926\u093f\u0916\u093e\u090f\u0902",
|
||||
"admin.config.show_footer_api_desc": "\u092b\u093c\u0941\u091f\u0930 \u092e\u0947\u0902 API (Swagger) \u0932\u093f\u0902\u0915 \u0926\u093f\u0916\u093e\u090f\u0902",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -3983,6 +3990,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "\u0938\u093e\u0930\u094d\u0935\u091c\u0928\u093f\u0915 \u0930\u093f\u0932\u0940\u091c\u093c",
|
||||
"repo.settings.pages.public_releases_desc": "\u0905\u092a\u094d\u0930\u092e\u093e\u0923\u093f\u0924 \u0909\u092a\u092f\u094b\u0917\u0915\u0930\u094d\u0924\u093e\u0913\u0902 \u0915\u094b \u0930\u093f\u0932\u0940\u091c\u093c \u0921\u093e\u0909\u0928\u0932\u094b\u0921 \u0915\u0930\u0928\u0947 \u0915\u0940 \u0905\u0928\u0941\u092e\u0924\u093f \u0926\u0947\u0902\u0964 \u0928\u093f\u091c\u0940 \u0930\u093f\u092a\u0949\u091c\u093f\u091f\u0930\u0940 \u0915\u0947 \u0932\u0948\u0902\u0921\u093f\u0902\u0917 \u092a\u0947\u091c \u0915\u0947 \u0932\u093f\u090f \u0909\u092a\u092f\u094b\u0917\u0940\u0964",
|
||||
"repo.settings.pages.brand_favicon_url": "\u092b\u093c\u0947\u0935\u093f\u0915\u0949\u0928 URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "\u0906\u092a\u0915\u0947 \u0932\u0948\u0902\u0921\u093f\u0902\u0917 \u092a\u0947\u091c \u0915\u0947 \u0932\u093f\u090f \u0915\u0938\u094d\u091f\u092e \u092b\u093c\u0947\u0935\u093f\u0915\u0949\u0928 \u0915\u093e URL (ICO, PNG, \u092f\u093e SVG)\u0964 \u0921\u093f\u092b\u093c\u0949\u0932\u094d\u091f \u0909\u092a\u092f\u094b\u0917 \u0915\u0947 \u0932\u093f\u090f \u0916\u093e\u0932\u0940 \u091b\u094b\u0921\u093c\u0947\u0902\u0964",
|
||||
"repo.settings.pages.navigation": "\u0928\u0947\u0935\u093f\u0917\u0947\u0936\u0928 \u0932\u093f\u0902\u0915",
|
||||
"repo.settings.pages.navigation_desc": "\u0928\u093f\u092f\u0902\u0924\u094d\u0930\u093f\u0924 \u0915\u0930\u0947\u0902 \u0915\u093f \u0939\u0947\u0921\u0930 \u0914\u0930 \u092b\u093c\u0941\u091f\u0930 \u0928\u0947\u0935\u093f\u0917\u0947\u0936\u0928 \u092e\u0947\u0902 \u0915\u094c\u0928 \u0938\u0947 \u092c\u093f\u0932\u094d\u091f-\u0907\u0928 \u0932\u093f\u0902\u0915 \u0926\u093f\u0916\u093e\u0908 \u0926\u0947\u0902\u0964",
|
||||
"repo.settings.pages.nav_show_docs": "Docs \u0932\u093f\u0902\u0915 \u0926\u093f\u0916\u093e\u090f\u0902 (\u0935\u093f\u0915\u0940 \u0938\u0947 \u0932\u093f\u0902\u0915)",
|
||||
"repo.settings.pages.nav_show_api": "API \u0932\u093f\u0902\u0915 \u0926\u093f\u0916\u093e\u090f\u0902 (Swagger \u0921\u0949\u0915\u094d\u092f\u0942\u092e\u0947\u0902\u091f\u0947\u0936\u0928 \u0938\u0947 \u0932\u093f\u0902\u0915)",
|
||||
"repo.settings.pages.nav_show_repository": "\u0930\u093f\u092a\u0949\u091c\u093f\u091f\u0930\u0940 \u0932\u093f\u0902\u0915 \u0926\u093f\u0916\u093e\u090f\u0902 (\u0938\u094b\u0930\u094d\u0938 \u0915\u094b\u0921 \u0926\u0947\u0916\u0947\u0902 \u092c\u091f\u0928)",
|
||||
"repo.settings.pages.nav_show_releases": "\u0930\u093f\u0932\u0940\u091c\u093c \u0932\u093f\u0902\u0915 \u0926\u093f\u0916\u093e\u090f\u0902",
|
||||
"repo.settings.pages.nav_show_issues": "Issues \u0932\u093f\u0902\u0915 \u0926\u093f\u0916\u093e\u090f\u0902",
|
||||
"repo.settings.pages.blog_section": "\u092c\u094d\u0932\u0949\u0917 \u0905\u0928\u0941\u092d\u093e\u0917",
|
||||
"repo.settings.pages.blog_enabled_desc": "\u0932\u0948\u0902\u0921\u093f\u0902\u0917 \u092a\u0947\u091c \u092a\u0930 \u0939\u093e\u0932 \u0915\u0947 \u092c\u094d\u0932\u0949\u0917 \u092a\u094b\u0938\u094d\u091f \u0926\u093f\u0916\u093e\u090f\u0902",
|
||||
"repo.settings.pages.blog_headline": "\u092c\u094d\u0932\u0949\u0917 \u0936\u0940\u0930\u094d\u0937\u0915",
|
||||
"repo.settings.pages.blog_subheadline": "\u092c\u094d\u0932\u0949\u0917 \u0909\u092a\u0936\u0940\u0930\u094d\u0937\u0915",
|
||||
"repo.settings.pages.blog_max_posts": "\u0926\u093f\u0916\u093e\u0928\u0947 \u0915\u0947 \u0932\u093f\u090f \u0905\u0927\u093f\u0915\u0924\u092e \u092a\u094b\u0938\u094d\u091f",
|
||||
"repo.settings.pages.ai_generate": "AI \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u091c\u0928\u0930\u0947\u091f\u0930",
|
||||
"repo.settings.pages.ai_generate_desc": "AI \u0915\u093e \u0909\u092a\u092f\u094b\u0917 \u0915\u0930\u0915\u0947 \u0905\u092a\u0928\u0947 \u0930\u093f\u092a\u0949\u091c\u093f\u091f\u0930\u0940 \u0915\u0947 README \u0914\u0930 \u092e\u0947\u091f\u093e\u0921\u0947\u091f\u093e \u0938\u0947 \u0932\u0948\u0902\u0921\u093f\u0902\u0917 \u092a\u0947\u091c \u0938\u093e\u092e\u0917\u094d\u0930\u0940 (\u0936\u0940\u0930\u094d\u0937\u0915, \u0938\u0941\u0935\u093f\u0927\u093e\u090f\u0901, \u0906\u0901\u0915\u0921\u093c\u0947, CTA) \u0938\u094d\u0935\u091a\u093e\u0932\u093f\u0924 \u0930\u0942\u092a \u0938\u0947 \u091c\u0928\u0930\u0947\u091f \u0915\u0930\u0947\u0902\u0964",
|
||||
"repo.settings.pages.ai_generate_button": "AI \u0938\u0947 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u091c\u0928\u0930\u0947\u091f \u0915\u0930\u0947\u0902",
|
||||
"repo.settings.pages.ai_generate_success": "\u0932\u0948\u0902\u0921\u093f\u0902\u0917 \u092a\u0947\u091c \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0938\u092b\u0932\u0924\u093e\u092a\u0942\u0930\u094d\u0935\u0915 \u091c\u0928\u0930\u0947\u091f \u0915\u0940 \u0917\u0908 \u0939\u0948\u0964 \u0905\u0928\u094d\u092f \u091f\u0948\u092c \u092e\u0947\u0902 \u0938\u092e\u0940\u0915\u094d\u0937\u093e \u0915\u0930\u0947\u0902 \u0914\u0930 \u0905\u0928\u0941\u0915\u0942\u0932\u093f\u0924 \u0915\u0930\u0947\u0902\u0964",
|
||||
"repo.settings.pages.ai_generate_failed": "AI \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u091c\u0928\u0930\u0947\u0936\u0928 \u0935\u093f\u092b\u0932 \u0930\u0939\u0940\u0964 \u0915\u0943\u092a\u092f\u093e \u092c\u093e\u0926 \u092e\u0947\u0902 \u092a\u0941\u0928\u0903 \u092a\u094d\u0930\u092f\u093e\u0938 \u0915\u0930\u0947\u0902 \u092f\u093e \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u092e\u0948\u0928\u094d\u092f\u0941\u0905\u0932 \u0930\u0942\u092a \u0938\u0947 \u0915\u0949\u0928\u094d\u092b\u093c\u093f\u0917\u0930 \u0915\u0930\u0947\u0902\u0964",
|
||||
"repo.settings.pages.languages": "\u092d\u093e\u0937\u093e\u090f\u0901",
|
||||
"repo.settings.pages.default_lang": "\u0921\u093f\u092b\u093c\u0949\u0932\u094d\u091f \u092d\u093e\u0937\u093e",
|
||||
"repo.settings.pages.default_lang_help": "\u0906\u092a\u0915\u0947 \u0932\u0948\u0902\u0921\u093f\u0902\u0917 \u092a\u0947\u091c \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u0915\u0940 \u092e\u0941\u0916\u094d\u092f \u092d\u093e\u0937\u093e",
|
||||
"repo.settings.pages.enabled_languages": "\u0938\u0915\u094d\u0937\u092e \u092d\u093e\u0937\u093e\u090f\u0901",
|
||||
"repo.settings.pages.enabled_languages_help": "\u091a\u0941\u0928\u0947\u0902 \u0915\u093f \u0906\u092a\u0915\u093e \u0932\u0948\u0902\u0921\u093f\u0902\u0917 \u092a\u0947\u091c \u0915\u093f\u0928 \u092d\u093e\u0937\u093e\u0913\u0902 \u0915\u093e \u0938\u092e\u0930\u094d\u0925\u0928 \u0915\u0930\u0947\u0964 \u0906\u0917\u0902\u0924\u0941\u0915\u094b\u0902 \u0915\u094b \u0928\u0947\u0935\u093f\u0917\u0947\u0936\u0928 \u092e\u0947\u0902 \u092d\u093e\u0937\u093e \u0938\u094d\u0935\u093f\u091a\u0930 \u0926\u093f\u0916\u093e\u0908 \u0926\u0947\u0917\u093e\u0964",
|
||||
"repo.settings.pages.save_languages": "\u092d\u093e\u0937\u093e \u0938\u0947\u091f\u093f\u0902\u0917 \u0938\u0939\u0947\u091c\u0947\u0902",
|
||||
"repo.settings.pages.languages_saved": "\u092d\u093e\u0937\u093e \u0938\u0947\u091f\u093f\u0902\u0917 \u0938\u092b\u0932\u0924\u093e\u092a\u0942\u0930\u094d\u0935\u0915 \u0938\u0939\u0947\u091c\u0940 \u0917\u0908\u0964",
|
||||
"repo.settings.pages.translations": "\u0905\u0928\u0941\u0935\u093e\u0926",
|
||||
"repo.settings.pages.ai_translate": "AI \u0905\u0928\u0941\u0935\u093e\u0926",
|
||||
"repo.settings.pages.ai_translate_success": "AI \u0926\u094d\u0935\u093e\u0930\u093e \u0905\u0928\u0941\u0935\u093e\u0926 \u0938\u092b\u0932\u0924\u093e\u092a\u0942\u0930\u094d\u0935\u0915 \u091c\u0928\u0930\u0947\u091f \u0915\u093f\u092f\u093e \u0917\u092f\u093e \u0939\u0948\u0964 \u0906\u0935\u0936\u094d\u092f\u0915\u0924\u093e\u0928\u0941\u0938\u093e\u0930 \u0938\u092e\u0940\u0915\u094d\u0937\u093e \u0915\u0930\u0947\u0902 \u0914\u0930 \u0938\u0902\u092a\u093e\u0926\u093f\u0924 \u0915\u0930\u0947\u0902\u0964",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "\u0939\u091f\u093e\u090f\u0901",
|
||||
"repo.settings.pages.save_translation": "\u0905\u0928\u0941\u0935\u093e\u0926 \u0938\u0939\u0947\u091c\u0947\u0902",
|
||||
"repo.settings.pages.translation_saved": "\u0905\u0928\u0941\u0935\u093e\u0926 \u0938\u092b\u0932\u0924\u093e\u092a\u0942\u0930\u094d\u0935\u0915 \u0938\u0939\u0947\u091c\u093e \u0917\u092f\u093e\u0964",
|
||||
"repo.settings.pages.translation_deleted": "\u0905\u0928\u0941\u0935\u093e\u0926 \u0939\u091f\u093e\u092f\u093e \u0917\u092f\u093e\u0964",
|
||||
"repo.settings.pages.translation_empty": "\u0915\u094b\u0908 \u0905\u0928\u0941\u0935\u093e\u0926 \u0938\u093e\u092e\u0917\u094d\u0930\u0940 \u092a\u094d\u0930\u0926\u093e\u0928 \u0928\u0939\u0940\u0902 \u0915\u0940 \u0917\u0908\u0964",
|
||||
"repo.settings.pages.trans_headline": "\u0936\u0940\u0930\u094d\u0937\u0915",
|
||||
"repo.settings.pages.trans_subheadline": "\u0909\u092a\u0936\u0940\u0930\u094d\u0937\u0915",
|
||||
"repo.settings.pages.trans_primary_cta": "\u092a\u094d\u0930\u093e\u0907\u092e\u0930\u0940 CTA \u0932\u0947\u092c\u0932",
|
||||
"repo.settings.pages.trans_secondary_cta": "\u0938\u0947\u0915\u0947\u0902\u0921\u0930\u0940 CTA \u0932\u0947\u092c\u0932",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA \u0905\u0928\u0941\u092d\u093e\u0917 \u0936\u0940\u0930\u094d\u0937\u0915",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA \u0905\u0928\u0941\u092d\u093e\u0917 \u0909\u092a\u0936\u0940\u0930\u094d\u0937\u0915",
|
||||
"repo.settings.pages.trans_cta_button": "CTA \u092c\u091f\u0928 \u0932\u0947\u092c\u0932",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault": "वॉल्ट",
|
||||
"repo.vault.secrets": "Secrets",
|
||||
"repo.vault.new_secret": "New Secret",
|
||||
|
||||
@@ -1833,6 +1833,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -2064,6 +2065,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "Powered By megjelen\u00edt\u00e9se",
|
||||
"admin.config.show_footer_powered_by_desc": "A \"Powered by GitCaddy Server\" \u00fczenet megjelen\u00edt\u00e9se a l\u00e1bl\u00e9cben",
|
||||
"admin.config.show_footer_licenses": "Licencek link megjelen\u00edt\u00e9se",
|
||||
"admin.config.show_footer_licenses_desc": "A Licencek link megjelen\u00edt\u00e9se a l\u00e1bl\u00e9cben",
|
||||
"admin.config.show_footer_api": "API link megjelen\u00edt\u00e9se",
|
||||
"admin.config.show_footer_api_desc": "Az API (Swagger) link megjelen\u00edt\u00e9se a l\u00e1bl\u00e9cben",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -2112,6 +2119,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Nyilv\u00e1nos kiad\u00e1sok",
|
||||
"repo.settings.pages.public_releases_desc": "A nem hiteles\u00edtett felhaszn\u00e1l\u00f3k sz\u00e1m\u00e1ra enged\u00e9lyezze a kiad\u00e1sok let\u00f6lt\u00e9s\u00e9t. Hasznos priv\u00e1t t\u00e1rol\u00f3k c\u00e9loldalaihoz.",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "Egy\u00e9ni favicon URL a c\u00e9loldalhoz (ICO, PNG vagy SVG). Hagyja \u00fcresen az alap\u00e9rtelmezett haszn\u00e1lat\u00e1hoz.",
|
||||
"repo.settings.pages.navigation": "Navig\u00e1ci\u00f3s linkek",
|
||||
"repo.settings.pages.navigation_desc": "Szab\u00e1lyozza, mely be\u00e9p\u00edtett linkek jelenjenek meg a fejl\u00e9c \u00e9s l\u00e1bl\u00e9c navig\u00e1ci\u00f3ban.",
|
||||
"repo.settings.pages.nav_show_docs": "Docs link megjelen\u00edt\u00e9se (wiki-re mutat)",
|
||||
"repo.settings.pages.nav_show_api": "API link megjelen\u00edt\u00e9se (Swagger dokument\u00e1ci\u00f3ra mutat)",
|
||||
"repo.settings.pages.nav_show_repository": "T\u00e1rol\u00f3 link megjelen\u00edt\u00e9se (Forr\u00e1sk\u00f3d megtekint\u00e9se gomb)",
|
||||
"repo.settings.pages.nav_show_releases": "Kiad\u00e1sok link megjelen\u00edt\u00e9se",
|
||||
"repo.settings.pages.nav_show_issues": "Hibajegyek link megjelen\u00edt\u00e9se",
|
||||
"repo.settings.pages.blog_section": "Blog szekci\u00f3",
|
||||
"repo.settings.pages.blog_enabled_desc": "Legut\u00f3bbi blogbejegyz\u00e9sek megjelen\u00edt\u00e9se a c\u00e9loldalon",
|
||||
"repo.settings.pages.blog_headline": "Blog c\u00edmsor",
|
||||
"repo.settings.pages.blog_subheadline": "Blog alc\u00edmsor",
|
||||
"repo.settings.pages.blog_max_posts": "Megjelen\u00edtend\u0151 bejegyz\u00e9sek maxim\u00e1lis sz\u00e1ma",
|
||||
"repo.settings.pages.ai_generate": "AI tartalomgener\u00e1tor",
|
||||
"repo.settings.pages.ai_generate_desc": "Automatikusan gener\u00e1ljon c\u00e9loldal tartalmat (c\u00edmsor, funkci\u00f3k, statisztik\u00e1k, CTA-k) a t\u00e1rol\u00f3 README-j\u00e9b\u0151l \u00e9s metaadataib\u00f3l AI seg\u00edts\u00e9g\u00e9vel.",
|
||||
"repo.settings.pages.ai_generate_button": "Tartalom gener\u00e1l\u00e1sa AI-val",
|
||||
"repo.settings.pages.ai_generate_success": "A c\u00e9loldal tartalma sikeresen gener\u00e1lva. Tekintse \u00e1t \u00e9s szabja testre a t\u00f6bbi f\u00fcl\u00f6n.",
|
||||
"repo.settings.pages.ai_generate_failed": "Az AI tartalomgener\u00e1l\u00e1s sikertelen. Pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb, vagy konfigur\u00e1lja a tartalmat manu\u00e1lisan.",
|
||||
"repo.settings.pages.languages": "Nyelvek",
|
||||
"repo.settings.pages.default_lang": "Alap\u00e9rtelmezett nyelv",
|
||||
"repo.settings.pages.default_lang_help": "A c\u00e9loldal tartalm\u00e1nak f\u0151 nyelve",
|
||||
"repo.settings.pages.enabled_languages": "Enged\u00e9lyezett nyelvek",
|
||||
"repo.settings.pages.enabled_languages_help": "V\u00e1lassza ki, mely nyelveket t\u00e1mogassa a c\u00e9loldala. A l\u00e1togat\u00f3k nyelvv\u00e1laszt\u00f3t fognak l\u00e1tni a navig\u00e1ci\u00f3ban.",
|
||||
"repo.settings.pages.save_languages": "Nyelvi be\u00e1ll\u00edt\u00e1sok ment\u00e9se",
|
||||
"repo.settings.pages.languages_saved": "Nyelvi be\u00e1ll\u00edt\u00e1sok sikeresen mentve.",
|
||||
"repo.settings.pages.translations": "Ford\u00edt\u00e1sok",
|
||||
"repo.settings.pages.ai_translate": "AI ford\u00edt\u00e1s",
|
||||
"repo.settings.pages.ai_translate_success": "A ford\u00edt\u00e1st az AI sikeresen gener\u00e1lta. Tekintse \u00e1t \u00e9s szerkessze sz\u00fcks\u00e9g szerint.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "T\u00f6rl\u00e9s",
|
||||
"repo.settings.pages.save_translation": "Ford\u00edt\u00e1s ment\u00e9se",
|
||||
"repo.settings.pages.translation_saved": "Ford\u00edt\u00e1s sikeresen mentve.",
|
||||
"repo.settings.pages.translation_deleted": "Ford\u00edt\u00e1s t\u00f6r\u00f6lve.",
|
||||
"repo.settings.pages.translation_empty": "Nem adtak meg ford\u00edt\u00e1si tartalmat.",
|
||||
"repo.settings.pages.trans_headline": "C\u00edmsor",
|
||||
"repo.settings.pages.trans_subheadline": "Alc\u00edmsor",
|
||||
"repo.settings.pages.trans_primary_cta": "Els\u0151dleges CTA c\u00edmke",
|
||||
"repo.settings.pages.trans_secondary_cta": "M\u00e1sodlagos CTA c\u00edmke",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA szekci\u00f3 c\u00edmsor",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA szekci\u00f3 alc\u00edmsor",
|
||||
"repo.settings.pages.trans_cta_button": "CTA gomb c\u00edmke",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -1652,6 +1652,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -1888,6 +1889,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "Tampilkan Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Tampilkan pesan \"Powered by GitCaddy Server\" di footer",
|
||||
"admin.config.show_footer_licenses": "Tampilkan tautan lisensi",
|
||||
"admin.config.show_footer_licenses_desc": "Tampilkan tautan lisensi di footer",
|
||||
"admin.config.show_footer_api": "Tampilkan tautan API",
|
||||
"admin.config.show_footer_api_desc": "Tampilkan tautan API (Swagger) di footer",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -1936,6 +1943,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Rilis publik",
|
||||
"repo.settings.pages.public_releases_desc": "Izinkan pengguna yang tidak terautentikasi mengunduh rilis. Berguna untuk halaman arahan repositori privat.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL Favicon",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL favicon kustom untuk halaman arahan Anda (ICO, PNG, atau SVG). Biarkan kosong untuk menggunakan default.",
|
||||
"repo.settings.pages.navigation": "Tautan navigasi",
|
||||
"repo.settings.pages.navigation_desc": "Kontrol tautan bawaan mana yang muncul di navigasi header dan footer.",
|
||||
"repo.settings.pages.nav_show_docs": "Tampilkan tautan Docs (menaut ke wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Tampilkan tautan API (menaut ke dokumentasi Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "Tampilkan tautan repositori (tombol Lihat kode sumber)",
|
||||
"repo.settings.pages.nav_show_releases": "Tampilkan tautan rilis",
|
||||
"repo.settings.pages.nav_show_issues": "Tampilkan tautan isu",
|
||||
"repo.settings.pages.blog_section": "Bagian blog",
|
||||
"repo.settings.pages.blog_enabled_desc": "Tampilkan postingan blog terbaru di halaman arahan",
|
||||
"repo.settings.pages.blog_headline": "Judul blog",
|
||||
"repo.settings.pages.blog_subheadline": "Subjudul blog",
|
||||
"repo.settings.pages.blog_max_posts": "Jumlah maksimal postingan yang ditampilkan",
|
||||
"repo.settings.pages.ai_generate": "Generator konten AI",
|
||||
"repo.settings.pages.ai_generate_desc": "Buat konten halaman arahan secara otomatis (judul, fitur, statistik, CTA) dari README dan metadata repositori Anda menggunakan AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Buat konten dengan AI",
|
||||
"repo.settings.pages.ai_generate_success": "Konten halaman arahan telah berhasil dibuat. Tinjau dan sesuaikan di tab lainnya.",
|
||||
"repo.settings.pages.ai_generate_failed": "Pembuatan konten AI gagal. Coba lagi nanti atau konfigurasikan konten secara manual.",
|
||||
"repo.settings.pages.languages": "Bahasa",
|
||||
"repo.settings.pages.default_lang": "Bahasa default",
|
||||
"repo.settings.pages.default_lang_help": "Bahasa utama konten halaman arahan Anda",
|
||||
"repo.settings.pages.enabled_languages": "Bahasa yang diaktifkan",
|
||||
"repo.settings.pages.enabled_languages_help": "Pilih bahasa yang harus didukung halaman arahan Anda. Pengunjung akan melihat pemilih bahasa di navigasi.",
|
||||
"repo.settings.pages.save_languages": "Simpan pengaturan bahasa",
|
||||
"repo.settings.pages.languages_saved": "Pengaturan bahasa berhasil disimpan.",
|
||||
"repo.settings.pages.translations": "Terjemahan",
|
||||
"repo.settings.pages.ai_translate": "Terjemahan AI",
|
||||
"repo.settings.pages.ai_translate_success": "Terjemahan telah berhasil dibuat oleh AI. Tinjau dan edit sesuai kebutuhan.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Hapus",
|
||||
"repo.settings.pages.save_translation": "Simpan terjemahan",
|
||||
"repo.settings.pages.translation_saved": "Terjemahan berhasil disimpan.",
|
||||
"repo.settings.pages.translation_deleted": "Terjemahan dihapus.",
|
||||
"repo.settings.pages.translation_empty": "Tidak ada konten terjemahan yang diberikan.",
|
||||
"repo.settings.pages.trans_headline": "Judul",
|
||||
"repo.settings.pages.trans_subheadline": "Subjudul",
|
||||
"repo.settings.pages.trans_primary_cta": "Label CTA utama",
|
||||
"repo.settings.pages.trans_secondary_cta": "Label CTA sekunder",
|
||||
"repo.settings.pages.trans_cta_headline": "Judul bagian CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Subjudul bagian CTA",
|
||||
"repo.settings.pages.trans_cta_button": "Label tombol CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -1555,6 +1555,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -1776,6 +1777,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "S\u00fdna Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "S\u00fdna \"Powered by GitCaddy Server\" skilabo\u00f0 \u00ed f\u00e6ti",
|
||||
"admin.config.show_footer_licenses": "S\u00fdna leyfistengil",
|
||||
"admin.config.show_footer_licenses_desc": "S\u00fdna leyfistengil \u00ed f\u00e6ti",
|
||||
"admin.config.show_footer_api": "S\u00fdna API tengil",
|
||||
"admin.config.show_footer_api_desc": "S\u00fdna API (Swagger) tengil \u00ed f\u00e6ti",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -1824,6 +1831,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Opinberar \u00fatg\u00e1fur",
|
||||
"repo.settings.pages.public_releases_desc": "Leyfa \u00f3sta\u00f0festum notendum a\u00f0 hla\u00f0a ni\u00f0ur \u00fatg\u00e1fum. Gagnlegt fyrir \u00e1fangasÃ\u00f0ur einkageymsla.",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL á sérsniðnu favicon fyrir áfangasíðuna þína (ICO, PNG eða SVG). Skildu eftir autt til að nota sjálfgefið.",
|
||||
"repo.settings.pages.navigation": "Leiðsögutenglar",
|
||||
"repo.settings.pages.navigation_desc": "Stjórnaðu hvaða innbyggðu tenglar birtast í leiðsögu haus og fóts.",
|
||||
"repo.settings.pages.nav_show_docs": "Sýna Docs tengil (tengist wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Sýna API tengil (tengist Swagger skjölun)",
|
||||
"repo.settings.pages.nav_show_repository": "Sýna geymslu tengil (Skoða frumkóða hnappur)",
|
||||
"repo.settings.pages.nav_show_releases": "Sýna útgáfu tengil",
|
||||
"repo.settings.pages.nav_show_issues": "Sýna vandamála tengil",
|
||||
"repo.settings.pages.blog_section": "Bloggshluti",
|
||||
"repo.settings.pages.blog_enabled_desc": "Sýna nýlegar bloggfærslur á áfangasíðunni",
|
||||
"repo.settings.pages.blog_headline": "Fyrirsögn bloggs",
|
||||
"repo.settings.pages.blog_subheadline": "Undirfyrirsögn bloggs",
|
||||
"repo.settings.pages.blog_max_posts": "Hámarksfjöldi færslna til að sýna",
|
||||
"repo.settings.pages.ai_generate": "AI efnisgerð",
|
||||
"repo.settings.pages.ai_generate_desc": "Búðu sjálfkrafa til efni áfangasíðu (fyrirsögn, eiginleika, tölfræði, CTA) úr README og lýsigögnum geymslunnar með AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Búa til efni með AI",
|
||||
"repo.settings.pages.ai_generate_success": "Efni áfangasíðu hefur verið búið til. Skoðaðu og sérsníddu í öðrum flipum.",
|
||||
"repo.settings.pages.ai_generate_failed": "AI efnisgerð mistókst. Reyndu aftur síðar eða stilltu efnið handvirkt.",
|
||||
"repo.settings.pages.languages": "Tungumál",
|
||||
"repo.settings.pages.default_lang": "Sjálfgefið tungumál",
|
||||
"repo.settings.pages.default_lang_help": "Aðaltungumál efnis áfangasíðunnar",
|
||||
"repo.settings.pages.enabled_languages": "Virk tungumál",
|
||||
"repo.settings.pages.enabled_languages_help": "Veldu hvaða tungumál áfangasíðan þín ætti að styðja. Gestir sjá tungumálaval í leiðsögu.",
|
||||
"repo.settings.pages.save_languages": "Vista tungumálastillingar",
|
||||
"repo.settings.pages.languages_saved": "Tungumálastillingar vistaðar.",
|
||||
"repo.settings.pages.translations": "Þýðingar",
|
||||
"repo.settings.pages.ai_translate": "AI þýðing",
|
||||
"repo.settings.pages.ai_translate_success": "Þýðing hefur verið búin til af AI. Skoðaðu og breyttu eftir þörfum.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Eyða",
|
||||
"repo.settings.pages.save_translation": "Vista þýðingu",
|
||||
"repo.settings.pages.translation_saved": "Þýðing vistuð.",
|
||||
"repo.settings.pages.translation_deleted": "Þýðingu eytt.",
|
||||
"repo.settings.pages.translation_empty": "Ekkert þýðingarefni gefið upp.",
|
||||
"repo.settings.pages.trans_headline": "Fyrirsögn",
|
||||
"repo.settings.pages.trans_subheadline": "Undirfyrirsögn",
|
||||
"repo.settings.pages.trans_primary_cta": "Merki aðal-CTA",
|
||||
"repo.settings.pages.trans_secondary_cta": "Merki auka-CTA",
|
||||
"repo.settings.pages.trans_cta_headline": "Fyrirsögn CTA hluta",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Undirfyrirsögn CTA hluta",
|
||||
"repo.settings.pages.trans_cta_button": "Merki CTA hnapps",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -2988,6 +2988,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Pulisci pacchetti",
|
||||
"admin.dashboard.cleanup_actions": "Pulisci azioni",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Pulisci upload scaduti",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -3200,6 +3201,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Formato visualizzazione per organizzazioni",
|
||||
"admin.config.explore_org_format_list": "Lista",
|
||||
"admin.config.explore_org_format_tiles": "Riquadri",
|
||||
"admin.config.show_footer_powered_by": "Mostra Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Mostra il messaggio \"Powered by GitCaddy Server\" nel pi\u00e8 di pagina",
|
||||
"admin.config.show_footer_licenses": "Mostra link licenze",
|
||||
"admin.config.show_footer_licenses_desc": "Mostra il link delle licenze nel pi\u00e8 di pagina",
|
||||
"admin.config.show_footer_api": "Mostra link API",
|
||||
"admin.config.show_footer_api_desc": "Mostra il link API (Swagger) nel pi\u00e8 di pagina",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -3248,6 +3255,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Release pubbliche",
|
||||
"repo.settings.pages.public_releases_desc": "Consenti agli utenti non autenticati di scaricare le release. Utile per le landing page dei repository privati.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL Favicon",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL di una favicon personalizzata per la tua landing page (ICO, PNG o SVG). Lascia vuoto per usare quella predefinita.",
|
||||
"repo.settings.pages.navigation": "Link di navigazione",
|
||||
"repo.settings.pages.navigation_desc": "Controlla quali link integrati appaiono nella navigazione dell'intestazione e del pi\u00e8 di pagina.",
|
||||
"repo.settings.pages.nav_show_docs": "Mostra link Docs (collega al wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Mostra link API (collega alla documentazione Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "Mostra link Repository (pulsante Visualizza codice sorgente)",
|
||||
"repo.settings.pages.nav_show_releases": "Mostra link Release",
|
||||
"repo.settings.pages.nav_show_issues": "Mostra link Issues",
|
||||
"repo.settings.pages.blog_section": "Sezione Blog",
|
||||
"repo.settings.pages.blog_enabled_desc": "Mostra gli articoli recenti del blog sulla landing page",
|
||||
"repo.settings.pages.blog_headline": "Titolo del Blog",
|
||||
"repo.settings.pages.blog_subheadline": "Sottotitolo del Blog",
|
||||
"repo.settings.pages.blog_max_posts": "Numero massimo di articoli da mostrare",
|
||||
"repo.settings.pages.ai_generate": "Generatore di contenuti IA",
|
||||
"repo.settings.pages.ai_generate_desc": "Genera automaticamente il contenuto della landing page (titolo, funzionalit\u00e0, statistiche, CTA) dal README e dai metadati del repository usando l'IA.",
|
||||
"repo.settings.pages.ai_generate_button": "Genera contenuti con IA",
|
||||
"repo.settings.pages.ai_generate_success": "Il contenuto della landing page \u00e8 stato generato con successo. Rivedi e personalizza nelle altre schede.",
|
||||
"repo.settings.pages.ai_generate_failed": "Generazione IA fallita. Riprova pi\u00f9 tardi o configura il contenuto manualmente.",
|
||||
"repo.settings.pages.languages": "Lingue",
|
||||
"repo.settings.pages.default_lang": "Lingua predefinita",
|
||||
"repo.settings.pages.default_lang_help": "La lingua principale del contenuto della tua landing page",
|
||||
"repo.settings.pages.enabled_languages": "Lingue abilitate",
|
||||
"repo.settings.pages.enabled_languages_help": "Seleziona quali lingue deve supportare la tua landing page. I visitatori vedranno un selettore di lingua nella navigazione.",
|
||||
"repo.settings.pages.save_languages": "Salva impostazioni lingua",
|
||||
"repo.settings.pages.languages_saved": "Impostazioni lingua salvate con successo.",
|
||||
"repo.settings.pages.translations": "Traduzioni",
|
||||
"repo.settings.pages.ai_translate": "Traduzione IA",
|
||||
"repo.settings.pages.ai_translate_success": "La traduzione \u00e8 stata generata con successo dall'IA. Rivedi e modifica secondo necessit\u00e0.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Elimina",
|
||||
"repo.settings.pages.save_translation": "Salva traduzione",
|
||||
"repo.settings.pages.translation_saved": "Traduzione salvata con successo.",
|
||||
"repo.settings.pages.translation_deleted": "Traduzione eliminata.",
|
||||
"repo.settings.pages.translation_empty": "Nessun contenuto di traduzione fornito.",
|
||||
"repo.settings.pages.trans_headline": "Titolo",
|
||||
"repo.settings.pages.trans_subheadline": "Sottotitolo",
|
||||
"repo.settings.pages.trans_primary_cta": "Etichetta CTA principale",
|
||||
"repo.settings.pages.trans_secondary_cta": "Etichetta CTA secondario",
|
||||
"repo.settings.pages.trans_cta_headline": "Titolo sezione CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Sottotitolo sezione CTA",
|
||||
"repo.settings.pages.trans_cta_button": "Etichetta pulsante CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -4014,6 +4014,7 @@
|
||||
"org.settings.pin_to_homepage_help": "When enabled, this organization will be featured on the public homepage. Only administrators can change this setting.",
|
||||
"admin.dashboard.cron.process": "Cron: %[1]s",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.users.bot": "Bot",
|
||||
"admin.users.2fa": "2FA",
|
||||
"admin.auths.ssh_keys_are_verified": "SSH keys in LDAP are considered as verified",
|
||||
@@ -4144,6 +4145,12 @@
|
||||
"admin.config.explore_org_display_format_help": "組織の表示形式",
|
||||
"admin.config.explore_org_format_list": "リスト",
|
||||
"admin.config.explore_org_format_tiles": "タイル",
|
||||
"admin.config.show_footer_powered_by": "Powered By\u3092\u8868\u793a",
|
||||
"admin.config.show_footer_powered_by_desc": "\u30d5\u30c3\u30bf\u30fc\u306b\u300cPowered by GitCaddy Server\u300d\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u8868\u793a\u3059\u308b",
|
||||
"admin.config.show_footer_licenses": "\u30e9\u30a4\u30bb\u30f3\u30b9\u30ea\u30f3\u30af\u3092\u8868\u793a",
|
||||
"admin.config.show_footer_licenses_desc": "\u30d5\u30c3\u30bf\u30fc\u306b\u30e9\u30a4\u30bb\u30f3\u30b9\u30ea\u30f3\u30af\u3092\u8868\u793a\u3059\u308b",
|
||||
"admin.config.show_footer_api": "API\u30ea\u30f3\u30af\u3092\u8868\u793a",
|
||||
"admin.config.show_footer_api_desc": "\u30d5\u30c3\u30bf\u30fc\u306bAPI\uff08Swagger\uff09\u30ea\u30f3\u30af\u3092\u8868\u793a\u3059\u308b",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -4192,6 +4199,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "\u516c\u958b\u30ea\u30ea\u30fc\u30b9",
|
||||
"repo.settings.pages.public_releases_desc": "\u672a\u8a8d\u8a3c\u30e6\u30fc\u30b6\u30fc\u306b\u30ea\u30ea\u30fc\u30b9\u306e\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3092\u8a31\u53ef\u3057\u307e\u3059\u3002\u30d7\u30e9\u30a4\u30d9\u30fc\u30c8\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u30e9\u30f3\u30c7\u30a3\u30f3\u30b0\u30da\u30fc\u30b8\u306b\u4fbf\u5229\u3067\u3059\u3002",
|
||||
"repo.settings.pages.brand_favicon_url": "\u30d5\u30a1\u30d3\u30b3\u30f3URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "\u30e9\u30f3\u30c7\u30a3\u30f3\u30b0\u30da\u30fc\u30b8\u7528\u306e\u30ab\u30b9\u30bf\u30e0\u30d5\u30a1\u30d3\u30b3\u30f3\u306eURL\uff08ICO\u3001PNG\u3001\u307e\u305f\u306fSVG\uff09\u3002\u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u4f7f\u7528\u3059\u308b\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
|
||||
"repo.settings.pages.navigation": "\u30ca\u30d3\u30b2\u30fc\u30b7\u30e7\u30f3\u30ea\u30f3\u30af",
|
||||
"repo.settings.pages.navigation_desc": "\u30d8\u30c3\u30c0\u30fc\u3068\u30d5\u30c3\u30bf\u30fc\u306e\u30ca\u30d3\u30b2\u30fc\u30b7\u30e7\u30f3\u306b\u8868\u793a\u3059\u308b\u7d44\u307f\u8fbc\u307f\u30ea\u30f3\u30af\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002",
|
||||
"repo.settings.pages.nav_show_docs": "Docs\u30ea\u30f3\u30af\u3092\u8868\u793a\uff08Wiki\u306b\u30ea\u30f3\u30af\uff09",
|
||||
"repo.settings.pages.nav_show_api": "API\u30ea\u30f3\u30af\u3092\u8868\u793a\uff08Swagger\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u30ea\u30f3\u30af\uff09",
|
||||
"repo.settings.pages.nav_show_repository": "\u30ea\u30dd\u30b8\u30c8\u30ea\u30ea\u30f3\u30af\u3092\u8868\u793a\uff08\u30bd\u30fc\u30b9\u30b3\u30fc\u30c9\u8868\u793a\u30dc\u30bf\u30f3\uff09",
|
||||
"repo.settings.pages.nav_show_releases": "Releases\u30ea\u30f3\u30af\u3092\u8868\u793a",
|
||||
"repo.settings.pages.nav_show_issues": "Issues\u30ea\u30f3\u30af\u3092\u8868\u793a",
|
||||
"repo.settings.pages.blog_section": "\u30d6\u30ed\u30b0\u30bb\u30af\u30b7\u30e7\u30f3",
|
||||
"repo.settings.pages.blog_enabled_desc": "\u30e9\u30f3\u30c7\u30a3\u30f3\u30b0\u30da\u30fc\u30b8\u306b\u6700\u8fd1\u306e\u30d6\u30ed\u30b0\u8a18\u4e8b\u3092\u8868\u793a\u3059\u308b",
|
||||
"repo.settings.pages.blog_headline": "\u30d6\u30ed\u30b0\u306e\u898b\u51fa\u3057",
|
||||
"repo.settings.pages.blog_subheadline": "\u30d6\u30ed\u30b0\u306e\u30b5\u30d6\u898b\u51fa\u3057",
|
||||
"repo.settings.pages.blog_max_posts": "\u8868\u793a\u3059\u308b\u6700\u5927\u6295\u7a3f\u6570",
|
||||
"repo.settings.pages.ai_generate": "AI\u30b3\u30f3\u30c6\u30f3\u30c4\u30b8\u30a7\u30cd\u30ec\u30fc\u30bf\u30fc",
|
||||
"repo.settings.pages.ai_generate_desc": "\u30ea\u30dd\u30b8\u30c8\u30ea\u306eREADME\u3068\u30e1\u30bf\u30c7\u30fc\u30bf\u304b\u3089AI\u3092\u4f7f\u7528\u3057\u3066\u30e9\u30f3\u30c7\u30a3\u30f3\u30b0\u30da\u30fc\u30b8\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\uff08\u898b\u51fa\u3057\u3001\u6a5f\u80fd\u3001\u7d71\u8a08\u3001CTA\uff09\u3092\u81ea\u52d5\u751f\u6210\u3057\u307e\u3059\u3002",
|
||||
"repo.settings.pages.ai_generate_button": "AI\u3067\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u751f\u6210",
|
||||
"repo.settings.pages.ai_generate_success": "\u30e9\u30f3\u30c7\u30a3\u30f3\u30b0\u30da\u30fc\u30b8\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u304c\u6b63\u5e38\u306b\u751f\u6210\u3055\u308c\u307e\u3057\u305f\u3002\u4ed6\u306e\u30bf\u30d6\u3067\u78ba\u8a8d\u3057\u3066\u30ab\u30b9\u30bf\u30de\u30a4\u30ba\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
|
||||
"repo.settings.pages.ai_generate_failed": "AI\u306b\u3088\u308b\u30b3\u30f3\u30c6\u30f3\u30c4\u751f\u6210\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u3044\u305f\u3060\u304f\u304b\u3001\u624b\u52d5\u3067\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
|
||||
"repo.settings.pages.languages": "\u8a00\u8a9e",
|
||||
"repo.settings.pages.default_lang": "\u30c7\u30d5\u30a9\u30eb\u30c8\u8a00\u8a9e",
|
||||
"repo.settings.pages.default_lang_help": "\u30e9\u30f3\u30c7\u30a3\u30f3\u30b0\u30da\u30fc\u30b8\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u4e3b\u8981\u8a00\u8a9e",
|
||||
"repo.settings.pages.enabled_languages": "\u6709\u52b9\u306a\u8a00\u8a9e",
|
||||
"repo.settings.pages.enabled_languages_help": "\u30e9\u30f3\u30c7\u30a3\u30f3\u30b0\u30da\u30fc\u30b8\u304c\u30b5\u30dd\u30fc\u30c8\u3059\u308b\u8a00\u8a9e\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u8a2a\u554f\u8005\u306b\u306f\u30ca\u30d3\u30b2\u30fc\u30b7\u30e7\u30f3\u306b\u8a00\u8a9e\u5207\u308a\u66ff\u3048\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002",
|
||||
"repo.settings.pages.save_languages": "\u8a00\u8a9e\u8a2d\u5b9a\u3092\u4fdd\u5b58",
|
||||
"repo.settings.pages.languages_saved": "\u8a00\u8a9e\u8a2d\u5b9a\u304c\u6b63\u5e38\u306b\u4fdd\u5b58\u3055\u308c\u307e\u3057\u305f\u3002",
|
||||
"repo.settings.pages.translations": "\u7ffb\u8a33",
|
||||
"repo.settings.pages.ai_translate": "AI\u7ffb\u8a33",
|
||||
"repo.settings.pages.ai_translate_success": "AI\u306b\u3088\u308b\u7ffb\u8a33\u304c\u6b63\u5e38\u306b\u751f\u6210\u3055\u308c\u307e\u3057\u305f\u3002\u5fc5\u8981\u306b\u5fdc\u3058\u3066\u78ba\u8a8d\u30fb\u7de8\u96c6\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "\u524a\u9664",
|
||||
"repo.settings.pages.save_translation": "\u7ffb\u8a33\u3092\u4fdd\u5b58",
|
||||
"repo.settings.pages.translation_saved": "\u7ffb\u8a33\u304c\u6b63\u5e38\u306b\u4fdd\u5b58\u3055\u308c\u307e\u3057\u305f\u3002",
|
||||
"repo.settings.pages.translation_deleted": "\u7ffb\u8a33\u304c\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002",
|
||||
"repo.settings.pages.translation_empty": "\u7ffb\u8a33\u30b3\u30f3\u30c6\u30f3\u30c4\u304c\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002",
|
||||
"repo.settings.pages.trans_headline": "\u898b\u51fa\u3057",
|
||||
"repo.settings.pages.trans_subheadline": "\u30b5\u30d6\u898b\u51fa\u3057",
|
||||
"repo.settings.pages.trans_primary_cta": "\u30d7\u30e9\u30a4\u30de\u30eaCTA\u30e9\u30d9\u30eb",
|
||||
"repo.settings.pages.trans_secondary_cta": "\u30bb\u30ab\u30f3\u30c0\u30eaCTA\u30e9\u30d9\u30eb",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA\u30bb\u30af\u30b7\u30e7\u30f3\u898b\u51fa\u3057",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA\u30bb\u30af\u30b7\u30e7\u30f3\u30b5\u30d6\u898b\u51fa\u3057",
|
||||
"repo.settings.pages.trans_cta_button": "CTA\u30dc\u30bf\u30f3\u30e9\u30d9\u30eb",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -1929,6 +1929,7 @@
|
||||
"admin.dashboard.cleanup_packages": "패키지 정리",
|
||||
"admin.dashboard.cleanup_actions": "작업 정리",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -2159,6 +2160,12 @@
|
||||
"admin.config.explore_org_display_format_help": "조직의 표시 형식",
|
||||
"admin.config.explore_org_format_list": "목록",
|
||||
"admin.config.explore_org_format_tiles": "타일",
|
||||
"admin.config.show_footer_powered_by": "Powered By \ud45c\uc2dc",
|
||||
"admin.config.show_footer_powered_by_desc": "\ud478\ud130\uc5d0 \"Powered by GitCaddy Server\" \uba54\uc2dc\uc9c0 \ud45c\uc2dc",
|
||||
"admin.config.show_footer_licenses": "\ub77c\uc774\uc120\uc2a4 \ub9c1\ud06c \ud45c\uc2dc",
|
||||
"admin.config.show_footer_licenses_desc": "\ud478\ud130\uc5d0 \ub77c\uc774\uc120\uc2a4 \ub9c1\ud06c \ud45c\uc2dc",
|
||||
"admin.config.show_footer_api": "API \ub9c1\ud06c \ud45c\uc2dc",
|
||||
"admin.config.show_footer_api_desc": "\ud478\ud130\uc5d0 API (Swagger) \ub9c1\ud06c \ud45c\uc2dc",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -2207,6 +2214,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "\uacf5\uac1c \ub9b4\ub9ac\uc2a4",
|
||||
"repo.settings.pages.public_releases_desc": "\uc778\uc99d\ub418\uc9c0 \uc54a\uc740 \uc0ac\uc6a9\uc790\uac00 \ub9b4\ub9ac\uc2a4\ub97c \ub2e4\uc6b4\ub85c\ub4dc\ud560 \uc218 \uc788\ub3c4\ub85d \ud5c8\uc6a9\ud569\ub2c8\ub2e4. \ube44\uacf5\uac1c \uc800\uc7a5\uc18c\uc758 \ub79c\ub529 \ud398\uc774\uc9c0\uc5d0 \uc720\uc6a9\ud569\ub2c8\ub2e4.",
|
||||
"repo.settings.pages.brand_favicon_url": "\ud30c\ube44\ucf58 URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "\ub79c\ub529 \ud398\uc774\uc9c0\uc6a9 \uc0ac\uc6a9\uc790 \uc815\uc758 \ud30c\ube44\ucf58 URL (ICO, PNG \ub610\ub294 SVG). \uae30\ubcf8\uac12\uc744 \uc0ac\uc6a9\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694.",
|
||||
"repo.settings.pages.navigation": "\ub0b4\ube44\uac8c\uc774\uc158 \ub9c1\ud06c",
|
||||
"repo.settings.pages.navigation_desc": "\ud5e4\ub354\uc640 \ud478\ud130 \ub0b4\ube44\uac8c\uc774\uc158\uc5d0 \ud45c\uc2dc\ud560 \uae30\ubcf8 \uc81c\uacf5 \ub9c1\ud06c\ub97c \uc81c\uc5b4\ud569\ub2c8\ub2e4.",
|
||||
"repo.settings.pages.nav_show_docs": "Docs \ub9c1\ud06c \ud45c\uc2dc (\uc704\ud0a4\ub85c \ub9c1\ud06c)",
|
||||
"repo.settings.pages.nav_show_api": "API \ub9c1\ud06c \ud45c\uc2dc (Swagger \ubb38\uc11c\ub85c \ub9c1\ud06c)",
|
||||
"repo.settings.pages.nav_show_repository": "\uc800\uc7a5\uc18c \ub9c1\ud06c \ud45c\uc2dc (\uc18c\uc2a4 \ucf54\ub4dc \ubcf4\uae30 \ubc84\ud2bc)",
|
||||
"repo.settings.pages.nav_show_releases": "\ub9b4\ub9ac\uc2a4 \ub9c1\ud06c \ud45c\uc2dc",
|
||||
"repo.settings.pages.nav_show_issues": "\uc774\uc288 \ub9c1\ud06c \ud45c\uc2dc",
|
||||
"repo.settings.pages.blog_section": "\ube14\ub85c\uadf8 \uc139\uc158",
|
||||
"repo.settings.pages.blog_enabled_desc": "\ub79c\ub529 \ud398\uc774\uc9c0\uc5d0 \ucd5c\uadfc \ube14\ub85c\uadf8 \uac8c\uc2dc\ubb3c \ud45c\uc2dc",
|
||||
"repo.settings.pages.blog_headline": "\ube14\ub85c\uadf8 \uc81c\ubaa9",
|
||||
"repo.settings.pages.blog_subheadline": "\ube14\ub85c\uadf8 \ubd80\uc81c\ubaa9",
|
||||
"repo.settings.pages.blog_max_posts": "\ud45c\uc2dc\ud560 \ucd5c\ub300 \uac8c\uc2dc\ubb3c \uc218",
|
||||
"repo.settings.pages.ai_generate": "AI \ucf58\ud150\uce20 \uc0dd\uc131\uae30",
|
||||
"repo.settings.pages.ai_generate_desc": "AI\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc800\uc7a5\uc18c\uc758 README\uc640 \uba54\ud0c0\ub370\uc774\ud130\uc5d0\uc11c \ub79c\ub529 \ud398\uc774\uc9c0 \ucf58\ud150\uce20(\uc81c\ubaa9, \uae30\ub2a5, \ud1b5\uacc4, CTA)\ub97c \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4.",
|
||||
"repo.settings.pages.ai_generate_button": "AI\ub85c \ucf58\ud150\uce20 \uc0dd\uc131",
|
||||
"repo.settings.pages.ai_generate_success": "\ub79c\ub529 \ud398\uc774\uc9c0 \ucf58\ud150\uce20\uac00 \uc131\uacf5\uc801\uc73c\ub85c \uc0dd\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ub2e4\ub978 \ud0ed\uc5d0\uc11c \ud655\uc778\ud558\uace0 \uc0ac\uc6a9\uc790 \uc815\uc758\ud558\uc138\uc694.",
|
||||
"repo.settings.pages.ai_generate_failed": "AI \ucf58\ud150\uce20 \uc0dd\uc131\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud558\uac70\ub098 \uc218\ub3d9\uc73c\ub85c \ucf58\ud150\uce20\ub97c \uad6c\uc131\ud558\uc138\uc694.",
|
||||
"repo.settings.pages.languages": "\uc5b8\uc5b4",
|
||||
"repo.settings.pages.default_lang": "\uae30\ubcf8 \uc5b8\uc5b4",
|
||||
"repo.settings.pages.default_lang_help": "\ub79c\ub529 \ud398\uc774\uc9c0 \ucf58\ud150\uce20\uc758 \uc8fc\uc694 \uc5b8\uc5b4",
|
||||
"repo.settings.pages.enabled_languages": "\ud65c\uc131\ud654\ub41c \uc5b8\uc5b4",
|
||||
"repo.settings.pages.enabled_languages_help": "\ub79c\ub529 \ud398\uc774\uc9c0\uac00 \uc9c0\uc6d0\ud560 \uc5b8\uc5b4\ub97c \uc120\ud0dd\ud558\uc138\uc694. \ubc29\ubb38\uc790\ub294 \ub0b4\ube44\uac8c\uc774\uc158\uc5d0\uc11c \uc5b8\uc5b4 \uc804\ud658\uae30\ub97c \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4.",
|
||||
"repo.settings.pages.save_languages": "\uc5b8\uc5b4 \uc124\uc815 \uc800\uc7a5",
|
||||
"repo.settings.pages.languages_saved": "\uc5b8\uc5b4 \uc124\uc815\uc774 \uc131\uacf5\uc801\uc73c\ub85c \uc800\uc7a5\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
|
||||
"repo.settings.pages.translations": "\ubc88\uc5ed",
|
||||
"repo.settings.pages.ai_translate": "AI \ubc88\uc5ed",
|
||||
"repo.settings.pages.ai_translate_success": "AI\uac00 \ubc88\uc5ed\uc744 \uc131\uacf5\uc801\uc73c\ub85c \uc0dd\uc131\ud588\uc2b5\ub2c8\ub2e4. \ud544\uc694\uc5d0 \ub530\ub77c \ud655\uc778\ud558\uace0 \ud3b8\uc9d1\ud558\uc138\uc694.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "\uc0ad\uc81c",
|
||||
"repo.settings.pages.save_translation": "\ubc88\uc5ed \uc800\uc7a5",
|
||||
"repo.settings.pages.translation_saved": "\ubc88\uc5ed\uc774 \uc131\uacf5\uc801\uc73c\ub85c \uc800\uc7a5\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
|
||||
"repo.settings.pages.translation_deleted": "\ubc88\uc5ed\uc774 \uc0ad\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
|
||||
"repo.settings.pages.translation_empty": "\ubc88\uc5ed \ucf58\ud150\uce20\uac00 \uc81c\uacf5\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.",
|
||||
"repo.settings.pages.trans_headline": "\uc81c\ubaa9",
|
||||
"repo.settings.pages.trans_subheadline": "\ubd80\uc81c\ubaa9",
|
||||
"repo.settings.pages.trans_primary_cta": "\uae30\ubcf8 CTA \ub77c\ubca8",
|
||||
"repo.settings.pages.trans_secondary_cta": "\ubcf4\uc870 CTA \ub77c\ubca8",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA \uc139\uc158 \uc81c\ubaa9",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA \uc139\uc158 \ubd80\uc81c\ubaa9",
|
||||
"repo.settings.pages.trans_cta_button": "CTA \ubc84\ud2bc \ub77c\ubca8",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3482,6 +3482,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.2fa": "2FA",
|
||||
@@ -3670,6 +3671,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "R\u0101d\u012bt Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "R\u0101d\u012bt \"Powered by GitCaddy Server\" zi\u0146ojumu k\u0101jen\u0113",
|
||||
"admin.config.show_footer_licenses": "R\u0101d\u012bt licen\u010du saiti",
|
||||
"admin.config.show_footer_licenses_desc": "R\u0101d\u012bt licen\u010du saiti k\u0101jen\u0113",
|
||||
"admin.config.show_footer_api": "R\u0101d\u012bt API saiti",
|
||||
"admin.config.show_footer_api_desc": "R\u0101d\u012bt API (Swagger) saiti k\u0101jen\u0113",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -3718,6 +3725,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Publisk\u0101s versijas",
|
||||
"repo.settings.pages.public_releases_desc": "\u013baut neautentific\u0113tiem lietot\u0101jiem lejupiel\u0101d\u0113t versijas. Noder\u012bgi priv\u0101tu repozitoriju galvenaj\u0101m lap\u0101m.",
|
||||
"repo.settings.pages.brand_favicon_url": "Favikona URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "Pielāgotas favikonas URL jūsu galvenajā lapā (ICO, PNG vai SVG). Atstājiet tukšu, lai izmantotu noklusējuma.",
|
||||
"repo.settings.pages.navigation": "Navigācijas saites",
|
||||
"repo.settings.pages.navigation_desc": "Kontrolējiet, kuras iebūvētās saites tiek rādītas galvenes un kājenes navigācijā.",
|
||||
"repo.settings.pages.nav_show_docs": "Rādīt Docs saiti (saite uz wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Rādīt API saiti (saite uz Swagger dokumentāciju)",
|
||||
"repo.settings.pages.nav_show_repository": "Rādīt repozitorija saiti (pogu Skatīt pirmkodu)",
|
||||
"repo.settings.pages.nav_show_releases": "Rādīt laidienu saiti",
|
||||
"repo.settings.pages.nav_show_issues": "Rādīt problēmu saiti",
|
||||
"repo.settings.pages.blog_section": "Emuāra sadaļa",
|
||||
"repo.settings.pages.blog_enabled_desc": "Rādīt jaunākos emuāra ierakstus galvenajā lapā",
|
||||
"repo.settings.pages.blog_headline": "Emuāra virsraksts",
|
||||
"repo.settings.pages.blog_subheadline": "Emuāra apakšvirsraksts",
|
||||
"repo.settings.pages.blog_max_posts": "Maksimālais rādāmo ierakstu skaits",
|
||||
"repo.settings.pages.ai_generate": "AI satura ģenerators",
|
||||
"repo.settings.pages.ai_generate_desc": "Automātiski ģenerējiet galvenās lapas saturu (virsrakstu, funkcijas, statistiku, CTA) no jūsu repozitorija README un metadatiem ar AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Ģenerēt saturu ar AI",
|
||||
"repo.settings.pages.ai_generate_success": "Galvenās lapas saturs ir veiksmīgi ģenerēts. Pārskatiet un pielāgojiet citās cilnēs.",
|
||||
"repo.settings.pages.ai_generate_failed": "AI satura ģenerēšana neizdevās. Mēģiniet vēlāk vai konfigurējiet saturu manuāli.",
|
||||
"repo.settings.pages.languages": "Valodas",
|
||||
"repo.settings.pages.default_lang": "Noklusējuma valoda",
|
||||
"repo.settings.pages.default_lang_help": "Galvenās lapas satura pamata valoda",
|
||||
"repo.settings.pages.enabled_languages": "Iespējotas valodas",
|
||||
"repo.settings.pages.enabled_languages_help": "Izvēlieties, kuras valodas jūsu galvenajai lapai jāatbalsta. Apmeklētāji navigācijā redzēs valodu pārslēdzēju.",
|
||||
"repo.settings.pages.save_languages": "Saglabāt valodu iestatījumus",
|
||||
"repo.settings.pages.languages_saved": "Valodu iestatījumi veiksmīgi saglabāti.",
|
||||
"repo.settings.pages.translations": "Tulkojumi",
|
||||
"repo.settings.pages.ai_translate": "AI tulkojums",
|
||||
"repo.settings.pages.ai_translate_success": "Tulkojums veiksmīgi ģenerēts ar AI. Pārskatiet un rediģējiet pēc nepieciešamības.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Dzēst",
|
||||
"repo.settings.pages.save_translation": "Saglabāt tulkojumu",
|
||||
"repo.settings.pages.translation_saved": "Tulkojums veiksmīgi saglabāts.",
|
||||
"repo.settings.pages.translation_deleted": "Tulkojums dzēsts.",
|
||||
"repo.settings.pages.translation_empty": "Nav norādīts tulkojuma saturs.",
|
||||
"repo.settings.pages.trans_headline": "Virsraksts",
|
||||
"repo.settings.pages.trans_subheadline": "Apakšvirsraksts",
|
||||
"repo.settings.pages.trans_primary_cta": "Galvenā CTA etiķete",
|
||||
"repo.settings.pages.trans_secondary_cta": "Sekundārā CTA etiķete",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA sadaļas virsraksts",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA sadaļas apakšvirsraksts",
|
||||
"repo.settings.pages.trans_cta_button": "CTA pogas etiķete",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -2582,6 +2582,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Pakketten opschonen",
|
||||
"admin.dashboard.cleanup_actions": "Acties opschonen",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Verlopen uploads opschonen",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Verwijderen oude acties gestart",
|
||||
"admin.dashboard.gc_lfs": "LFS-garbage collection",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -2818,6 +2819,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Weergaveformaat voor organisaties",
|
||||
"admin.config.explore_org_format_list": "Lijst",
|
||||
"admin.config.explore_org_format_tiles": "Tegels",
|
||||
"admin.config.show_footer_powered_by": "Powered By weergeven",
|
||||
"admin.config.show_footer_powered_by_desc": "Het bericht \"Powered by GitCaddy Server\" weergeven in de voettekst",
|
||||
"admin.config.show_footer_licenses": "Licenties-link weergeven",
|
||||
"admin.config.show_footer_licenses_desc": "De licenties-link weergeven in de voettekst",
|
||||
"admin.config.show_footer_api": "API-link weergeven",
|
||||
"admin.config.show_footer_api_desc": "De API (Swagger)-link weergeven in de voettekst",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -2866,6 +2873,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Openbare releases",
|
||||
"repo.settings.pages.public_releases_desc": "Niet-geauthenticeerde gebruikers toestaan releases te downloaden. Handig voor landingspagina's van priv\u00e9repository's.",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon-URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL naar een aangepast favicon voor uw landingspagina (ICO, PNG of SVG). Laat leeg om de standaard te gebruiken.",
|
||||
"repo.settings.pages.navigation": "Navigatielinks",
|
||||
"repo.settings.pages.navigation_desc": "Bepaal welke ingebouwde links verschijnen in de kop- en voettekstnavigatie.",
|
||||
"repo.settings.pages.nav_show_docs": "Docs-link tonen (linkt naar wiki)",
|
||||
"repo.settings.pages.nav_show_api": "API-link tonen (linkt naar Swagger-documentatie)",
|
||||
"repo.settings.pages.nav_show_repository": "Repository-link tonen (Broncode bekijken-knop)",
|
||||
"repo.settings.pages.nav_show_releases": "Releases-link tonen",
|
||||
"repo.settings.pages.nav_show_issues": "Issues-link tonen",
|
||||
"repo.settings.pages.blog_section": "Blogsectie",
|
||||
"repo.settings.pages.blog_enabled_desc": "Toon recente blogberichten op de landingspagina",
|
||||
"repo.settings.pages.blog_headline": "Blogtitel",
|
||||
"repo.settings.pages.blog_subheadline": "Blog-ondertitel",
|
||||
"repo.settings.pages.blog_max_posts": "Maximaal aantal berichten om te tonen",
|
||||
"repo.settings.pages.ai_generate": "AI-inhoudsgenerator",
|
||||
"repo.settings.pages.ai_generate_desc": "Genereer automatisch landingspagina-inhoud (kop, functies, statistieken, CTA's) uit de README en metadata van uw repository met AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Inhoud genereren met AI",
|
||||
"repo.settings.pages.ai_generate_success": "Landingspagina-inhoud is succesvol gegenereerd. Bekijk en pas aan in de andere tabbladen.",
|
||||
"repo.settings.pages.ai_generate_failed": "AI-generatie mislukt. Probeer het later opnieuw of configureer de inhoud handmatig.",
|
||||
"repo.settings.pages.languages": "Talen",
|
||||
"repo.settings.pages.default_lang": "Standaardtaal",
|
||||
"repo.settings.pages.default_lang_help": "De hoofdtaal van uw landingspagina-inhoud",
|
||||
"repo.settings.pages.enabled_languages": "Ingeschakelde talen",
|
||||
"repo.settings.pages.enabled_languages_help": "Selecteer welke talen uw landingspagina moet ondersteunen. Bezoekers zien een taalwisselaar in de navigatie.",
|
||||
"repo.settings.pages.save_languages": "Taalinstellingen opslaan",
|
||||
"repo.settings.pages.languages_saved": "Taalinstellingen succesvol opgeslagen.",
|
||||
"repo.settings.pages.translations": "Vertalingen",
|
||||
"repo.settings.pages.ai_translate": "AI-vertaling",
|
||||
"repo.settings.pages.ai_translate_success": "Vertaling is succesvol gegenereerd door AI. Controleer en bewerk indien nodig.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Verwijderen",
|
||||
"repo.settings.pages.save_translation": "Vertaling opslaan",
|
||||
"repo.settings.pages.translation_saved": "Vertaling succesvol opgeslagen.",
|
||||
"repo.settings.pages.translation_deleted": "Vertaling verwijderd.",
|
||||
"repo.settings.pages.translation_empty": "Geen vertaalinhoud opgegeven.",
|
||||
"repo.settings.pages.trans_headline": "Kop",
|
||||
"repo.settings.pages.trans_subheadline": "Ondertitel",
|
||||
"repo.settings.pages.trans_primary_cta": "Primaire CTA-label",
|
||||
"repo.settings.pages.trans_secondary_cta": "Secundaire CTA-label",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA-sectietitel",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA-sectie ondertitel",
|
||||
"repo.settings.pages.trans_cta_button": "CTA-knoplabel",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -2559,6 +2559,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Wyczyść pakiety",
|
||||
"admin.dashboard.cleanup_actions": "Wyczyść akcje",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Wyczyść wygasłe przesyłania",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Usuwanie starych akcji rozpoczęte",
|
||||
"admin.dashboard.gc_lfs": "Garbage collection LFS",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -2789,6 +2790,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Format wyświetlania dla organizacji",
|
||||
"admin.config.explore_org_format_list": "Lista",
|
||||
"admin.config.explore_org_format_tiles": "Kafelki",
|
||||
"admin.config.show_footer_powered_by": "Poka\u017c Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Poka\u017c wiadomo\u015b\u0107 \"Powered by GitCaddy Server\" w stopce",
|
||||
"admin.config.show_footer_licenses": "Poka\u017c link do licencji",
|
||||
"admin.config.show_footer_licenses_desc": "Poka\u017c link do licencji w stopce",
|
||||
"admin.config.show_footer_api": "Poka\u017c link do API",
|
||||
"admin.config.show_footer_api_desc": "Poka\u017c link do API (Swagger) w stopce",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -2837,6 +2844,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Publiczne wydania",
|
||||
"repo.settings.pages.public_releases_desc": "Zezw\u00f3l nieuwierzytelnionym u\u017cytkownikom na pobieranie wyda\u0144. Przydatne dla stron docelowych prywatnych repozytori\u00f3w.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL favikony",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL niestandardowej favikony dla strony docelowej (ICO, PNG lub SVG). Pozostaw puste, aby u\u017cy\u0107 domy\u015blnej.",
|
||||
"repo.settings.pages.navigation": "Linki nawigacyjne",
|
||||
"repo.settings.pages.navigation_desc": "Kontroluj, kt\u00f3re wbudowane linki pojawiaj\u0105 si\u0119 w nawigacji nag\u0142\u00f3wka i stopki.",
|
||||
"repo.settings.pages.nav_show_docs": "Poka\u017c link Docs (prowadzi do wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Poka\u017c link API (prowadzi do dokumentacji Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "Poka\u017c link repozytorium (przycisk Zobacz kod \u017ar\u00f3d\u0142owy)",
|
||||
"repo.settings.pages.nav_show_releases": "Poka\u017c link wyda\u0144",
|
||||
"repo.settings.pages.nav_show_issues": "Poka\u017c link zg\u0142osze\u0144",
|
||||
"repo.settings.pages.blog_section": "Sekcja bloga",
|
||||
"repo.settings.pages.blog_enabled_desc": "Poka\u017c ostatnie wpisy bloga na stronie docelowej",
|
||||
"repo.settings.pages.blog_headline": "Nag\u0142\u00f3wek bloga",
|
||||
"repo.settings.pages.blog_subheadline": "Podnag\u0142\u00f3wek bloga",
|
||||
"repo.settings.pages.blog_max_posts": "Maksymalna liczba wpis\u00f3w do wy\u015bwietlenia",
|
||||
"repo.settings.pages.ai_generate": "Generator tre\u015bci AI",
|
||||
"repo.settings.pages.ai_generate_desc": "Automatycznie generuj tre\u015bci strony docelowej (nag\u0142\u00f3wek, funkcje, statystyki, CTA) z README i metadanych repozytorium za pomoc\u0105 AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Wygeneruj tre\u015b\u0107 za pomoc\u0105 AI",
|
||||
"repo.settings.pages.ai_generate_success": "Tre\u015b\u0107 strony docelowej zosta\u0142a wygenerowana pomy\u015blnie. Sprawd\u017a i dostosuj j\u0105 w innych zak\u0142adkach.",
|
||||
"repo.settings.pages.ai_generate_failed": "Generowanie tre\u015bci przez AI nie powiod\u0142o si\u0119. Spr\u00f3buj ponownie p\u00f3\u017aniej lub skonfiguruj tre\u015b\u0107 r\u0119cznie.",
|
||||
"repo.settings.pages.languages": "J\u0119zyki",
|
||||
"repo.settings.pages.default_lang": "Domy\u015blny j\u0119zyk",
|
||||
"repo.settings.pages.default_lang_help": "G\u0142\u00f3wny j\u0119zyk tre\u015bci strony docelowej",
|
||||
"repo.settings.pages.enabled_languages": "W\u0142\u0105czone j\u0119zyki",
|
||||
"repo.settings.pages.enabled_languages_help": "Wybierz, kt\u00f3re j\u0119zyki ma obs\u0142ugiwa\u0107 strona docelowa. Odwiedzaj\u0105cy zobacz\u0105 prze\u0142\u0105cznik j\u0119zyk\u00f3w w nawigacji.",
|
||||
"repo.settings.pages.save_languages": "Zapisz ustawienia j\u0119zykowe",
|
||||
"repo.settings.pages.languages_saved": "Ustawienia j\u0119zykowe zapisane pomy\u015blnie.",
|
||||
"repo.settings.pages.translations": "T\u0142umaczenia",
|
||||
"repo.settings.pages.ai_translate": "T\u0142umaczenie AI",
|
||||
"repo.settings.pages.ai_translate_success": "T\u0142umaczenie zosta\u0142o wygenerowane przez AI. Sprawd\u017a i edytuj w razie potrzeby.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Usu\u0144",
|
||||
"repo.settings.pages.save_translation": "Zapisz t\u0142umaczenie",
|
||||
"repo.settings.pages.translation_saved": "T\u0142umaczenie zapisane pomy\u015blnie.",
|
||||
"repo.settings.pages.translation_deleted": "T\u0142umaczenie usuni\u0119te.",
|
||||
"repo.settings.pages.translation_empty": "Nie podano tre\u015bci t\u0142umaczenia.",
|
||||
"repo.settings.pages.trans_headline": "Nag\u0142\u00f3wek",
|
||||
"repo.settings.pages.trans_subheadline": "Podnag\u0142\u00f3wek",
|
||||
"repo.settings.pages.trans_primary_cta": "Etykieta g\u0142\u00f3wnego CTA",
|
||||
"repo.settings.pages.trans_secondary_cta": "Etykieta drugorz\u0119dnego CTA",
|
||||
"repo.settings.pages.trans_cta_headline": "Nag\u0142\u00f3wek sekcji CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Podnag\u0142\u00f3wek sekcji CTA",
|
||||
"repo.settings.pages.trans_cta_button": "Etykieta przycisku CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3768,6 +3768,7 @@
|
||||
"admin.dashboard.resync_all_sshprincipals": "Update the '.ssh/authorized_principals' file with GitCaddy SSH principals",
|
||||
"admin.dashboard.resync_all_hooks": "Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...)",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -3942,6 +3943,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Formato de exibição para organizações",
|
||||
"admin.config.explore_org_format_list": "Lista",
|
||||
"admin.config.explore_org_format_tiles": "Blocos",
|
||||
"admin.config.show_footer_powered_by": "Mostrar Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Mostrar a mensagem \"Powered by GitCaddy Server\" no rodap\u00e9",
|
||||
"admin.config.show_footer_licenses": "Mostrar link de licen\u00e7as",
|
||||
"admin.config.show_footer_licenses_desc": "Mostrar o link de licen\u00e7as no rodap\u00e9",
|
||||
"admin.config.show_footer_api": "Mostrar link da API",
|
||||
"admin.config.show_footer_api_desc": "Mostrar o link da API (Swagger) no rodap\u00e9",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -3990,6 +3997,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Releases p\u00fablicos",
|
||||
"repo.settings.pages.public_releases_desc": "Permitir que usu\u00e1rios n\u00e3o autenticados baixem releases. \u00datil para p\u00e1ginas de destino de reposit\u00f3rios privados.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL do Favicon",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL de um favicon personalizado para sua landing page (ICO, PNG ou SVG). Deixe em branco para usar o padr\u00e3o.",
|
||||
"repo.settings.pages.navigation": "Links de navega\u00e7\u00e3o",
|
||||
"repo.settings.pages.navigation_desc": "Controle quais links integrados aparecem na navega\u00e7\u00e3o do cabe\u00e7alho e rodap\u00e9.",
|
||||
"repo.settings.pages.nav_show_docs": "Mostrar link de Docs (link para o wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Mostrar link de API (link para a documenta\u00e7\u00e3o Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "Mostrar link do Reposit\u00f3rio (bot\u00e3o Ver c\u00f3digo fonte)",
|
||||
"repo.settings.pages.nav_show_releases": "Mostrar link de Releases",
|
||||
"repo.settings.pages.nav_show_issues": "Mostrar link de Issues",
|
||||
"repo.settings.pages.blog_section": "Se\u00e7\u00e3o do Blog",
|
||||
"repo.settings.pages.blog_enabled_desc": "Mostrar postagens recentes do blog na landing page",
|
||||
"repo.settings.pages.blog_headline": "T\u00edtulo do Blog",
|
||||
"repo.settings.pages.blog_subheadline": "Subt\u00edtulo do Blog",
|
||||
"repo.settings.pages.blog_max_posts": "N\u00famero m\u00e1ximo de postagens a exibir",
|
||||
"repo.settings.pages.ai_generate": "Gerador de conte\u00fado com IA",
|
||||
"repo.settings.pages.ai_generate_desc": "Gere automaticamente o conte\u00fado da landing page (t\u00edtulo, recursos, estat\u00edsticas, CTAs) a partir do README e dos metadados do seu reposit\u00f3rio usando IA.",
|
||||
"repo.settings.pages.ai_generate_button": "Gerar conte\u00fado com IA",
|
||||
"repo.settings.pages.ai_generate_success": "O conte\u00fado da landing page foi gerado com sucesso. Revise e personalize nas outras abas.",
|
||||
"repo.settings.pages.ai_generate_failed": "Falha ao gerar conte\u00fado com IA. Tente novamente mais tarde ou configure o conte\u00fado manualmente.",
|
||||
"repo.settings.pages.languages": "Idiomas",
|
||||
"repo.settings.pages.default_lang": "Idioma padr\u00e3o",
|
||||
"repo.settings.pages.default_lang_help": "O idioma principal do conte\u00fado da sua landing page",
|
||||
"repo.settings.pages.enabled_languages": "Idiomas habilitados",
|
||||
"repo.settings.pages.enabled_languages_help": "Selecione quais idiomas sua landing page deve suportar. Os visitantes ver\u00e3o um seletor de idioma na navega\u00e7\u00e3o.",
|
||||
"repo.settings.pages.save_languages": "Salvar configura\u00e7\u00f5es de idioma",
|
||||
"repo.settings.pages.languages_saved": "Configura\u00e7\u00f5es de idioma salvas com sucesso.",
|
||||
"repo.settings.pages.translations": "Tradu\u00e7\u00f5es",
|
||||
"repo.settings.pages.ai_translate": "Tradu\u00e7\u00e3o com IA",
|
||||
"repo.settings.pages.ai_translate_success": "A tradu\u00e7\u00e3o foi gerada com sucesso pela IA. Revise e edite conforme necess\u00e1rio.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Excluir",
|
||||
"repo.settings.pages.save_translation": "Salvar tradu\u00e7\u00e3o",
|
||||
"repo.settings.pages.translation_saved": "Tradu\u00e7\u00e3o salva com sucesso.",
|
||||
"repo.settings.pages.translation_deleted": "Tradu\u00e7\u00e3o exclu\u00edda.",
|
||||
"repo.settings.pages.translation_empty": "Nenhum conte\u00fado de tradu\u00e7\u00e3o fornecido.",
|
||||
"repo.settings.pages.trans_headline": "T\u00edtulo",
|
||||
"repo.settings.pages.trans_subheadline": "Subt\u00edtulo",
|
||||
"repo.settings.pages.trans_primary_cta": "R\u00f3tulo do CTA principal",
|
||||
"repo.settings.pages.trans_secondary_cta": "R\u00f3tulo do CTA secund\u00e1rio",
|
||||
"repo.settings.pages.trans_cta_headline": "T\u00edtulo da se\u00e7\u00e3o CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Subt\u00edtulo da se\u00e7\u00e3o CTA",
|
||||
"repo.settings.pages.trans_cta_button": "R\u00f3tulo do bot\u00e3o CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3909,6 +3909,7 @@
|
||||
"org.settings.pin_to_homepage": "Pin this organization to the homepage",
|
||||
"org.settings.pin_to_homepage_help": "When enabled, this organization will be featured on the public homepage. Only administrators can change this setting.",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.auths.ssh_keys_are_verified": "SSH keys in LDAP are considered as verified",
|
||||
"actions.runners.status.unhealthy": "Não saudável",
|
||||
"actions.runners.capabilities": "Capacidades",
|
||||
@@ -4031,6 +4032,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Formato de exibição para organizações",
|
||||
"admin.config.explore_org_format_list": "Lista",
|
||||
"admin.config.explore_org_format_tiles": "Blocos",
|
||||
"admin.config.show_footer_powered_by": "Mostrar Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Mostrar a mensagem \"Powered by GitCaddy Server\" no rodap\u00e9",
|
||||
"admin.config.show_footer_licenses": "Mostrar liga\u00e7\u00e3o de licen\u00e7as",
|
||||
"admin.config.show_footer_licenses_desc": "Mostrar a liga\u00e7\u00e3o de licen\u00e7as no rodap\u00e9",
|
||||
"admin.config.show_footer_api": "Mostrar liga\u00e7\u00e3o da API",
|
||||
"admin.config.show_footer_api_desc": "Mostrar a liga\u00e7\u00e3o da API (Swagger) no rodap\u00e9",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -4079,6 +4086,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Lan\u00e7amentos p\u00fablicos",
|
||||
"repo.settings.pages.public_releases_desc": "Permitir que utilizadores n\u00e3o autenticados descarreguem lan\u00e7amentos. \u00datil para p\u00e1ginas de destino de reposit\u00f3rios privados.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL do Favicon",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL de um favicon personalizado para a sua p\u00e1gina de destino (ICO, PNG ou SVG). Deixe em branco para usar o predefinido.",
|
||||
"repo.settings.pages.navigation": "Liga\u00e7\u00f5es de navega\u00e7\u00e3o",
|
||||
"repo.settings.pages.navigation_desc": "Controle quais liga\u00e7\u00f5es integradas aparecem na navega\u00e7\u00e3o do cabe\u00e7alho e rodap\u00e9.",
|
||||
"repo.settings.pages.nav_show_docs": "Mostrar liga\u00e7\u00e3o Docs (liga ao wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Mostrar liga\u00e7\u00e3o API (liga \u00e0 documenta\u00e7\u00e3o Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "Mostrar liga\u00e7\u00e3o do Reposit\u00f3rio (bot\u00e3o Ver c\u00f3digo fonte)",
|
||||
"repo.settings.pages.nav_show_releases": "Mostrar liga\u00e7\u00e3o de Releases",
|
||||
"repo.settings.pages.nav_show_issues": "Mostrar liga\u00e7\u00e3o de Issues",
|
||||
"repo.settings.pages.blog_section": "Sec\u00e7\u00e3o do Blog",
|
||||
"repo.settings.pages.blog_enabled_desc": "Mostrar publica\u00e7\u00f5es recentes do blog na p\u00e1gina de destino",
|
||||
"repo.settings.pages.blog_headline": "T\u00edtulo do Blog",
|
||||
"repo.settings.pages.blog_subheadline": "Subt\u00edtulo do Blog",
|
||||
"repo.settings.pages.blog_max_posts": "N\u00famero m\u00e1ximo de publica\u00e7\u00f5es a mostrar",
|
||||
"repo.settings.pages.ai_generate": "Gerador de conte\u00fado com IA",
|
||||
"repo.settings.pages.ai_generate_desc": "Gere automaticamente o conte\u00fado da p\u00e1gina de destino (t\u00edtulo, funcionalidades, estat\u00edsticas, CTAs) a partir do README e dos metadados do seu reposit\u00f3rio usando IA.",
|
||||
"repo.settings.pages.ai_generate_button": "Gerar conte\u00fado com IA",
|
||||
"repo.settings.pages.ai_generate_success": "O conte\u00fado da p\u00e1gina de destino foi gerado com sucesso. Reveja e personalize nos outros separadores.",
|
||||
"repo.settings.pages.ai_generate_failed": "Falha ao gerar conte\u00fado com IA. Tente novamente mais tarde ou configure o conte\u00fado manualmente.",
|
||||
"repo.settings.pages.languages": "Idiomas",
|
||||
"repo.settings.pages.default_lang": "Idioma predefinido",
|
||||
"repo.settings.pages.default_lang_help": "O idioma principal do conte\u00fado da sua p\u00e1gina de destino",
|
||||
"repo.settings.pages.enabled_languages": "Idiomas ativados",
|
||||
"repo.settings.pages.enabled_languages_help": "Selecione quais idiomas a sua p\u00e1gina de destino deve suportar. Os visitantes ver\u00e3o um seletor de idioma na navega\u00e7\u00e3o.",
|
||||
"repo.settings.pages.save_languages": "Guardar defini\u00e7\u00f5es de idioma",
|
||||
"repo.settings.pages.languages_saved": "Defini\u00e7\u00f5es de idioma guardadas com sucesso.",
|
||||
"repo.settings.pages.translations": "Tradu\u00e7\u00f5es",
|
||||
"repo.settings.pages.ai_translate": "Tradu\u00e7\u00e3o com IA",
|
||||
"repo.settings.pages.ai_translate_success": "A tradu\u00e7\u00e3o foi gerada com sucesso pela IA. Reveja e edite conforme necess\u00e1rio.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Eliminar",
|
||||
"repo.settings.pages.save_translation": "Guardar tradu\u00e7\u00e3o",
|
||||
"repo.settings.pages.translation_saved": "Tradu\u00e7\u00e3o guardada com sucesso.",
|
||||
"repo.settings.pages.translation_deleted": "Tradu\u00e7\u00e3o eliminada.",
|
||||
"repo.settings.pages.translation_empty": "Nenhum conte\u00fado de tradu\u00e7\u00e3o fornecido.",
|
||||
"repo.settings.pages.trans_headline": "T\u00edtulo",
|
||||
"repo.settings.pages.trans_subheadline": "Subt\u00edtulo",
|
||||
"repo.settings.pages.trans_primary_cta": "Etiqueta do CTA principal",
|
||||
"repo.settings.pages.trans_secondary_cta": "Etiqueta do CTA secund\u00e1rio",
|
||||
"repo.settings.pages.trans_cta_headline": "T\u00edtulo da sec\u00e7\u00e3o CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Subt\u00edtulo da sec\u00e7\u00e3o CTA",
|
||||
"repo.settings.pages.trans_cta_button": "Etiqueta do bot\u00e3o CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3574,6 +3574,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Очистить пакеты",
|
||||
"admin.dashboard.cleanup_actions": "Очистить действия",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.remote": "Remote",
|
||||
@@ -3763,6 +3764,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Формат отображения для организаций",
|
||||
"admin.config.explore_org_format_list": "Список",
|
||||
"admin.config.explore_org_format_tiles": "Плитки",
|
||||
"admin.config.show_footer_powered_by": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \"Powered by GitCaddy Server\" \u0432 \u043f\u043e\u0434\u0432\u0430\u043b\u0435",
|
||||
"admin.config.show_footer_licenses": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 \u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0438",
|
||||
"admin.config.show_footer_licenses_desc": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 \u043b\u0438\u0446\u0435\u043d\u0437\u0438\u0438 \u0432 \u043f\u043e\u0434\u0432\u0430\u043b\u0435",
|
||||
"admin.config.show_footer_api": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 API",
|
||||
"admin.config.show_footer_api_desc": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 API (Swagger) \u0432 \u043f\u043e\u0434\u0432\u0430\u043b\u0435",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -3811,6 +3818,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "\u041f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u0440\u0435\u043b\u0438\u0437\u044b",
|
||||
"repo.settings.pages.public_releases_desc": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043d\u0435\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0441\u043a\u0430\u0447\u0438\u0432\u0430\u0442\u044c \u0440\u0435\u043b\u0438\u0437\u044b. \u041f\u043e\u043b\u0435\u0437\u043d\u043e \u0434\u043b\u044f \u0446\u0435\u043b\u0435\u0432\u044b\u0445 \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u044b\u0445 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0435\u0432.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL \u0444\u0430\u0432\u0438\u043a\u043e\u043d\u0430",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0444\u0430\u0432\u0438\u043a\u043e\u043d\u0430 \u0434\u043b\u044f \u0432\u0430\u0448\u0435\u0439 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b (ICO, PNG \u0438\u043b\u0438 SVG). \u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e.",
|
||||
"repo.settings.pages.navigation": "\u0421\u0441\u044b\u043b\u043a\u0438 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438",
|
||||
"repo.settings.pages.navigation_desc": "\u0423\u043f\u0440\u0430\u0432\u043b\u044f\u0439\u0442\u0435 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u043c\u0438 \u0441\u0441\u044b\u043b\u043a\u0430\u043c\u0438, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u043c\u0438 \u0432 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0448\u0430\u043f\u043a\u0438 \u0438 \u043f\u043e\u0434\u0432\u0430\u043b\u0430.",
|
||||
"repo.settings.pages.nav_show_docs": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e (\u0432\u0435\u0434\u0451\u0442 \u043d\u0430 \u0432\u0438\u043a\u0438)",
|
||||
"repo.settings.pages.nav_show_api": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 API (\u0432\u0435\u0434\u0451\u0442 \u043d\u0430 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0439 (\u043a\u043d\u043e\u043f\u043a\u0430 \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430)",
|
||||
"repo.settings.pages.nav_show_releases": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 \u0440\u0435\u043b\u0438\u0437\u044b",
|
||||
"repo.settings.pages.nav_show_issues": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 \u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0438",
|
||||
"repo.settings.pages.blog_section": "\u0420\u0430\u0437\u0434\u0435\u043b \u0431\u043b\u043e\u0433\u0430",
|
||||
"repo.settings.pages.blog_enabled_desc": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 \u0431\u043b\u043e\u0433\u0430 \u043d\u0430 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435",
|
||||
"repo.settings.pages.blog_headline": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0431\u043b\u043e\u0433\u0430",
|
||||
"repo.settings.pages.blog_subheadline": "\u041f\u043e\u0434\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0431\u043b\u043e\u0433\u0430",
|
||||
"repo.settings.pages.blog_max_posts": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0434\u043b\u044f \u043f\u043e\u043a\u0430\u0437\u0430",
|
||||
"repo.settings.pages.ai_generate": "\u0413\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440 \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0418\u0418",
|
||||
"repo.settings.pages.ai_generate_desc": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0439\u0442\u0435 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b (\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a, \u0444\u0443\u043d\u043a\u0446\u0438\u0438, \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443, CTA) \u0438\u0437 README \u0438 \u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0445 \u0432\u0430\u0448\u0435\u0433\u043e \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0418\u0418.",
|
||||
"repo.settings.pages.ai_generate_button": "\u0421\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0418\u0418",
|
||||
"repo.settings.pages.ai_generate_success": "\u041a\u043e\u043d\u0442\u0435\u043d\u0442 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u043d. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0433\u043e \u043d\u0430 \u0434\u0440\u0443\u0433\u0438\u0445 \u0432\u043a\u043b\u0430\u0434\u043a\u0430\u0445.",
|
||||
"repo.settings.pages.ai_generate_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0418\u0418. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u043e\u0437\u0436\u0435 \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.",
|
||||
"repo.settings.pages.languages": "\u042f\u0437\u044b\u043a\u0438",
|
||||
"repo.settings.pages.default_lang": "\u042f\u0437\u044b\u043a \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e",
|
||||
"repo.settings.pages.default_lang_help": "\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u044f\u0437\u044b\u043a \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0430 \u0432\u0430\u0448\u0435\u0439 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b",
|
||||
"repo.settings.pages.enabled_languages": "\u0412\u043a\u043b\u044e\u0447\u0451\u043d\u043d\u044b\u0435 \u044f\u0437\u044b\u043a\u0438",
|
||||
"repo.settings.pages.enabled_languages_help": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u044f\u0437\u044b\u043a\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0434\u043e\u043b\u0436\u043d\u0430 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0430\u0448\u0430 \u0446\u0435\u043b\u0435\u0432\u0430\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430. \u041f\u043e\u0441\u0435\u0442\u0438\u0442\u0435\u043b\u0438 \u0443\u0432\u0438\u0434\u044f\u0442 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u044f\u0437\u044b\u043a\u043e\u0432 \u0432 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438.",
|
||||
"repo.settings.pages.save_languages": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u044f\u0437\u044b\u043a\u043e\u0432\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438",
|
||||
"repo.settings.pages.languages_saved": "\u042f\u0437\u044b\u043a\u043e\u0432\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b.",
|
||||
"repo.settings.pages.translations": "\u041f\u0435\u0440\u0435\u0432\u043e\u0434\u044b",
|
||||
"repo.settings.pages.ai_translate": "\u0418\u0418-\u043f\u0435\u0440\u0435\u0432\u043e\u0434",
|
||||
"repo.settings.pages.ai_translate_success": "\u041f\u0435\u0440\u0435\u0432\u043e\u0434 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u043d \u0418\u0418. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0438 \u043e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c",
|
||||
"repo.settings.pages.save_translation": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u043f\u0435\u0440\u0435\u0432\u043e\u0434",
|
||||
"repo.settings.pages.translation_saved": "\u041f\u0435\u0440\u0435\u0432\u043e\u0434 \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d.",
|
||||
"repo.settings.pages.translation_deleted": "\u041f\u0435\u0440\u0435\u0432\u043e\u0434 \u0443\u0434\u0430\u043b\u0451\u043d.",
|
||||
"repo.settings.pages.translation_empty": "\u0421\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0430 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043e.",
|
||||
"repo.settings.pages.trans_headline": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
|
||||
"repo.settings.pages.trans_subheadline": "\u041f\u043e\u0434\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
|
||||
"repo.settings.pages.trans_primary_cta": "\u041c\u0435\u0442\u043a\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e CTA",
|
||||
"repo.settings.pages.trans_secondary_cta": "\u041c\u0435\u0442\u043a\u0430 \u0432\u0442\u043e\u0440\u0438\u0447\u043d\u043e\u0433\u043e CTA",
|
||||
"repo.settings.pages.trans_cta_headline": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0440\u0430\u0437\u0434\u0435\u043b\u0430 CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "\u041f\u043e\u0434\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0440\u0430\u0437\u0434\u0435\u043b\u0430 CTA",
|
||||
"repo.settings.pages.trans_cta_button": "\u041c\u0435\u0442\u043a\u0430 \u043a\u043d\u043e\u043f\u043a\u0438 CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -2613,6 +2613,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -2835,6 +2836,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "Powered By \u0db4\u0dd9\u0db1\u0dca\u0dc0\u0db1\u0dca\u0db1",
|
||||
"admin.config.show_footer_powered_by_desc": "\u0db4\u0dcf\u0daf\u0dba\u0dda \"Powered by GitCaddy Server\" \u0db4\u0dab\u0dd2\u0dc0\u0dd2\u0da9\u0dba \u0db4\u0dd9\u0db1\u0dca\u0dc0\u0db1\u0dca\u0db1",
|
||||
"admin.config.show_footer_licenses": "\u0db6\u0dbd\u0db4\u0dad\u0dca\u200d\u0dbb \u0dc3\u0db6\u0dd0\u0db3\u0dd2\u0dba \u0db4\u0dd9\u0db1\u0dca\u0dc0\u0db1\u0dca\u0db1",
|
||||
"admin.config.show_footer_licenses_desc": "\u0db4\u0dcf\u0daf\u0dba\u0dda \u0db6\u0dbd\u0db4\u0dad\u0dca\u200d\u0dbb \u0dc3\u0db6\u0dd0\u0db3\u0dd2\u0dba \u0db4\u0dd9\u0db1\u0dca\u0dc0\u0db1\u0dca\u0db1",
|
||||
"admin.config.show_footer_api": "API \u0dc3\u0db6\u0dd0\u0db3\u0dd2\u0dba \u0db4\u0dd9\u0db1\u0dca\u0dc0\u0db1\u0dca\u0db1",
|
||||
"admin.config.show_footer_api_desc": "\u0db4\u0dcf\u0daf\u0dba\u0dda API (Swagger) \u0dc3\u0db6\u0dd0\u0db3\u0dd2\u0dba \u0db4\u0dd9\u0db1\u0dca\u0dc0\u0db1\u0dca\u0db1",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -2883,6 +2890,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "\u0db4\u0ddc\u0daf\u0dd4 \u0db1\u0dd2\u0d9a\u0dd4\u0dad\u0dd4",
|
||||
"repo.settings.pages.public_releases_desc": "\u0d85\u0db1\u0dc0\u0dc3\u0dbb \u0db4\u0dbb\u0dd2\u0dc1\u0dd3\u0dbd\u0d9a\u0dba\u0dd2\u0db1\u0dca\u0da7 \u0db1\u0dd2\u0d9a\u0dd4\u0dad\u0dd4 \u0db6\u0dcf\u0d9c\u0dad \u0d9a\u0dd2\u0dbb\u0dd3\u0db8\u0da7 \u0d89\u0da9 \u0daf\u0dd9\u0db1\u0dca\u0db1. \u0db4\u0dd4\u0daf\u0dca\u0d9c\u0dbd\u0dd2\u0d9a \u0d9a\u0db6\u0da9\u0dcf\u0dc0\u0dbd \u0d9c\u0ddc\u0da9\u0db1\u0dd0\u0db8\u0dca \u0db4\u0dd2\u0da7\u0dd4 \u0dc3\u0db3\u0dc4\u0dcf \u0db4\u0dca\u200d\u0dbb\u0dba\u0ddd\u0da2\u0db1\u0dc0\u0dad\u0dca \u0dc0\u0dda.",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "ඔබගේ ගොඩබෑමේ පිටුව සඳහා අභිරුචි favicon URL (ICO, PNG, හෝ SVG). පෙරනිමිය භාවිත කිරීමට හිස්ව තබන්න.",
|
||||
"repo.settings.pages.navigation": "සංචාලන සබැඳි",
|
||||
"repo.settings.pages.navigation_desc": "ශීර්ෂකය සහ පාදකය සංචාලනයේ පෙන්වන ගොඩනැගිච්ච සබැඳි පාලනය කරන්න.",
|
||||
"repo.settings.pages.nav_show_docs": "Docs සබැඳිය පෙන්වන්න (wiki වෙත සබැඳිය)",
|
||||
"repo.settings.pages.nav_show_api": "API සබැඳිය පෙන්වන්න (Swagger ලේඛන වෙත සබැඳිය)",
|
||||
"repo.settings.pages.nav_show_repository": "ගබඩාව සබැඳිය පෙන්වන්න (මූලාශ්ර කේතය බලන්න බොත්තම)",
|
||||
"repo.settings.pages.nav_show_releases": "නිකුතු සබැඳිය පෙන්වන්න",
|
||||
"repo.settings.pages.nav_show_issues": "ගැටලු සබැඳිය පෙන්වන්න",
|
||||
"repo.settings.pages.blog_section": "බ්ලොග් අංශය",
|
||||
"repo.settings.pages.blog_enabled_desc": "ගොඩබෑමේ පිටුවේ මෑත බ්ලොග් සටහන් පෙන්වන්න",
|
||||
"repo.settings.pages.blog_headline": "බ්ලොග් මාතෘකාව",
|
||||
"repo.settings.pages.blog_subheadline": "බ්ලොග් උපමාතෘකාව",
|
||||
"repo.settings.pages.blog_max_posts": "පෙන්විය යුතු උපරිම සටහන් ගණන",
|
||||
"repo.settings.pages.ai_generate": "AI අන්තර්ගත උත්පාදකය",
|
||||
"repo.settings.pages.ai_generate_desc": "AI භාවිතයෙන් ඔබගේ ගබඩාවේ README සහ පාර-දත්ත වලින් ගොඩබෑමේ පිටුවේ අන්තර්ගතය (මාතෘකාව, විශේෂාංග, සංඛ්යාලේඛන, CTA) ස්වයංක්රීයව උත්පාදනය කරන්න.",
|
||||
"repo.settings.pages.ai_generate_button": "AI සමඟ අන්තර්ගතය උත්පාදනය කරන්න",
|
||||
"repo.settings.pages.ai_generate_success": "ගොඩබෑමේ පිටුවේ අන්තර්ගතය සාර්ථකව උත්පාදනය කරන ලදී. අනෙක් ටැබ් වල සමාලෝචනය කර අභිරුචිකරණය කරන්න.",
|
||||
"repo.settings.pages.ai_generate_failed": "AI අන්තර්ගත උත්පාදනය අසාර්ථක විය. පසුව නැවත උත්සාහ කරන්න හෝ අන්තර්ගතය අතින් වින්යාස කරන්න.",
|
||||
"repo.settings.pages.languages": "භාෂා",
|
||||
"repo.settings.pages.default_lang": "පෙරනිමි භාෂාව",
|
||||
"repo.settings.pages.default_lang_help": "ඔබගේ ගොඩබෑමේ පිටුවේ අන්තර්ගතයේ ප්රධාන භාෂාව",
|
||||
"repo.settings.pages.enabled_languages": "සක්රිය භාෂා",
|
||||
"repo.settings.pages.enabled_languages_help": "ඔබගේ ගොඩබෑමේ පිටුව සහාය දිය යුතු භාෂා තෝරන්න. නරඹන්නන්ට සංචාලනයේ භාෂා මාරුකය පෙනෙනු ඇත.",
|
||||
"repo.settings.pages.save_languages": "භාෂා සැකසුම් සුරකින්න",
|
||||
"repo.settings.pages.languages_saved": "භාෂා සැකසුම් සාර්ථකව සුරැකිණි.",
|
||||
"repo.settings.pages.translations": "පරිවර්තන",
|
||||
"repo.settings.pages.ai_translate": "AI පරිවර්තනය",
|
||||
"repo.settings.pages.ai_translate_success": "AI විසින් පරිවර්තනය සාර්ථකව උත්පාදනය කරන ලදී. අවශ්ය පරිදි සමාලෝචනය කර සංස්කරණය කරන්න.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "මකන්න",
|
||||
"repo.settings.pages.save_translation": "පරිවර්තනය සුරකින්න",
|
||||
"repo.settings.pages.translation_saved": "පරිවර්තනය සාර්ථකව සුරැකිණි.",
|
||||
"repo.settings.pages.translation_deleted": "පරිවර්තනය මකා දමන ලදී.",
|
||||
"repo.settings.pages.translation_empty": "පරිවර්තන අන්තර්ගතයක් ලබා දී නැත.",
|
||||
"repo.settings.pages.trans_headline": "මාතෘකාව",
|
||||
"repo.settings.pages.trans_subheadline": "උපමාතෘකාව",
|
||||
"repo.settings.pages.trans_primary_cta": "ප්රාථමික CTA ලේබලය",
|
||||
"repo.settings.pages.trans_secondary_cta": "ද්විතීයික CTA ලේබලය",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA අංශ මාතෘකාව",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA අංශ උපමාතෘකාව",
|
||||
"repo.settings.pages.trans_cta_button": "CTA බොත්තම් ලේබලය",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -1624,6 +1624,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -1861,6 +1862,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "Zobrazi\u0165 Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Zobrazi\u0165 spr\u00e1vu \"Powered by GitCaddy Server\" v p\u00e4ti\u010dke",
|
||||
"admin.config.show_footer_licenses": "Zobrazi\u0165 odkaz na licencie",
|
||||
"admin.config.show_footer_licenses_desc": "Zobrazi\u0165 odkaz na licencie v p\u00e4ti\u010dke",
|
||||
"admin.config.show_footer_api": "Zobrazi\u0165 odkaz na API",
|
||||
"admin.config.show_footer_api_desc": "Zobrazi\u0165 odkaz na API (Swagger) v p\u00e4ti\u010dke",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -1909,6 +1916,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Verejn\u00e9 vydania",
|
||||
"repo.settings.pages.public_releases_desc": "Umo\u017eni\u0165 neautentifikovan\u00fdm pou\u017e\u00edvate\u013eom s\u0165ahova\u0165 vydania. U\u017eito\u010dn\u00e9 pre vstupn\u00e9 str\u00e1nky s\u00fakromn\u00fdch repozit\u00e1rov.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL favikony",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL vlastnej favikony pre va\u0161u cie\u013eov\u00fa str\u00e1nku (ICO, PNG alebo SVG). Nechajte pr\u00e1zdne pre pou\u017eitie predvolenej.",
|
||||
"repo.settings.pages.navigation": "Naviga\u010dn\u00e9 odkazy",
|
||||
"repo.settings.pages.navigation_desc": "Ovl\u00e1dajte, ktor\u00e9 vstavan\u00e9 odkazy sa zobrazia v navig\u00e1cii hlavi\u010dky a p\u00e4ty.",
|
||||
"repo.settings.pages.nav_show_docs": "Zobrazi\u0165 odkaz na Docs (odkaz na wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Zobrazi\u0165 odkaz na API (odkaz na Swagger dokument\u00e1ciu)",
|
||||
"repo.settings.pages.nav_show_repository": "Zobrazi\u0165 odkaz na repozit\u00e1r (tla\u010didlo Zobrazi\u0165 zdrojov\u00fd k\u00f3d)",
|
||||
"repo.settings.pages.nav_show_releases": "Zobrazi\u0165 odkaz na vydania",
|
||||
"repo.settings.pages.nav_show_issues": "Zobrazi\u0165 odkaz na probl\u00e9my",
|
||||
"repo.settings.pages.blog_section": "Sekcia blogu",
|
||||
"repo.settings.pages.blog_enabled_desc": "Zobrazi\u0165 ned\u00e1vne pr\u00edspevky blogu na cie\u013eovej str\u00e1nke",
|
||||
"repo.settings.pages.blog_headline": "Nadpis blogu",
|
||||
"repo.settings.pages.blog_subheadline": "Podnadpis blogu",
|
||||
"repo.settings.pages.blog_max_posts": "Maxim\u00e1lny po\u010det pr\u00edspevkov na zobrazenie",
|
||||
"repo.settings.pages.ai_generate": "AI gener\u00e1tor obsahu",
|
||||
"repo.settings.pages.ai_generate_desc": "Automaticky generujte obsah cie\u013eovej str\u00e1nky (nadpis, funkcie, \u0161tatistiky, CTA) z README a metad\u00e1t v\u00e1\u0161ho repozit\u00e1ra pomocou AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Generova\u0165 obsah pomocou AI",
|
||||
"repo.settings.pages.ai_generate_success": "Obsah cie\u013eovej str\u00e1nky bol \u00faspe\u0161ne vygenerovan\u00fd. Skontrolujte a prisp\u00f4sobte ho v ostatn\u00fdch kart\u00e1ch.",
|
||||
"repo.settings.pages.ai_generate_failed": "Generovanie obsahu AI zlyhalo. Sk\u00faste to nesk\u00f4r znova alebo nakonfigurujte obsah ru\u010dne.",
|
||||
"repo.settings.pages.languages": "Jazyky",
|
||||
"repo.settings.pages.default_lang": "Predvolen\u00fd jazyk",
|
||||
"repo.settings.pages.default_lang_help": "Hlavn\u00fd jazyk obsahu va\u0161ej cie\u013eovej str\u00e1nky",
|
||||
"repo.settings.pages.enabled_languages": "Povolen\u00e9 jazyky",
|
||||
"repo.settings.pages.enabled_languages_help": "Vyberte, ktor\u00e9 jazyky m\u00e1 va\u0161a cie\u013eov\u00e1 str\u00e1nka podporova\u0165. N\u00e1v\u0161tevn\u00edci uvidia v navig\u00e1cii prep\u00edna\u010d jazykov.",
|
||||
"repo.settings.pages.save_languages": "Ulo\u017ei\u0165 jazykov\u00e9 nastavenia",
|
||||
"repo.settings.pages.languages_saved": "Jazykov\u00e9 nastavenia \u00faspe\u0161ne ulo\u017een\u00e9.",
|
||||
"repo.settings.pages.translations": "Preklady",
|
||||
"repo.settings.pages.ai_translate": "AI preklad",
|
||||
"repo.settings.pages.ai_translate_success": "Preklad bol \u00faspe\u0161ne vygenerovan\u00fd AI. Skontrolujte a upravte pod\u013ea potreby.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Odstr\u00e1ni\u0165",
|
||||
"repo.settings.pages.save_translation": "Ulo\u017ei\u0165 preklad",
|
||||
"repo.settings.pages.translation_saved": "Preklad \u00faspe\u0161ne ulo\u017een\u00fd.",
|
||||
"repo.settings.pages.translation_deleted": "Preklad odstr\u00e1nen\u00fd.",
|
||||
"repo.settings.pages.translation_empty": "Nebol poskytnut\u00fd \u017eiadny obsah prekladu.",
|
||||
"repo.settings.pages.trans_headline": "Nadpis",
|
||||
"repo.settings.pages.trans_subheadline": "Podnadpis",
|
||||
"repo.settings.pages.trans_primary_cta": "\u0160t\u00edtok hlavn\u00e9ho CTA",
|
||||
"repo.settings.pages.trans_secondary_cta": "\u0160t\u00edtok ved\u013eaj\u0161ieho CTA",
|
||||
"repo.settings.pages.trans_cta_headline": "Nadpis sekcie CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "Podnadpis sekcie CTA",
|
||||
"repo.settings.pages.trans_cta_button": "\u0160t\u00edtok tla\u010didla CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -2182,6 +2182,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.bot": "Bot",
|
||||
@@ -2419,6 +2420,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "Visa Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "Visa meddelandet \"Powered by GitCaddy Server\" i sidfoten",
|
||||
"admin.config.show_footer_licenses": "Visa licensl\u00e4nk",
|
||||
"admin.config.show_footer_licenses_desc": "Visa licensl\u00e4nken i sidfoten",
|
||||
"admin.config.show_footer_api": "Visa API-l\u00e4nk",
|
||||
"admin.config.show_footer_api_desc": "Visa API (Swagger)-l\u00e4nken i sidfoten",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -2467,6 +2474,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Publika utg\u00e5vor",
|
||||
"repo.settings.pages.public_releases_desc": "Till\u00e5t oautentiserade anv\u00e4ndare att ladda ner utg\u00e5vor. Anv\u00e4ndbart f\u00f6r landningssidor f\u00f6r privata arkiv.",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon-URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL till en anpassad favicon f\u00f6r din landningssida (ICO, PNG eller SVG). L\u00e4mna tomt f\u00f6r att anv\u00e4nda standardikonen.",
|
||||
"repo.settings.pages.navigation": "Navigeringsl\u00e4nkar",
|
||||
"repo.settings.pages.navigation_desc": "Styr vilka inbyggda l\u00e4nkar som visas i sidhuvud- och sidfotnavigering.",
|
||||
"repo.settings.pages.nav_show_docs": "Visa Docs-l\u00e4nk (l\u00e4nkar till wiki)",
|
||||
"repo.settings.pages.nav_show_api": "Visa API-l\u00e4nk (l\u00e4nkar till Swagger-dokumentation)",
|
||||
"repo.settings.pages.nav_show_repository": "Visa f\u00f6rvarsl\u00e4nk (Visa k\u00e4llkod-knapp)",
|
||||
"repo.settings.pages.nav_show_releases": "Visa utgivningsl\u00e4nk",
|
||||
"repo.settings.pages.nav_show_issues": "Visa \u00e4rendel\u00e4nk",
|
||||
"repo.settings.pages.blog_section": "Bloggsektion",
|
||||
"repo.settings.pages.blog_enabled_desc": "Visa senaste blogginl\u00e4gg p\u00e5 landningssidan",
|
||||
"repo.settings.pages.blog_headline": "Bloggrubrik",
|
||||
"repo.settings.pages.blog_subheadline": "Bloggens underrubrik",
|
||||
"repo.settings.pages.blog_max_posts": "Maximalt antal inl\u00e4gg att visa",
|
||||
"repo.settings.pages.ai_generate": "AI-inneh\u00e5llsgenerator",
|
||||
"repo.settings.pages.ai_generate_desc": "Generera automatiskt landningssidans inneh\u00e5ll (rubrik, funktioner, statistik, CTA:er) fr\u00e5n ditt f\u00f6rvars README och metadata med AI.",
|
||||
"repo.settings.pages.ai_generate_button": "Generera inneh\u00e5ll med AI",
|
||||
"repo.settings.pages.ai_generate_success": "Landningssidans inneh\u00e5ll har genererats framg\u00e5ngsrikt. Granska och anpassa i de andra flikarna.",
|
||||
"repo.settings.pages.ai_generate_failed": "AI-generering misslyckades. F\u00f6rs\u00f6k igen senare eller konfigurera inneh\u00e5llet manuellt.",
|
||||
"repo.settings.pages.languages": "Spr\u00e5k",
|
||||
"repo.settings.pages.default_lang": "Standardspr\u00e5k",
|
||||
"repo.settings.pages.default_lang_help": "Huvudspr\u00e5ket f\u00f6r inneh\u00e5llet p\u00e5 din landningssida",
|
||||
"repo.settings.pages.enabled_languages": "Aktiverade spr\u00e5k",
|
||||
"repo.settings.pages.enabled_languages_help": "V\u00e4lj vilka spr\u00e5k din landningssida ska st\u00f6dja. Bes\u00f6kare ser en spr\u00e5kv\u00e4ljare i navigeringen.",
|
||||
"repo.settings.pages.save_languages": "Spara spr\u00e5kinst\u00e4llningar",
|
||||
"repo.settings.pages.languages_saved": "Spr\u00e5kinst\u00e4llningarna har sparats.",
|
||||
"repo.settings.pages.translations": "\u00d6vers\u00e4ttningar",
|
||||
"repo.settings.pages.ai_translate": "AI-\u00f6vers\u00e4ttning",
|
||||
"repo.settings.pages.ai_translate_success": "\u00d6vers\u00e4ttningen har genererats av AI. Granska och redigera vid behov.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Ta bort",
|
||||
"repo.settings.pages.save_translation": "Spara \u00f6vers\u00e4ttning",
|
||||
"repo.settings.pages.translation_saved": "\u00d6vers\u00e4ttningen har sparats.",
|
||||
"repo.settings.pages.translation_deleted": "\u00d6vers\u00e4ttningen har tagits bort.",
|
||||
"repo.settings.pages.translation_empty": "Inget \u00f6vers\u00e4ttningsinneh\u00e5ll angivet.",
|
||||
"repo.settings.pages.trans_headline": "Rubrik",
|
||||
"repo.settings.pages.trans_subheadline": "Underrubrik",
|
||||
"repo.settings.pages.trans_primary_cta": "Prim\u00e4r CTA-etikett",
|
||||
"repo.settings.pages.trans_secondary_cta": "Sekund\u00e4r CTA-etikett",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA-sektionsrubrik",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA-sektions underrubrik",
|
||||
"repo.settings.pages.trans_cta_button": "CTA-knappsetikett",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3894,6 +3894,7 @@
|
||||
"admin.dashboard.cron.process": "Cron: %[1]s",
|
||||
"admin.dashboard.resync_all_hooks": "Resynchronize git hooks of all repositories (pre-receive, update, post-receive, proc-receive, ...)",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.users.bot": "Bot",
|
||||
"admin.auths.ssh_keys_are_verified": "SSH keys in LDAP are considered as verified",
|
||||
"admin.config.db_ssl_mode": "SSL",
|
||||
@@ -4028,6 +4029,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "Powered By'y\u0131 G\u00f6ster",
|
||||
"admin.config.show_footer_powered_by_desc": "Alt bilgide \"Powered by GitCaddy Server\" mesaj\u0131n\u0131 g\u00f6ster",
|
||||
"admin.config.show_footer_licenses": "Lisanslar Ba\u011flant\u0131s\u0131n\u0131 G\u00f6ster",
|
||||
"admin.config.show_footer_licenses_desc": "Alt bilgide lisanslar ba\u011flant\u0131s\u0131n\u0131 g\u00f6ster",
|
||||
"admin.config.show_footer_api": "API Ba\u011flant\u0131s\u0131n\u0131 G\u00f6ster",
|
||||
"admin.config.show_footer_api_desc": "Alt bilgide API (Swagger) ba\u011flant\u0131s\u0131n\u0131 g\u00f6ster",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -4076,6 +4083,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "Herkese A\u00e7\u0131k S\u00fcr\u00fcmler",
|
||||
"repo.settings.pages.public_releases_desc": "Kimli\u011fi do\u011frulanmam\u0131\u015f kullan\u0131c\u0131lar\u0131n s\u00fcr\u00fcmleri indirmesine izin verin. \u00d6zel depolar\u0131n a\u00e7\u0131l\u0131\u015f sayfalar\u0131 i\u00e7in kullan\u0131\u015fl\u0131d\u0131r.",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon URL'si",
|
||||
"repo.settings.pages.brand_favicon_url_help": "A\u00e7\u0131l\u0131\u015f sayfan\u0131z i\u00e7in \u00f6zel favicon URL'si (ICO, PNG veya SVG). Varsay\u0131lan\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n.",
|
||||
"repo.settings.pages.navigation": "Navigasyon ba\u011flant\u0131lar\u0131",
|
||||
"repo.settings.pages.navigation_desc": "Ba\u015fl\u0131k ve alt bilgi navigasyonunda hangi yerle\u015fik ba\u011flant\u0131lar\u0131n g\u00f6r\u00fcnece\u011fini kontrol edin.",
|
||||
"repo.settings.pages.nav_show_docs": "Docs ba\u011flant\u0131s\u0131n\u0131 g\u00f6ster (wiki'ye ba\u011flar)",
|
||||
"repo.settings.pages.nav_show_api": "API ba\u011flant\u0131s\u0131n\u0131 g\u00f6ster (Swagger belgelerine ba\u011flar)",
|
||||
"repo.settings.pages.nav_show_repository": "Depo ba\u011flant\u0131s\u0131n\u0131 g\u00f6ster (Kaynak kodu g\u00f6r\u00fcnt\u00fcle d\u00fc\u011fmesi)",
|
||||
"repo.settings.pages.nav_show_releases": "S\u00fcr\u00fcmler ba\u011flant\u0131s\u0131n\u0131 g\u00f6ster",
|
||||
"repo.settings.pages.nav_show_issues": "Sorunlar ba\u011flant\u0131s\u0131n\u0131 g\u00f6ster",
|
||||
"repo.settings.pages.blog_section": "Blog b\u00f6l\u00fcm\u00fc",
|
||||
"repo.settings.pages.blog_enabled_desc": "A\u00e7\u0131l\u0131\u015f sayfas\u0131nda son blog g\u00f6nderilerini g\u00f6ster",
|
||||
"repo.settings.pages.blog_headline": "Blog ba\u015fl\u0131\u011f\u0131",
|
||||
"repo.settings.pages.blog_subheadline": "Blog alt ba\u015fl\u0131\u011f\u0131",
|
||||
"repo.settings.pages.blog_max_posts": "G\u00f6sterilecek maksimum g\u00f6nderi say\u0131s\u0131",
|
||||
"repo.settings.pages.ai_generate": "AI \u0130\u00e7erik Olu\u015fturucu",
|
||||
"repo.settings.pages.ai_generate_desc": "AI kullanarak deponuzun README ve meta verilerinden a\u00e7\u0131l\u0131\u015f sayfas\u0131 i\u00e7eri\u011fini (ba\u015fl\u0131k, \u00f6zellikler, istatistikler, CTA'lar) otomatik olu\u015fturun.",
|
||||
"repo.settings.pages.ai_generate_button": "AI ile i\u00e7erik olu\u015ftur",
|
||||
"repo.settings.pages.ai_generate_success": "A\u00e7\u0131l\u0131\u015f sayfas\u0131 i\u00e7eri\u011fi ba\u015far\u0131yla olu\u015fturuldu. Di\u011fer sekmelerde inceleyin ve \u00f6zelle\u015ftirin.",
|
||||
"repo.settings.pages.ai_generate_failed": "AI ile i\u00e7erik olu\u015fturma ba\u015far\u0131s\u0131z oldu. Daha sonra tekrar deneyin veya i\u00e7eri\u011fi manuel olarak yap\u0131land\u0131r\u0131n.",
|
||||
"repo.settings.pages.languages": "Diller",
|
||||
"repo.settings.pages.default_lang": "Varsay\u0131lan dil",
|
||||
"repo.settings.pages.default_lang_help": "A\u00e7\u0131l\u0131\u015f sayfas\u0131 i\u00e7eri\u011finizin ana dili",
|
||||
"repo.settings.pages.enabled_languages": "Etkinle\u015ftirilmi\u015f diller",
|
||||
"repo.settings.pages.enabled_languages_help": "A\u00e7\u0131l\u0131\u015f sayfan\u0131z\u0131n hangi dilleri desteklemesi gerekti\u011fini se\u00e7in. Ziyaret\u00e7iler navigasyonda bir dil se\u00e7ici g\u00f6recekler.",
|
||||
"repo.settings.pages.save_languages": "Dil ayarlar\u0131n\u0131 kaydet",
|
||||
"repo.settings.pages.languages_saved": "Dil ayarlar\u0131 ba\u015far\u0131yla kaydedildi.",
|
||||
"repo.settings.pages.translations": "\u00c7eviriler",
|
||||
"repo.settings.pages.ai_translate": "AI \u00c7eviri",
|
||||
"repo.settings.pages.ai_translate_success": "\u00c7eviri AI taraf\u0131ndan ba\u015far\u0131yla olu\u015fturuldu. Gerekti\u011fi gibi inceleyin ve d\u00fczenleyin.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "Sil",
|
||||
"repo.settings.pages.save_translation": "\u00c7eviriyi kaydet",
|
||||
"repo.settings.pages.translation_saved": "\u00c7eviri ba\u015far\u0131yla kaydedildi.",
|
||||
"repo.settings.pages.translation_deleted": "\u00c7eviri silindi.",
|
||||
"repo.settings.pages.translation_empty": "\u00c7eviri i\u00e7eri\u011fi sa\u011flanmad\u0131.",
|
||||
"repo.settings.pages.trans_headline": "Ba\u015fl\u0131k",
|
||||
"repo.settings.pages.trans_subheadline": "Alt ba\u015fl\u0131k",
|
||||
"repo.settings.pages.trans_primary_cta": "Birincil CTA etiketi",
|
||||
"repo.settings.pages.trans_secondary_cta": "\u0130kincil CTA etiketi",
|
||||
"repo.settings.pages.trans_cta_headline": "CTA b\u00f6l\u00fcm ba\u015fl\u0131\u011f\u0131",
|
||||
"repo.settings.pages.trans_cta_subheadline": "CTA b\u00f6l\u00fcm alt ba\u015fl\u0131\u011f\u0131",
|
||||
"repo.settings.pages.trans_cta_button": "CTA d\u00fc\u011fme etiketi",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3622,6 +3622,7 @@
|
||||
"admin.dashboard.cleanup_packages": "Clean up expired packages",
|
||||
"admin.dashboard.cleanup_actions": "Clean up expired actions' resources",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.2fa": "2FA",
|
||||
@@ -3813,6 +3814,12 @@
|
||||
"admin.config.explore_org_display_format_help": "Choose how organizations are displayed on the explore/organizations page",
|
||||
"admin.config.explore_org_format_list": "List View",
|
||||
"admin.config.explore_org_format_tiles": "Tile Cards",
|
||||
"admin.config.show_footer_powered_by": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f \"Powered by GitCaddy Server\" \u0443 \u043f\u0456\u0434\u0432\u0430\u043b\u0456",
|
||||
"admin.config.show_footer_licenses": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u043b\u0456\u0446\u0435\u043d\u0437\u0456\u0457",
|
||||
"admin.config.show_footer_licenses_desc": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u043b\u0456\u0446\u0435\u043d\u0437\u0456\u0457 \u0443 \u043f\u0456\u0434\u0432\u0430\u043b\u0456",
|
||||
"admin.config.show_footer_api": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 API",
|
||||
"admin.config.show_footer_api_desc": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 API (Swagger) \u0443 \u043f\u0456\u0434\u0432\u0430\u043b\u0456",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -3861,6 +3868,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "\u041f\u0443\u0431\u043b\u0456\u0447\u043d\u0456 \u0440\u0435\u043b\u0456\u0437\u0438",
|
||||
"repo.settings.pages.public_releases_desc": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u043d\u0435\u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u043e\u0432\u0430\u043d\u0438\u043c \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430\u043c \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u043b\u0456\u0437\u0438. \u041a\u043e\u0440\u0438\u0441\u043d\u043e \u0434\u043b\u044f \u0446\u0456\u043b\u044c\u043e\u0432\u0438\u0445 \u0441\u0442\u043e\u0440\u0456\u043d\u043e\u043a \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u0438\u0445 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0456\u0457\u0432.",
|
||||
"repo.settings.pages.brand_favicon_url": "URL \u0444\u0430\u0432\u0456\u043a\u043e\u043d\u0443",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0446\u044c\u043a\u043e\u0433\u043e \u0444\u0430\u0432\u0456\u043a\u043e\u043d\u0443 \u0434\u043b\u044f \u0432\u0430\u0448\u043e\u0457 \u0446\u0456\u043b\u044c\u043e\u0432\u043e\u0457 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0438 (ICO, PNG \u0430\u0431\u043e SVG). \u0417\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c.",
|
||||
"repo.settings.pages.navigation": "\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430\u0432\u0456\u0433\u0430\u0446\u0456\u0457",
|
||||
"repo.settings.pages.navigation_desc": "\u041a\u0435\u0440\u0443\u0439\u0442\u0435 \u0432\u0431\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u043c\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c\u0438, \u0449\u043e \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0442\u044c\u0441\u044f \u0432 \u043d\u0430\u0432\u0456\u0433\u0430\u0446\u0456\u0457 \u0448\u0430\u043f\u043a\u0438 \u0442\u0430 \u043f\u0456\u0434\u0432\u0430\u043b\u0443.",
|
||||
"repo.settings.pages.nav_show_docs": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u044e (\u0432\u0435\u0434\u0435 \u043d\u0430 \u0432\u0456\u043a\u0456)",
|
||||
"repo.settings.pages.nav_show_api": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 API (\u0432\u0435\u0434\u0435 \u043d\u0430 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u044e Swagger)",
|
||||
"repo.settings.pages.nav_show_repository": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0456\u0439 (\u043a\u043d\u043e\u043f\u043a\u0430 \u041f\u0435\u0440\u0435\u0433\u043b\u044f\u043d\u0443\u0442\u0438 \u0432\u0438\u0445\u0456\u0434\u043d\u0438\u0439 \u043a\u043e\u0434)",
|
||||
"repo.settings.pages.nav_show_releases": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u0440\u0435\u043b\u0456\u0437\u0438",
|
||||
"repo.settings.pages.nav_show_issues": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u0437\u0430\u0434\u0430\u0447\u0456",
|
||||
"repo.settings.pages.blog_section": "\u0420\u043e\u0437\u0434\u0456\u043b \u0431\u043b\u043e\u0433\u0443",
|
||||
"repo.settings.pages.blog_enabled_desc": "\u041f\u043e\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043e\u0441\u0442\u0430\u043d\u043d\u0456 \u0437\u0430\u043f\u0438\u0441\u0438 \u0431\u043b\u043e\u0433\u0443 \u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0456\u0439 \u0441\u0442\u043e\u0440\u0456\u043d\u0446\u0456",
|
||||
"repo.settings.pages.blog_headline": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0431\u043b\u043e\u0433\u0443",
|
||||
"repo.settings.pages.blog_subheadline": "\u041f\u0456\u0434\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0431\u043b\u043e\u0433\u0443",
|
||||
"repo.settings.pages.blog_max_posts": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u0456\u0432 \u0434\u043b\u044f \u043f\u043e\u043a\u0430\u0437\u0443",
|
||||
"repo.settings.pages.ai_generate": "\u0413\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440 \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0443 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0456 \u0428\u0406",
|
||||
"repo.settings.pages.ai_generate_desc": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0443\u0439\u0442\u0435 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0446\u0456\u043b\u044c\u043e\u0432\u043e\u0457 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0438 (\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a, \u0444\u0443\u043d\u043a\u0446\u0456\u0457, \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443, CTA) \u0437 README \u0442\u0430 \u043c\u0435\u0442\u0430\u0434\u0430\u043d\u0438\u0445 \u0432\u0430\u0448\u043e\u0433\u043e \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0456\u044e \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0428\u0406.",
|
||||
"repo.settings.pages.ai_generate_button": "\u0417\u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0428\u0406",
|
||||
"repo.settings.pages.ai_generate_success": "\u041a\u043e\u043d\u0442\u0435\u043d\u0442 \u0446\u0456\u043b\u044c\u043e\u0432\u043e\u0457 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0438 \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0433\u0435\u043d\u0435\u0440\u043e\u0432\u0430\u043d\u043e. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0442\u0430 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0439\u043e\u0433\u043e \u043d\u0430 \u0456\u043d\u0448\u0438\u0445 \u0432\u043a\u043b\u0430\u0434\u043a\u0430\u0445.",
|
||||
"repo.settings.pages.ai_generate_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0428\u0406. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0456\u0437\u043d\u0456\u0448\u0435 \u0430\u0431\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u043a\u043e\u043d\u0442\u0435\u043d\u0442 \u0432\u0440\u0443\u0447\u043d\u0443.",
|
||||
"repo.settings.pages.languages": "\u041c\u043e\u0432\u0438",
|
||||
"repo.settings.pages.default_lang": "\u041c\u043e\u0432\u0430 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c",
|
||||
"repo.settings.pages.default_lang_help": "\u041e\u0441\u043d\u043e\u0432\u043d\u0430 \u043c\u043e\u0432\u0430 \u043a\u043e\u043d\u0442\u0435\u043d\u0442\u0443 \u0432\u0430\u0448\u043e\u0457 \u0446\u0456\u043b\u044c\u043e\u0432\u043e\u0457 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0438",
|
||||
"repo.settings.pages.enabled_languages": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0456 \u043c\u043e\u0432\u0438",
|
||||
"repo.settings.pages.enabled_languages_help": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u043e\u0432\u0438, \u044f\u043a\u0456 \u043c\u0430\u0454 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u0442\u0438 \u0432\u0430\u0448\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0430. \u0412\u0456\u0434\u0432\u0456\u0434\u0443\u0432\u0430\u0447\u0456 \u043f\u043e\u0431\u0430\u0447\u0430\u0442\u044c \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447 \u043c\u043e\u0432 \u0443 \u043d\u0430\u0432\u0456\u0433\u0430\u0446\u0456\u0457.",
|
||||
"repo.settings.pages.save_languages": "\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438 \u043c\u043e\u0432\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f",
|
||||
"repo.settings.pages.languages_saved": "\u041c\u043e\u0432\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043d\u043e.",
|
||||
"repo.settings.pages.translations": "\u041f\u0435\u0440\u0435\u043a\u043b\u0430\u0434\u0438",
|
||||
"repo.settings.pages.ai_translate": "\u0428\u0406-\u043f\u0435\u0440\u0435\u043a\u043b\u0430\u0434",
|
||||
"repo.settings.pages.ai_translate_success": "\u041f\u0435\u0440\u0435\u043a\u043b\u0430\u0434 \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0433\u0435\u043d\u0435\u0440\u043e\u0432\u0430\u043d\u043e \u0428\u0406. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0442\u0430 \u0432\u0456\u0434\u0440\u0435\u0434\u0430\u0433\u0443\u0439\u0442\u0435 \u0437\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438.",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438",
|
||||
"repo.settings.pages.save_translation": "\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438 \u043f\u0435\u0440\u0435\u043a\u043b\u0430\u0434",
|
||||
"repo.settings.pages.translation_saved": "\u041f\u0435\u0440\u0435\u043a\u043b\u0430\u0434 \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043d\u043e.",
|
||||
"repo.settings.pages.translation_deleted": "\u041f\u0435\u0440\u0435\u043a\u043b\u0430\u0434 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e.",
|
||||
"repo.settings.pages.translation_empty": "\u0412\u043c\u0456\u0441\u0442 \u043f\u0435\u0440\u0435\u043a\u043b\u0430\u0434\u0443 \u043d\u0435 \u043d\u0430\u0434\u0430\u043d\u043e.",
|
||||
"repo.settings.pages.trans_headline": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
|
||||
"repo.settings.pages.trans_subheadline": "\u041f\u0456\u0434\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
|
||||
"repo.settings.pages.trans_primary_cta": "\u041c\u0456\u0442\u043a\u0430 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e CTA",
|
||||
"repo.settings.pages.trans_secondary_cta": "\u041c\u0456\u0442\u043a\u0430 \u0432\u0442\u043e\u0440\u0438\u043d\u043d\u043e\u0433\u043e CTA",
|
||||
"repo.settings.pages.trans_cta_headline": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0440\u043e\u0437\u0434\u0456\u043b\u0443 CTA",
|
||||
"repo.settings.pages.trans_cta_subheadline": "\u041f\u0456\u0434\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a \u0440\u043e\u0437\u0434\u0456\u043b\u0443 CTA",
|
||||
"repo.settings.pages.trans_cta_button": "\u041c\u0456\u0442\u043a\u0430 \u043a\u043d\u043e\u043f\u043a\u0438 CTA",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -4041,6 +4041,7 @@
|
||||
"org.settings.pin_to_homepage": "Pin this organization to the homepage",
|
||||
"org.settings.pin_to_homepage_help": "When enabled, this organization will be featured on the public homepage. Only administrators can change this setting.",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"actions.runners.status.unhealthy": "不健康",
|
||||
"actions.runners.capabilities": "功能",
|
||||
"actions.runners.capabilities.os": "操作系统",
|
||||
@@ -4162,6 +4163,12 @@
|
||||
"admin.config.explore_org_display_format_help": "组织的显示格式",
|
||||
"admin.config.explore_org_format_list": "列表",
|
||||
"admin.config.explore_org_format_tiles": "图块",
|
||||
"admin.config.show_footer_powered_by": "\u663e\u793a Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "\u5728\u9875\u811a\u663e\u793a\u201cPowered by GitCaddy Server\u201d\u6d88\u606f",
|
||||
"admin.config.show_footer_licenses": "\u663e\u793a\u8bb8\u53ef\u8bc1\u94fe\u63a5",
|
||||
"admin.config.show_footer_licenses_desc": "\u5728\u9875\u811a\u663e\u793a\u8bb8\u53ef\u8bc1\u94fe\u63a5",
|
||||
"admin.config.show_footer_api": "\u663e\u793a API \u94fe\u63a5",
|
||||
"admin.config.show_footer_api_desc": "\u5728\u9875\u811a\u663e\u793a API (Swagger) \u94fe\u63a5",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -4210,6 +4217,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "\u516c\u5f00\u53d1\u5e03",
|
||||
"repo.settings.pages.public_releases_desc": "\u5141\u8bb8\u672a\u8ba4\u8bc1\u7528\u6237\u4e0b\u8f7d\u53d1\u5e03\u7248\u672c\u3002\u9002\u7528\u4e8e\u79c1\u6709\u4ed3\u5e93\u7684\u7740\u9646\u9875\u3002",
|
||||
"repo.settings.pages.brand_favicon_url": "\u7f51\u7ad9\u56fe\u6807URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "\u7740\u9646\u9875\u81ea\u5b9a\u4e49\u7f51\u7ad9\u56fe\u6807\u7684URL\uff08ICO\u3001PNG\u6216SVG\uff09\u3002\u7559\u7a7a\u5219\u4f7f\u7528\u9ed8\u8ba4\u56fe\u6807\u3002",
|
||||
"repo.settings.pages.navigation": "\u5bfc\u822a\u94fe\u63a5",
|
||||
"repo.settings.pages.navigation_desc": "\u63a7\u5236\u54ea\u4e9b\u5185\u7f6e\u94fe\u63a5\u663e\u793a\u5728\u9875\u7709\u548c\u9875\u811a\u5bfc\u822a\u4e2d\u3002",
|
||||
"repo.settings.pages.nav_show_docs": "\u663e\u793a\u6587\u6863\u94fe\u63a5\uff08\u94fe\u63a5\u5230Wiki\uff09",
|
||||
"repo.settings.pages.nav_show_api": "\u663e\u793aAPI\u94fe\u63a5\uff08\u94fe\u63a5\u5230Swagger\u6587\u6863\uff09",
|
||||
"repo.settings.pages.nav_show_repository": "\u663e\u793a\u4ed3\u5e93\u94fe\u63a5\uff08\u67e5\u770b\u6e90\u4ee3\u7801\u6309\u94ae\uff09",
|
||||
"repo.settings.pages.nav_show_releases": "\u663e\u793a\u53d1\u884c\u7248\u94fe\u63a5",
|
||||
"repo.settings.pages.nav_show_issues": "\u663e\u793aIssues\u94fe\u63a5",
|
||||
"repo.settings.pages.blog_section": "\u535a\u5ba2\u533a\u57df",
|
||||
"repo.settings.pages.blog_enabled_desc": "\u5728\u7740\u9646\u9875\u4e0a\u663e\u793a\u6700\u8fd1\u7684\u535a\u5ba2\u6587\u7ae0",
|
||||
"repo.settings.pages.blog_headline": "\u535a\u5ba2\u6807\u9898",
|
||||
"repo.settings.pages.blog_subheadline": "\u535a\u5ba2\u526f\u6807\u9898",
|
||||
"repo.settings.pages.blog_max_posts": "\u6700\u591a\u663e\u793a\u6587\u7ae0\u6570",
|
||||
"repo.settings.pages.ai_generate": "AI\u5185\u5bb9\u751f\u6210\u5668",
|
||||
"repo.settings.pages.ai_generate_desc": "\u4f7f\u7528AI\u4ece\u4ed3\u5e93\u7684README\u548c\u5143\u6570\u636e\u81ea\u52a8\u751f\u6210\u7740\u9646\u9875\u5185\u5bb9\uff08\u6807\u9898\u3001\u529f\u80fd\u3001\u7edf\u8ba1\u6570\u636e\u3001\u884c\u52a8\u53f7\u53ec\uff09\u3002",
|
||||
"repo.settings.pages.ai_generate_button": "\u4f7f\u7528AI\u751f\u6210\u5185\u5bb9",
|
||||
"repo.settings.pages.ai_generate_success": "\u7740\u9646\u9875\u5185\u5bb9\u5df2\u6210\u529f\u751f\u6210\u3002\u8bf7\u5728\u5176\u4ed6\u6807\u7b7e\u9875\u4e2d\u67e5\u770b\u548c\u81ea\u5b9a\u4e49\u3002",
|
||||
"repo.settings.pages.ai_generate_failed": "AI\u5185\u5bb9\u751f\u6210\u5931\u8d25\u3002\u8bf7\u7a0d\u540e\u91cd\u8bd5\u6216\u624b\u52a8\u914d\u7f6e\u5185\u5bb9\u3002",
|
||||
"repo.settings.pages.languages": "\u8bed\u8a00",
|
||||
"repo.settings.pages.default_lang": "\u9ed8\u8ba4\u8bed\u8a00",
|
||||
"repo.settings.pages.default_lang_help": "\u7740\u9646\u9875\u5185\u5bb9\u7684\u4e3b\u8981\u8bed\u8a00",
|
||||
"repo.settings.pages.enabled_languages": "\u5df2\u542f\u7528\u7684\u8bed\u8a00",
|
||||
"repo.settings.pages.enabled_languages_help": "\u9009\u62e9\u7740\u9646\u9875\u5e94\u652f\u6301\u7684\u8bed\u8a00\u3002\u8bbf\u95ee\u8005\u5c06\u5728\u5bfc\u822a\u4e2d\u770b\u5230\u8bed\u8a00\u5207\u6362\u5668\u3002",
|
||||
"repo.settings.pages.save_languages": "\u4fdd\u5b58\u8bed\u8a00\u8bbe\u7f6e",
|
||||
"repo.settings.pages.languages_saved": "\u8bed\u8a00\u8bbe\u7f6e\u5df2\u6210\u529f\u4fdd\u5b58\u3002",
|
||||
"repo.settings.pages.translations": "\u7ffb\u8bd1",
|
||||
"repo.settings.pages.ai_translate": "AI\u7ffb\u8bd1",
|
||||
"repo.settings.pages.ai_translate_success": "AI\u5df2\u6210\u529f\u751f\u6210\u7ffb\u8bd1\u3002\u8bf7\u6839\u636e\u9700\u8981\u67e5\u770b\u548c\u7f16\u8f91\u3002",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "\u5220\u9664",
|
||||
"repo.settings.pages.save_translation": "\u4fdd\u5b58\u7ffb\u8bd1",
|
||||
"repo.settings.pages.translation_saved": "\u7ffb\u8bd1\u5df2\u6210\u529f\u4fdd\u5b58\u3002",
|
||||
"repo.settings.pages.translation_deleted": "\u7ffb\u8bd1\u5df2\u5220\u9664\u3002",
|
||||
"repo.settings.pages.translation_empty": "\u672a\u63d0\u4f9b\u7ffb\u8bd1\u5185\u5bb9\u3002",
|
||||
"repo.settings.pages.trans_headline": "\u6807\u9898",
|
||||
"repo.settings.pages.trans_subheadline": "\u526f\u6807\u9898",
|
||||
"repo.settings.pages.trans_primary_cta": "\u4e3b\u8981\u884c\u52a8\u53f7\u53ec\u6807\u7b7e",
|
||||
"repo.settings.pages.trans_secondary_cta": "\u6b21\u8981\u884c\u52a8\u53f7\u53ec\u6807\u7b7e",
|
||||
"repo.settings.pages.trans_cta_headline": "\u884c\u52a8\u53f7\u53ec\u533a\u57df\u6807\u9898",
|
||||
"repo.settings.pages.trans_cta_subheadline": "\u884c\u52a8\u53f7\u53ec\u533a\u57df\u526f\u6807\u9898",
|
||||
"repo.settings.pages.trans_cta_button": "\u884c\u52a8\u53f7\u53ec\u6309\u94ae\u6807\u7b7e",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -3880,6 +3880,7 @@
|
||||
"admin.dashboard.cleanup_packages": "清理套件",
|
||||
"admin.dashboard.cleanup_actions": "清理操作",
|
||||
"admin.dashboard.cleanup_expired_upload_sessions": "Clean up expired upload sessions",
|
||||
"admin.dashboard.analyze_page_experiments": "Analyze landing page A/B experiments",
|
||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||
"admin.users.never_login": "Never Signed In",
|
||||
@@ -4070,6 +4071,12 @@
|
||||
"admin.config.explore_org_display_format_help": "組織的顯示格式",
|
||||
"admin.config.explore_org_format_list": "清單",
|
||||
"admin.config.explore_org_format_tiles": "圖塊",
|
||||
"admin.config.show_footer_powered_by": "\u986f\u793a Powered By",
|
||||
"admin.config.show_footer_powered_by_desc": "\u5728\u9801\u8173\u986f\u793a\u300cPowered by GitCaddy Server\u300d\u8a0a\u606f",
|
||||
"admin.config.show_footer_licenses": "\u986f\u793a\u6388\u6b0a\u689d\u6b3e\u9023\u7d50",
|
||||
"admin.config.show_footer_licenses_desc": "\u5728\u9801\u8173\u986f\u793a\u6388\u6b0a\u689d\u6b3e\u9023\u7d50",
|
||||
"admin.config.show_footer_api": "\u986f\u793a API \u9023\u7d50",
|
||||
"admin.config.show_footer_api_desc": "\u5728\u9801\u8173\u986f\u793a API (Swagger) \u9023\u7d50",
|
||||
"repo.settings.pages.general": "General",
|
||||
"repo.settings.pages.brand": "Brand",
|
||||
"repo.settings.pages.hero": "Hero",
|
||||
@@ -4118,6 +4125,89 @@
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.public_releases": "\u516c\u958b\u767c\u5e03",
|
||||
"repo.settings.pages.public_releases_desc": "\u5141\u8a31\u672a\u8a8d\u8b49\u4f7f\u7528\u8005\u4e0b\u8f09\u767c\u5e03\u7248\u672c\u3002\u9069\u7528\u65bc\u79c1\u4eba\u5132\u5b58\u5eab\u7684\u8457\u9678\u9801\u3002",
|
||||
"repo.settings.pages.brand_favicon_url": "\u7db2\u7ad9\u5716\u793aURL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "\u8457\u9678\u9801\u81ea\u8a02\u7db2\u7ad9\u5716\u793a\u7684URL\uff08ICO\u3001PNG\u6216SVG\uff09\u3002\u7559\u7a7a\u5247\u4f7f\u7528\u9810\u8a2d\u5716\u793a\u3002",
|
||||
"repo.settings.pages.navigation": "\u5c0e\u89bd\u9023\u7d50",
|
||||
"repo.settings.pages.navigation_desc": "\u63a7\u5236\u54ea\u4e9b\u5167\u5efa\u9023\u7d50\u986f\u793a\u5728\u9801\u9996\u548c\u9801\u5c3e\u5c0e\u89bd\u4e2d\u3002",
|
||||
"repo.settings.pages.nav_show_docs": "\u986f\u793a\u6587\u4ef6\u9023\u7d50\uff08\u9023\u7d50\u5230Wiki\uff09",
|
||||
"repo.settings.pages.nav_show_api": "\u986f\u793aAPI\u9023\u7d50\uff08\u9023\u7d50\u5230Swagger\u6587\u4ef6\uff09",
|
||||
"repo.settings.pages.nav_show_repository": "\u986f\u793a\u5132\u5b58\u5eab\u9023\u7d50\uff08\u6aa2\u8996\u539f\u59cb\u78bc\u6309\u9215\uff09",
|
||||
"repo.settings.pages.nav_show_releases": "\u986f\u793a\u7248\u672c\u767c\u5e03\u9023\u7d50",
|
||||
"repo.settings.pages.nav_show_issues": "\u986f\u793aIssues\u9023\u7d50",
|
||||
"repo.settings.pages.blog_section": "\u90e8\u843d\u683c\u5340\u57df",
|
||||
"repo.settings.pages.blog_enabled_desc": "\u5728\u8457\u9678\u9801\u4e0a\u986f\u793a\u6700\u8fd1\u7684\u90e8\u843d\u683c\u6587\u7ae0",
|
||||
"repo.settings.pages.blog_headline": "\u90e8\u843d\u683c\u6a19\u984c",
|
||||
"repo.settings.pages.blog_subheadline": "\u90e8\u843d\u683c\u526f\u6a19\u984c",
|
||||
"repo.settings.pages.blog_max_posts": "\u6700\u591a\u986f\u793a\u6587\u7ae0\u6578",
|
||||
"repo.settings.pages.ai_generate": "AI\u5167\u5bb9\u7522\u751f\u5668",
|
||||
"repo.settings.pages.ai_generate_desc": "\u4f7f\u7528AI\u5f9e\u5132\u5b58\u5eab\u7684README\u548c\u4e2d\u7e7c\u8cc7\u6599\u81ea\u52d5\u7522\u751f\u8457\u9678\u9801\u5167\u5bb9\uff08\u6a19\u984c\u3001\u529f\u80fd\u3001\u7d71\u8a08\u8cc7\u6599\u3001\u884c\u52d5\u547c\u7c72\uff09\u3002",
|
||||
"repo.settings.pages.ai_generate_button": "\u4f7f\u7528AI\u7522\u751f\u5167\u5bb9",
|
||||
"repo.settings.pages.ai_generate_success": "\u8457\u9678\u9801\u5167\u5bb9\u5df2\u6210\u529f\u7522\u751f\u3002\u8acb\u5728\u5176\u4ed6\u5206\u9801\u4e2d\u6aa2\u8996\u548c\u81ea\u8a02\u3002",
|
||||
"repo.settings.pages.ai_generate_failed": "AI\u5167\u5bb9\u7522\u751f\u5931\u6557\u3002\u8acb\u7a0d\u5f8c\u91cd\u8a66\u6216\u624b\u52d5\u8a2d\u5b9a\u5167\u5bb9\u3002",
|
||||
"repo.settings.pages.languages": "\u8a9e\u8a00",
|
||||
"repo.settings.pages.default_lang": "\u9810\u8a2d\u8a9e\u8a00",
|
||||
"repo.settings.pages.default_lang_help": "\u8457\u9678\u9801\u5167\u5bb9\u7684\u4e3b\u8981\u8a9e\u8a00",
|
||||
"repo.settings.pages.enabled_languages": "\u5df2\u555f\u7528\u7684\u8a9e\u8a00",
|
||||
"repo.settings.pages.enabled_languages_help": "\u9078\u64c7\u8457\u9678\u9801\u61c9\u652f\u63f4\u7684\u8a9e\u8a00\u3002\u8a2a\u5ba2\u5c07\u5728\u5c0e\u89bd\u4e2d\u770b\u5230\u8a9e\u8a00\u5207\u63db\u5668\u3002",
|
||||
"repo.settings.pages.save_languages": "\u5132\u5b58\u8a9e\u8a00\u8a2d\u5b9a",
|
||||
"repo.settings.pages.languages_saved": "\u8a9e\u8a00\u8a2d\u5b9a\u5df2\u6210\u529f\u5132\u5b58\u3002",
|
||||
"repo.settings.pages.translations": "\u7ffb\u8b6f",
|
||||
"repo.settings.pages.ai_translate": "AI\u7ffb\u8b6f",
|
||||
"repo.settings.pages.ai_translate_success": "AI\u5df2\u6210\u529f\u7522\u751f\u7ffb\u8b6f\u3002\u8acb\u6839\u64da\u9700\u8981\u6aa2\u8996\u548c\u7de8\u8f2f\u3002",
|
||||
"repo.settings.pages.ai_translate_all": "Translate All (AI)",
|
||||
"repo.settings.pages.ai_translate_all_success": "Successfully translated %d languages.",
|
||||
"repo.settings.pages.ai_translate_all_partial": "Translated %d of %d languages. %d failed.",
|
||||
"repo.settings.pages.delete_translation": "\u522a\u9664",
|
||||
"repo.settings.pages.save_translation": "\u5132\u5b58\u7ffb\u8b6f",
|
||||
"repo.settings.pages.translation_saved": "\u7ffb\u8b6f\u5df2\u6210\u529f\u5132\u5b58\u3002",
|
||||
"repo.settings.pages.translation_deleted": "\u7ffb\u8b6f\u5df2\u522a\u9664\u3002",
|
||||
"repo.settings.pages.translation_empty": "\u672a\u63d0\u4f9b\u7ffb\u8b6f\u5167\u5bb9\u3002",
|
||||
"repo.settings.pages.trans_headline": "\u6a19\u984c",
|
||||
"repo.settings.pages.trans_subheadline": "\u526f\u6a19\u984c",
|
||||
"repo.settings.pages.trans_primary_cta": "\u4e3b\u8981\u884c\u52d5\u547c\u7c72\u6a19\u7c64",
|
||||
"repo.settings.pages.trans_secondary_cta": "\u6b21\u8981\u884c\u52d5\u547c\u7c72\u6a19\u7c64",
|
||||
"repo.settings.pages.trans_cta_headline": "\u884c\u52d5\u547c\u7c72\u5340\u57df\u6a19\u984c",
|
||||
"repo.settings.pages.trans_cta_subheadline": "\u884c\u52d5\u547c\u7c72\u5340\u57df\u526f\u6a19\u984c",
|
||||
"repo.settings.pages.trans_cta_button": "\u884c\u52d5\u547c\u7c72\u6309\u9215\u6a19\u7c64",
|
||||
"repo.settings.pages.trans_section_brand": "Brand",
|
||||
"repo.settings.pages.trans_section_hero": "Hero",
|
||||
"repo.settings.pages.trans_section_stats": "Stats",
|
||||
"repo.settings.pages.trans_section_value_props": "Value Propositions",
|
||||
"repo.settings.pages.trans_section_features": "Features",
|
||||
"repo.settings.pages.trans_section_testimonials": "Testimonials",
|
||||
"repo.settings.pages.trans_section_pricing": "Pricing",
|
||||
"repo.settings.pages.trans_section_cta": "Call to Action",
|
||||
"repo.settings.pages.trans_section_blog": "Blog",
|
||||
"repo.settings.pages.trans_section_gallery": "Gallery",
|
||||
"repo.settings.pages.trans_section_comparison": "Comparison",
|
||||
"repo.settings.pages.trans_section_footer": "Footer",
|
||||
"repo.settings.pages.trans_section_seo": "SEO",
|
||||
"repo.settings.pages.trans_brand_name": "Brand Name",
|
||||
"repo.settings.pages.trans_brand_tagline": "Tagline",
|
||||
"repo.settings.pages.trans_stat_value": "Value",
|
||||
"repo.settings.pages.trans_stat_label": "Label",
|
||||
"repo.settings.pages.trans_title": "Title",
|
||||
"repo.settings.pages.trans_description": "Description",
|
||||
"repo.settings.pages.trans_quote": "Quote",
|
||||
"repo.settings.pages.trans_role": "Role",
|
||||
"repo.settings.pages.trans_pricing_headline": "Pricing Headline",
|
||||
"repo.settings.pages.trans_pricing_subheadline": "Pricing Subheadline",
|
||||
"repo.settings.pages.trans_plan_name": "Plan Name",
|
||||
"repo.settings.pages.trans_plan_period": "Period",
|
||||
"repo.settings.pages.trans_plan_cta": "Plan Button",
|
||||
"repo.settings.pages.trans_blog_headline": "Blog Headline",
|
||||
"repo.settings.pages.trans_blog_subheadline": "Blog Subheadline",
|
||||
"repo.settings.pages.trans_blog_cta": "Blog Button",
|
||||
"repo.settings.pages.trans_gallery_headline": "Gallery Headline",
|
||||
"repo.settings.pages.trans_gallery_subheadline": "Gallery Subheadline",
|
||||
"repo.settings.pages.trans_comparison_headline": "Comparison Headline",
|
||||
"repo.settings.pages.trans_comparison_subheadline": "Comparison Subheadline",
|
||||
"repo.settings.pages.trans_footer_copyright": "Copyright",
|
||||
"repo.settings.pages.trans_footer_link": "Link Label",
|
||||
"repo.settings.pages.trans_seo_title": "SEO Title",
|
||||
"repo.settings.pages.trans_seo_description": "SEO Description",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
"repo.vault.secret_limit_reached": "Secret limit reached. Your current tier allows %d secrets per repository. Upgrade to Pro for unlimited secrets.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
--------------------------------------------------------------------------------
|
||||
GitCaddy Server - Third Party Licenses
|
||||
Copyright (c) 2024-2026 MarketAlly Inc.
|
||||
Copyright (c) 2010-2026 MarketAlly Inc.
|
||||
https://marketally.com
|
||||
|
||||
GitCaddy is built on Gitea (https://gitea.com) and includes the following
|
||||
|
||||
@@ -57,7 +57,7 @@ func GetOverview(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// Get stats
|
||||
stats, err := org_service.GetOrgOverviewStats(ctx, org.ID)
|
||||
stats, err := org_service.GetOrgOverviewStats(ctx, org.ID, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
@@ -72,7 +72,7 @@ func AIReviewPullRequest(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
review, err := ai_service.ReviewPullRequest(ctx, pr)
|
||||
review, err := ai_service.ReviewPullRequest(ctx, pr, nil)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": err.Error(),
|
||||
@@ -141,7 +141,7 @@ func AITriageIssue(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
triage, err := ai_service.TriageIssue(ctx, issue)
|
||||
triage, err := ai_service.TriageIssue(ctx, issue, nil)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": err.Error(),
|
||||
@@ -203,7 +203,7 @@ func AISuggestLabels(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
suggestions, err := ai_service.SuggestLabels(ctx, issue)
|
||||
suggestions, err := ai_service.SuggestLabels(ctx, issue, nil)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": err.Error(),
|
||||
@@ -279,7 +279,7 @@ func AIExplainCode(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
explanation, err := ai_service.ExplainCode(ctx, ctx.Repo.Repository, req.FilePath, req.Code, req.StartLine, req.EndLine, req.Question)
|
||||
explanation, err := ai_service.ExplainCode(ctx, ctx.Repo.Repository, req.FilePath, req.Code, req.StartLine, req.EndLine, req.Question, nil)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": err.Error(),
|
||||
@@ -355,7 +355,7 @@ func AIGenerateDocumentation(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
docs, err := ai_service.GenerateDocumentation(ctx, ctx.Repo.Repository, req.FilePath, req.Code, req.DocType, req.Language, req.Style)
|
||||
docs, err := ai_service.GenerateDocumentation(ctx, ctx.Repo.Repository, req.FilePath, req.Code, req.DocType, req.Language, req.Style, nil)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": err.Error(),
|
||||
|
||||
184
routers/api/v2/actions_failure.go
Normal file
184
routers/api/v2/actions_failure.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitcaddy.com/server/v3/models/actions"
|
||||
"code.gitcaddy.com/server/v3/modules/actions"
|
||||
"code.gitcaddy.com/server/v3/modules/gitrepo"
|
||||
"code.gitcaddy.com/server/v3/modules/log"
|
||||
api "code.gitcaddy.com/server/v3/modules/structs"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
"code.gitcaddy.com/server/v3/services/convert"
|
||||
)
|
||||
|
||||
const maxLogTailLines = 200
|
||||
|
||||
// GetRunFailureLog returns a structured failure summary for a workflow run.
|
||||
// It includes failed jobs, their failed step names, extracted log tails,
|
||||
// and the workflow YAML — all in a single response.
|
||||
func GetRunFailureLog(ctx *context.APIContext) {
|
||||
runID := ctx.PathParamInt64("run_id")
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
// 1. Load the run
|
||||
run, err := actions_model.GetRunByID(ctx, runID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if run == nil || run.RepoID != repo.ID {
|
||||
ctx.APIErrorNotFound("run not found")
|
||||
return
|
||||
}
|
||||
|
||||
runStatus, runConclusion := convert.ToActionsStatus(run.Status)
|
||||
|
||||
// 2. Load all jobs for this run
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Filter to failed jobs; if none explicitly failed, include all
|
||||
var targetJobs []*actions_model.ActionRunJob
|
||||
for _, job := range jobs {
|
||||
if job.Status.IsFailure() {
|
||||
targetJobs = append(targetJobs, job)
|
||||
}
|
||||
}
|
||||
if len(targetJobs) == 0 {
|
||||
targetJobs = jobs
|
||||
}
|
||||
|
||||
// 4. For each target job, load steps and log tail
|
||||
failedJobs := make([]*api.ActionJobFailureDetail, 0, len(targetJobs))
|
||||
for _, job := range targetJobs {
|
||||
jobStatus, jobConclusion := convert.ToActionsStatus(job.Status)
|
||||
|
||||
detail := &api.ActionJobFailureDetail{
|
||||
JobID: job.ID,
|
||||
JobName: job.Name,
|
||||
Status: jobStatus,
|
||||
Conclusion: jobConclusion,
|
||||
}
|
||||
|
||||
// Load steps to find failed step names
|
||||
if job.TaskID > 0 {
|
||||
steps, err := actions_model.GetTaskStepsByTaskID(ctx, job.TaskID)
|
||||
if err != nil {
|
||||
log.Error("GetTaskStepsByTaskID(%d): %v", job.TaskID, err)
|
||||
} else {
|
||||
for _, step := range steps {
|
||||
if step.Status.IsFailure() {
|
||||
detail.FailedSteps = append(detail.FailedSteps, step.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Read log tail from the task
|
||||
detail.Log = readLogTail(ctx, job.TaskID)
|
||||
}
|
||||
|
||||
failedJobs = append(failedJobs, detail)
|
||||
}
|
||||
|
||||
// 5. Read workflow YAML from the git repo
|
||||
workflowYAML := readWorkflowYAML(ctx, run.WorkflowID)
|
||||
|
||||
ctx.JSON(200, &api.ActionRunFailureLog{
|
||||
RunID: run.ID,
|
||||
Status: runStatus,
|
||||
Conclusion: runConclusion,
|
||||
WorkflowID: run.WorkflowID,
|
||||
WorkflowYAML: workflowYAML,
|
||||
FailedJobs: failedJobs,
|
||||
})
|
||||
}
|
||||
|
||||
// readLogTail reads the last maxLogTailLines from a task's log, stripping timestamps.
|
||||
func readLogTail(ctx *context.APIContext, taskID int64) string {
|
||||
task, err := actions_model.GetTaskByID(ctx, taskID)
|
||||
if err != nil {
|
||||
log.Error("GetTaskByID(%d): %v", taskID, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
f, err := actions.OpenLogs(ctx, task.LogInStorage, task.LogFilename)
|
||||
if err != nil {
|
||||
log.Error("OpenLogs(%s): %v", task.LogFilename, err)
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
content, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
log.Error("ReadAll log: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Split into lines, take last N
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(content)))
|
||||
var allLines []string
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// Strip timestamp prefix
|
||||
_, parsed, parseErr := actions.ParseLog(line)
|
||||
if parseErr == nil {
|
||||
allLines = append(allLines, parsed)
|
||||
} else {
|
||||
allLines = append(allLines, line)
|
||||
}
|
||||
}
|
||||
|
||||
if len(allLines) > maxLogTailLines {
|
||||
allLines = allLines[len(allLines)-maxLogTailLines:]
|
||||
}
|
||||
|
||||
return strings.Join(allLines, "\n")
|
||||
}
|
||||
|
||||
// readWorkflowYAML reads the workflow file content from the repo's default branch.
|
||||
func readWorkflowYAML(ctx *context.APIContext, workflowID string) string {
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
log.Error("OpenRepository: %v", err)
|
||||
return ""
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
commit, err := gitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
|
||||
if err != nil {
|
||||
log.Error("GetBranchCommit(%s): %v", ctx.Repo.Repository.DefaultBranch, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try .gitea/workflows/ first, then .github/workflows/
|
||||
for _, prefix := range []string{".gitea/workflows/", ".github/workflows/"} {
|
||||
blob, err := commit.GetBlobByPath(prefix + workflowID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
reader, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer reader.Close()
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
46
routers/api/v2/actions_status.go
Normal file
46
routers/api/v2/actions_status.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
actions_model "code.gitcaddy.com/server/v3/models/actions"
|
||||
"code.gitcaddy.com/server/v3/modules/git"
|
||||
api "code.gitcaddy.com/server/v3/modules/structs"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
"code.gitcaddy.com/server/v3/services/convert"
|
||||
)
|
||||
|
||||
// ListWorkflowStatuses returns the latest run status for each workflow in the repository.
|
||||
// This is a batch endpoint that replaces the need to fetch all runs and filter client-side.
|
||||
func ListWorkflowStatuses(ctx *context.APIContext) {
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
runs, err := actions_model.GetLatestRunPerWorkflow(ctx, repo.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
workflows := make([]*api.ActionWorkflowStatus, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
status, conclusion := convert.ToActionsStatus(run.Status)
|
||||
workflows = append(workflows, &api.ActionWorkflowStatus{
|
||||
WorkflowID: run.WorkflowID,
|
||||
WorkflowName: run.Title,
|
||||
Status: status,
|
||||
Conclusion: conclusion,
|
||||
RunID: run.ID,
|
||||
RunNumber: run.Index,
|
||||
Event: string(run.Event),
|
||||
HeadBranch: git.RefName(run.Ref).BranchName(),
|
||||
StartedAt: run.Started.AsLocalTime(),
|
||||
CompletedAt: run.Stopped.AsLocalTime(),
|
||||
HTMLURL: run.HTMLURL(),
|
||||
})
|
||||
}
|
||||
|
||||
ctx.JSON(200, &api.ActionWorkflowStatusResponse{
|
||||
Workflows: workflows,
|
||||
})
|
||||
}
|
||||
479
routers/api/v2/ai_operations.go
Normal file
479
routers/api/v2/ai_operations.go
Normal file
@@ -0,0 +1,479 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
ai_model "code.gitcaddy.com/server/v3/models/ai"
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
issues_model "code.gitcaddy.com/server/v3/models/issues"
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
"code.gitcaddy.com/server/v3/models/unit"
|
||||
ai_module "code.gitcaddy.com/server/v3/modules/ai"
|
||||
apierrors "code.gitcaddy.com/server/v3/modules/errors"
|
||||
"code.gitcaddy.com/server/v3/modules/setting"
|
||||
api "code.gitcaddy.com/server/v3/modules/structs"
|
||||
"code.gitcaddy.com/server/v3/modules/web"
|
||||
"code.gitcaddy.com/server/v3/services/ai"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
)
|
||||
|
||||
// getRepoAIConfig is a helper that loads the AI config for the repo in ctx,
|
||||
// returning nil and writing an error response if AI is not available.
|
||||
func getRepoAIConfig(ctx *context.APIContext) *repo_model.AIConfig {
|
||||
if !setting.AI.Enabled {
|
||||
ctx.APIErrorWithCode(apierrors.AIDisabled)
|
||||
return nil
|
||||
}
|
||||
|
||||
aiUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeAI)
|
||||
if err != nil {
|
||||
ctx.APIErrorWithCode(apierrors.AIUnitNotEnabled)
|
||||
return nil
|
||||
}
|
||||
return aiUnit.AIConfig()
|
||||
}
|
||||
|
||||
// resolveProviderConfig builds a ProviderConfig from the repo/org/system cascade.
|
||||
func resolveProviderConfig(ctx *context.APIContext) *ai_module.ProviderConfig {
|
||||
var orgID int64
|
||||
if ctx.Repo.Repository.Owner.IsOrganization() {
|
||||
orgID = ctx.Repo.Repository.OwnerID
|
||||
}
|
||||
|
||||
var repoProvider, repoModel string
|
||||
if aiUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeAI); err == nil {
|
||||
cfg := aiUnit.AIConfig()
|
||||
repoProvider = cfg.PreferredProvider
|
||||
repoModel = cfg.PreferredModel
|
||||
}
|
||||
|
||||
provider := ai_model.ResolveProvider(ctx, orgID, repoProvider)
|
||||
model := ai_model.ResolveModel(ctx, orgID, repoModel)
|
||||
apiKey := ai_model.ResolveAPIKey(ctx, orgID, provider)
|
||||
|
||||
return &ai_module.ProviderConfig{
|
||||
Provider: provider,
|
||||
Model: model,
|
||||
APIKey: apiKey,
|
||||
}
|
||||
}
|
||||
|
||||
func toAIOperationV2(op *ai_model.OperationLog) *api.AIOperationV2 {
|
||||
return &api.AIOperationV2{
|
||||
ID: op.ID,
|
||||
RepoID: op.RepoID,
|
||||
Operation: op.Operation,
|
||||
Tier: op.Tier,
|
||||
TriggerEvent: op.TriggerEvent,
|
||||
TriggerUserID: op.TriggerUserID,
|
||||
TargetID: op.TargetID,
|
||||
TargetType: op.TargetType,
|
||||
Provider: op.Provider,
|
||||
Model: op.Model,
|
||||
InputTokens: op.InputTokens,
|
||||
OutputTokens: op.OutputTokens,
|
||||
Status: op.Status,
|
||||
ResultCommentID: op.ResultCommentID,
|
||||
ActionRunID: op.ActionRunID,
|
||||
ErrorMessage: op.ErrorMessage,
|
||||
DurationMs: op.DurationMs,
|
||||
CreatedAt: op.CreatedUnix.AsTime(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetRepoAISettings returns the AI settings for a repository
|
||||
func GetRepoAISettings(ctx *context.APIContext) {
|
||||
aiCfg := getRepoAIConfig(ctx)
|
||||
if aiCfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve cascade values for display
|
||||
var orgID int64
|
||||
if ctx.Repo.Repository.Owner.IsOrganization() {
|
||||
orgID = ctx.Repo.Repository.OwnerID
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &api.AISettingsV2{
|
||||
AutoRespondToIssues: aiCfg.AutoRespondToIssues,
|
||||
AutoReviewPRs: aiCfg.AutoReviewPRs,
|
||||
AutoInspectWorkflows: aiCfg.AutoInspectWorkflows,
|
||||
AutoTriageIssues: aiCfg.AutoTriageIssues,
|
||||
AgentModeEnabled: aiCfg.AgentModeEnabled,
|
||||
AgentTriggerLabels: aiCfg.AgentTriggerLabels,
|
||||
AgentMaxRunMinutes: aiCfg.AgentMaxRunMinutes,
|
||||
EscalateToStaff: aiCfg.EscalateToStaff,
|
||||
EscalationLabel: aiCfg.EscalationLabel,
|
||||
EscalationAssignTeam: aiCfg.EscalationAssignTeam,
|
||||
PreferredProvider: aiCfg.PreferredProvider,
|
||||
PreferredModel: aiCfg.PreferredModel,
|
||||
SystemInstructions: aiCfg.SystemInstructions,
|
||||
ReviewInstructions: aiCfg.ReviewInstructions,
|
||||
IssueInstructions: aiCfg.IssueInstructions,
|
||||
ResolvedProvider: ai_model.ResolveProvider(ctx, orgID, aiCfg.PreferredProvider),
|
||||
ResolvedModel: ai_model.ResolveModel(ctx, orgID, aiCfg.PreferredModel),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRepoAISettings updates the AI settings for a repository
|
||||
func UpdateRepoAISettings(ctx *context.APIContext) {
|
||||
if !setting.AI.Enabled {
|
||||
ctx.APIErrorWithCode(apierrors.AIDisabled)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.Permission.IsAdmin() {
|
||||
ctx.APIErrorWithCode(apierrors.PermRepoAdminRequired)
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdateAISettingsOption)
|
||||
|
||||
// Get existing AI unit, or create it
|
||||
aiUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeAI)
|
||||
if err != nil {
|
||||
// AI unit doesn't exist yet — create it
|
||||
aiUnit = &repo_model.RepoUnit{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Type: unit.TypeAI,
|
||||
Config: &repo_model.AIConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
cfg := aiUnit.AIConfig()
|
||||
|
||||
// Apply updates using optional fields (nil = don't change)
|
||||
if form.AutoRespondToIssues != nil {
|
||||
cfg.AutoRespondToIssues = *form.AutoRespondToIssues
|
||||
}
|
||||
if form.AutoReviewPRs != nil {
|
||||
cfg.AutoReviewPRs = *form.AutoReviewPRs
|
||||
}
|
||||
if form.AutoInspectWorkflows != nil {
|
||||
cfg.AutoInspectWorkflows = *form.AutoInspectWorkflows
|
||||
}
|
||||
if form.AutoTriageIssues != nil {
|
||||
cfg.AutoTriageIssues = *form.AutoTriageIssues
|
||||
}
|
||||
if form.AgentModeEnabled != nil {
|
||||
if *form.AgentModeEnabled && !setting.AI.AllowAgentMode {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
|
||||
"detail": "Agent mode is disabled by the system administrator",
|
||||
})
|
||||
return
|
||||
}
|
||||
cfg.AgentModeEnabled = *form.AgentModeEnabled
|
||||
}
|
||||
if form.AgentTriggerLabels != nil {
|
||||
cfg.AgentTriggerLabels = form.AgentTriggerLabels
|
||||
}
|
||||
if form.AgentMaxRunMinutes != nil {
|
||||
cfg.AgentMaxRunMinutes = *form.AgentMaxRunMinutes
|
||||
}
|
||||
if form.EscalateToStaff != nil {
|
||||
cfg.EscalateToStaff = *form.EscalateToStaff
|
||||
}
|
||||
if form.EscalationLabel != nil {
|
||||
cfg.EscalationLabel = *form.EscalationLabel
|
||||
}
|
||||
if form.EscalationAssignTeam != nil {
|
||||
cfg.EscalationAssignTeam = *form.EscalationAssignTeam
|
||||
}
|
||||
if form.PreferredProvider != nil {
|
||||
cfg.PreferredProvider = *form.PreferredProvider
|
||||
}
|
||||
if form.PreferredModel != nil {
|
||||
cfg.PreferredModel = *form.PreferredModel
|
||||
}
|
||||
if form.SystemInstructions != nil {
|
||||
cfg.SystemInstructions = *form.SystemInstructions
|
||||
}
|
||||
if form.ReviewInstructions != nil {
|
||||
cfg.ReviewInstructions = *form.ReviewInstructions
|
||||
}
|
||||
if form.IssueInstructions != nil {
|
||||
cfg.IssueInstructions = *form.IssueInstructions
|
||||
}
|
||||
|
||||
aiUnit.Config = cfg
|
||||
|
||||
if aiUnit.ID == 0 {
|
||||
if err := db.Insert(ctx, aiUnit); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := repo_model.UpdateRepoUnit(ctx, aiUnit); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, cfg)
|
||||
}
|
||||
|
||||
// ListAIOperations returns the AI operation log for a repository
|
||||
func ListAIOperations(ctx *context.APIContext) {
|
||||
if !setting.AI.Enabled {
|
||||
ctx.APIErrorWithCode(apierrors.AIDisabled)
|
||||
return
|
||||
}
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
limit := ctx.FormInt("limit")
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
opts := ai_model.FindOperationLogsOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: limit,
|
||||
},
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Operation: ctx.FormString("operation"),
|
||||
Status: ctx.FormString("status"),
|
||||
Tier: ctx.FormInt("tier"),
|
||||
}
|
||||
|
||||
ops, err := db.Find[ai_model.OperationLog](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := db.Count[ai_model.OperationLog](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]*api.AIOperationV2, 0, len(ops))
|
||||
for _, op := range ops {
|
||||
result = append(result, toAIOperationV2(op))
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &api.AIOperationListV2{
|
||||
Operations: result,
|
||||
TotalCount: count,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAIOperation returns a single AI operation by ID
|
||||
func GetAIOperation(ctx *context.APIContext) {
|
||||
if !setting.AI.Enabled {
|
||||
ctx.APIErrorWithCode(apierrors.AIDisabled)
|
||||
return
|
||||
}
|
||||
|
||||
id := ctx.PathParamInt64("id")
|
||||
op, err := ai_model.GetOperationLog(ctx, id)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if op == nil || op.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, toAIOperationV2(op))
|
||||
}
|
||||
|
||||
// TriggerAIReview manually triggers an AI code review for a pull request
|
||||
func TriggerAIReview(ctx *context.APIContext) {
|
||||
if getRepoAIConfig(ctx) == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !setting.AI.EnableCodeReview {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
|
||||
"detail": "Code review is disabled by the system administrator",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
pullIndex := ctx.PathParamInt64("pull")
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, pullIndex)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorWithCode(apierrors.PRNotFound)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !issue.IsPull {
|
||||
ctx.APIErrorWithCode(apierrors.PRNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ai.EnqueueOperation(&ai.OperationRequest{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Operation: "code-review",
|
||||
Tier: 1,
|
||||
TriggerEvent: "api.manual",
|
||||
TriggerUserID: ctx.Doer.ID,
|
||||
TargetID: issue.ID,
|
||||
TargetType: "pull",
|
||||
}); err != nil {
|
||||
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
|
||||
"detail": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusAccepted, map[string]any{
|
||||
"message": "AI code review has been queued",
|
||||
"issue_id": issue.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// triggerIssueAIOp is a shared helper for issue-targeted AI operations (respond, triage).
|
||||
func triggerIssueAIOp(ctx *context.APIContext, operation, successMsg string) {
|
||||
issueIndex := ctx.PathParamInt64("issue")
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorWithCode(apierrors.IssueNotFound)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := ai.EnqueueOperation(&ai.OperationRequest{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Operation: operation,
|
||||
Tier: 1,
|
||||
TriggerEvent: "api.manual",
|
||||
TriggerUserID: ctx.Doer.ID,
|
||||
TargetID: issue.ID,
|
||||
TargetType: "issue",
|
||||
}); err != nil {
|
||||
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
|
||||
"detail": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusAccepted, map[string]any{
|
||||
"message": successMsg,
|
||||
"issue_id": issue.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// TriggerAIRespond manually triggers an AI response to an issue
|
||||
func TriggerAIRespond(ctx *context.APIContext) {
|
||||
if getRepoAIConfig(ctx) == nil {
|
||||
return
|
||||
}
|
||||
if !setting.AI.AllowAutoRespond {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
|
||||
"detail": "Auto-respond is disabled by the system administrator",
|
||||
})
|
||||
return
|
||||
}
|
||||
triggerIssueAIOp(ctx, "issue-response", "AI response has been queued")
|
||||
}
|
||||
|
||||
// TriggerAITriage manually triggers AI triage for an issue
|
||||
func TriggerAITriage(ctx *context.APIContext) {
|
||||
if getRepoAIConfig(ctx) == nil {
|
||||
return
|
||||
}
|
||||
if !setting.AI.EnableIssueTriage {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
|
||||
"detail": "Issue triage is disabled by the system administrator",
|
||||
})
|
||||
return
|
||||
}
|
||||
triggerIssueAIOp(ctx, "issue-triage", "AI triage has been queued")
|
||||
}
|
||||
|
||||
// TriggerAIExplain triggers an AI explanation of code
|
||||
func TriggerAIExplain(ctx *context.APIContext) {
|
||||
if !setting.AI.Enabled {
|
||||
ctx.APIErrorWithCode(apierrors.AIDisabled)
|
||||
return
|
||||
}
|
||||
|
||||
if !setting.AI.EnableExplainCode {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
|
||||
"detail": "Code explanation is disabled by the system administrator",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.AIExplainRequest)
|
||||
|
||||
providerCfg := resolveProviderConfig(ctx)
|
||||
|
||||
resp, err := ai.ExplainCode(ctx, ctx.Repo.Repository, form.FilePath, "", form.StartLine, form.EndLine, form.Question, providerCfg)
|
||||
if err != nil {
|
||||
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
|
||||
"detail": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// TriggerAIFix triggers a Tier 2 agent fix for an issue
|
||||
func TriggerAIFix(ctx *context.APIContext) {
|
||||
aiCfg := getRepoAIConfig(ctx)
|
||||
if aiCfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !setting.AI.AllowAgentMode {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
|
||||
"detail": "Agent mode is disabled by the system administrator",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !aiCfg.AgentModeEnabled {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
|
||||
"detail": "Agent mode is not enabled for this repository",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
issueIndex := ctx.PathParamInt64("issue")
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, issueIndex)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
ctx.APIErrorWithCode(apierrors.IssueNotFound)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := ai.EnqueueOperation(&ai.OperationRequest{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Operation: "agent-fix",
|
||||
Tier: 2,
|
||||
TriggerEvent: "api.manual",
|
||||
TriggerUserID: ctx.Doer.ID,
|
||||
TargetID: issue.ID,
|
||||
TargetType: "issue",
|
||||
}); err != nil {
|
||||
ctx.APIErrorWithCode(apierrors.AIServiceError, map[string]any{
|
||||
"detail": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusAccepted, map[string]any{
|
||||
"message": "AI agent fix has been queued",
|
||||
"issue_id": issue.ID,
|
||||
"tier": 2,
|
||||
})
|
||||
}
|
||||
242
routers/api/v2/ai_settings.go
Normal file
242
routers/api/v2/ai_settings.go
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
ai_model "code.gitcaddy.com/server/v3/models/ai"
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
"code.gitcaddy.com/server/v3/models/organization"
|
||||
ai_module "code.gitcaddy.com/server/v3/modules/ai"
|
||||
apierrors "code.gitcaddy.com/server/v3/modules/errors"
|
||||
"code.gitcaddy.com/server/v3/modules/setting"
|
||||
api "code.gitcaddy.com/server/v3/modules/structs"
|
||||
"code.gitcaddy.com/server/v3/modules/web"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
)
|
||||
|
||||
// GetOrgAISettingsV2 returns the AI settings for an organization
|
||||
func GetOrgAISettingsV2(ctx *context.APIContext) {
|
||||
if !setting.AI.Enabled {
|
||||
ctx.APIErrorWithCode(apierrors.AIDisabled)
|
||||
return
|
||||
}
|
||||
|
||||
orgName := ctx.PathParam("org")
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
ctx.APIErrorWithCode(apierrors.OrgNotFound)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Require org admin
|
||||
if !ctx.Doer.IsAdmin {
|
||||
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if !isOwner {
|
||||
ctx.APIErrorWithCode(apierrors.PermOrgAdminRequired)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
settings, err := ai_model.GetOrgAISettings(ctx, org.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
// Return empty defaults
|
||||
ctx.JSON(http.StatusOK, &api.OrgAISettingsV2{})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &api.OrgAISettingsV2{
|
||||
Provider: settings.Provider,
|
||||
Model: settings.Model,
|
||||
HasAPIKey: settings.APIKeyEncrypted != "",
|
||||
MaxOpsPerHour: settings.MaxOpsPerHour,
|
||||
AllowedOps: settings.AllowedOps,
|
||||
AgentModeAllowed: settings.AgentModeAllowed,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateOrgAISettingsV2 updates the AI settings for an organization
|
||||
func UpdateOrgAISettingsV2(ctx *context.APIContext) {
|
||||
if !setting.AI.Enabled {
|
||||
ctx.APIErrorWithCode(apierrors.AIDisabled)
|
||||
return
|
||||
}
|
||||
|
||||
orgName := ctx.PathParam("org")
|
||||
org, err := organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
ctx.APIErrorWithCode(apierrors.OrgNotFound)
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Require org admin
|
||||
if !ctx.Doer.IsAdmin {
|
||||
isOwner, err := org.IsOwnedBy(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if !isOwner {
|
||||
ctx.APIErrorWithCode(apierrors.PermOrgAdminRequired)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdateOrgAISettingsOption)
|
||||
|
||||
// Load existing or create new
|
||||
settings, err := ai_model.GetOrgAISettings(ctx, org.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
if settings == nil {
|
||||
settings = &ai_model.OrgAISettings{OrgID: org.ID}
|
||||
}
|
||||
|
||||
if form.Provider != nil {
|
||||
settings.Provider = *form.Provider
|
||||
}
|
||||
if form.Model != nil {
|
||||
settings.Model = *form.Model
|
||||
}
|
||||
if form.APIKey != nil {
|
||||
if err := settings.SetAPIKey(*form.APIKey); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if form.MaxOpsPerHour != nil {
|
||||
settings.MaxOpsPerHour = *form.MaxOpsPerHour
|
||||
}
|
||||
if form.AllowedOps != nil {
|
||||
settings.AllowedOps = *form.AllowedOps
|
||||
}
|
||||
if form.AgentModeAllowed != nil {
|
||||
if *form.AgentModeAllowed && !setting.AI.AllowAgentMode {
|
||||
ctx.APIErrorWithCode(apierrors.AIOperationDisabled, map[string]any{
|
||||
"detail": "Agent mode is disabled by the system administrator",
|
||||
})
|
||||
return
|
||||
}
|
||||
settings.AgentModeAllowed = *form.AgentModeAllowed
|
||||
}
|
||||
|
||||
if err := ai_model.CreateOrUpdateOrgAISettings(ctx, settings); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &api.OrgAISettingsV2{
|
||||
Provider: settings.Provider,
|
||||
Model: settings.Model,
|
||||
HasAPIKey: settings.APIKeyEncrypted != "",
|
||||
MaxOpsPerHour: settings.MaxOpsPerHour,
|
||||
AllowedOps: settings.AllowedOps,
|
||||
AgentModeAllowed: settings.AgentModeAllowed,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAIServiceStatus returns the AI service health and global stats (admin only)
|
||||
func GetAIServiceStatus(ctx *context.APIContext) {
|
||||
if !ctx.Doer.IsAdmin {
|
||||
ctx.APIErrorWithCode(apierrors.PermActionDenied)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &api.AIServiceStatusV2{
|
||||
Enabled: setting.AI.Enabled,
|
||||
ServiceURL: setting.AI.ServiceURL,
|
||||
}
|
||||
|
||||
if !setting.AI.Enabled {
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
|
||||
// Check AI service health
|
||||
client := ai_module.GetClient()
|
||||
health, err := client.CheckHealth(ctx)
|
||||
if err == nil && health != nil {
|
||||
resp.Healthy = health.Healthy
|
||||
resp.Version = health.Version
|
||||
resp.ProviderStatus = health.ProviderStatus
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// ListAllAIOperations returns the global AI operation log (admin only)
|
||||
func ListAllAIOperations(ctx *context.APIContext) {
|
||||
if !ctx.Doer.IsAdmin {
|
||||
ctx.APIErrorWithCode(apierrors.PermActionDenied)
|
||||
return
|
||||
}
|
||||
|
||||
if !setting.AI.Enabled {
|
||||
ctx.APIErrorWithCode(apierrors.AIDisabled)
|
||||
return
|
||||
}
|
||||
|
||||
page := max(ctx.FormInt("page"), 1)
|
||||
limit := ctx.FormInt("limit")
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
opts := ai_model.FindOperationLogsOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: limit,
|
||||
},
|
||||
Operation: ctx.FormString("operation"),
|
||||
Status: ctx.FormString("status"),
|
||||
Tier: ctx.FormInt("tier"),
|
||||
}
|
||||
|
||||
// Allow filtering by repo
|
||||
if repoID := ctx.FormInt64("repo_id"); repoID > 0 {
|
||||
opts.RepoID = repoID
|
||||
}
|
||||
|
||||
ops, err := db.Find[ai_model.OperationLog](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := db.Count[ai_model.OperationLog](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
result := make([]*api.AIOperationV2, 0, len(ops))
|
||||
for _, op := range ops {
|
||||
result = append(result, toAIOperationV2(op))
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, &api.AIOperationListV2{
|
||||
Operations: result,
|
||||
TotalCount: count,
|
||||
})
|
||||
}
|
||||
@@ -146,6 +146,8 @@ func Routes() *web.Router {
|
||||
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)
|
||||
m.Get("/workflows/status", repoAssignment(), ListWorkflowStatuses)
|
||||
m.Get("/runs/{run_id}/failure-log", repoAssignment(), GetRunFailureLog)
|
||||
})
|
||||
|
||||
// Releases v2 API - Enhanced releases with app update support
|
||||
@@ -169,10 +171,25 @@ func Routes() *web.Router {
|
||||
// Upload instructions endpoint
|
||||
m.Get("/upload/instructions", GetUploadInstructions)
|
||||
|
||||
// Public landing page API - for private repos with public_landing enabled
|
||||
// Landing page API
|
||||
m.Group("/repos/{owner}/{repo}/pages", func() {
|
||||
// Public read endpoints
|
||||
m.Get("/config", repoAssignmentWithPublicAccess(), GetPagesConfig)
|
||||
m.Get("/content", repoAssignmentWithPublicAccess(), GetPagesContent)
|
||||
|
||||
// Write endpoints (require auth + repo admin)
|
||||
m.Group("/config", func() {
|
||||
m.Put("", web.Bind(api.UpdatePagesConfigOption{}), UpdatePagesConfig)
|
||||
m.Patch("", web.Bind(api.UpdatePagesConfigOption{}), PatchPagesConfig)
|
||||
m.Put("/brand", web.Bind(api.UpdatePagesBrandOption{}), UpdatePagesBrand)
|
||||
m.Put("/hero", web.Bind(api.UpdatePagesHeroOption{}), UpdatePagesHero)
|
||||
m.Put("/content", web.Bind(api.UpdatePagesContentOption{}), UpdatePagesContentSection)
|
||||
m.Put("/comparison", web.Bind(api.UpdatePagesComparisonOption{}), UpdatePagesComparison)
|
||||
m.Put("/social", web.Bind(api.UpdatePagesSocialOption{}), UpdatePagesSocial)
|
||||
m.Put("/pricing", web.Bind(api.UpdatePagesPricingOption{}), UpdatePagesPricing)
|
||||
m.Put("/footer", web.Bind(api.UpdatePagesFooterOption{}), UpdatePagesFooter)
|
||||
m.Put("/theme", web.Bind(api.UpdatePagesThemeOption{}), UpdatePagesTheme)
|
||||
}, repoAssignment(), reqToken())
|
||||
})
|
||||
|
||||
// Blog v2 API - repository blog endpoints
|
||||
@@ -214,6 +231,34 @@ func Routes() *web.Router {
|
||||
m.Delete("", web.Bind(api.HiddenFolderOptionV2{}), RemoveHiddenFolderV2)
|
||||
}, repoAssignment(), reqToken())
|
||||
})
|
||||
|
||||
// AI operations API - repo-scoped AI operations and settings
|
||||
m.Group("/repos/{owner}/{repo}/ai", func() {
|
||||
m.Get("/settings", GetRepoAISettings)
|
||||
m.Get("/operations", ListAIOperations)
|
||||
m.Get("/operations/{id}", GetAIOperation)
|
||||
|
||||
m.Group("", func() {
|
||||
m.Put("/settings", web.Bind(api.UpdateAISettingsOption{}), UpdateRepoAISettings)
|
||||
m.Post("/review/{pull}", TriggerAIReview)
|
||||
m.Post("/respond/{issue}", TriggerAIRespond)
|
||||
m.Post("/triage/{issue}", TriggerAITriage)
|
||||
m.Post("/explain", web.Bind(api.AIExplainRequest{}), TriggerAIExplain)
|
||||
m.Post("/fix/{issue}", TriggerAIFix)
|
||||
}, reqToken())
|
||||
}, repoAssignment())
|
||||
|
||||
// AI settings API - org-scoped AI settings
|
||||
m.Group("/orgs/{org}/ai", func() {
|
||||
m.Get("/settings", GetOrgAISettingsV2)
|
||||
m.Put("/settings", web.Bind(api.UpdateOrgAISettingsOption{}), UpdateOrgAISettingsV2)
|
||||
}, reqToken())
|
||||
|
||||
// AI admin API - site admin AI management
|
||||
m.Group("/admin/ai", func() {
|
||||
m.Get("/status", GetAIServiceStatus)
|
||||
m.Get("/operations", ListAllAIOperations)
|
||||
}, reqToken())
|
||||
})
|
||||
|
||||
return m
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
actions_model "code.gitcaddy.com/server/v3/models/actions"
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
issue_model "code.gitcaddy.com/server/v3/models/issues"
|
||||
packages_model "code.gitcaddy.com/server/v3/models/packages"
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
secret_model "code.gitcaddy.com/server/v3/models/secret"
|
||||
@@ -572,6 +573,59 @@ var mcpTools = []MCPTool{
|
||||
"required": []string{"owner"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list_issues",
|
||||
Description: "List issues for a repository with pagination. Returns issue number, title, state, labels, poster, timestamps, and total count.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"state": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Filter by state: open, closed, or all (default: open)",
|
||||
"enum": []string{"open", "closed", "all"},
|
||||
},
|
||||
"page": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Page number (default 1)",
|
||||
},
|
||||
"limit": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Results per page (default 20, max 100)",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "get_issue",
|
||||
Description: "Get detailed information about a specific issue including body content and comments.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository owner",
|
||||
},
|
||||
"repo": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Repository name",
|
||||
},
|
||||
"number": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Issue number",
|
||||
},
|
||||
},
|
||||
"required": []string{"owner", "repo", "number"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// MCPHandler handles MCP protocol requests
|
||||
@@ -631,9 +685,10 @@ func handleInitialize(ctx *context_service.APIContext, req *MCPRequest) {
|
||||
}
|
||||
|
||||
func handleToolsList(ctx *context_service.APIContext, req *MCPRequest) {
|
||||
allTools := make([]MCPTool, 0, len(mcpTools)+len(mcpAITools))
|
||||
allTools := make([]MCPTool, 0, len(mcpTools)+len(mcpAITools)+len(mcpPagesTools))
|
||||
allTools = append(allTools, mcpTools...)
|
||||
allTools = append(allTools, mcpAITools...)
|
||||
allTools = append(allTools, mcpPagesTools...)
|
||||
result := MCPToolsListResult{Tools: allTools}
|
||||
sendMCPResult(ctx, req.ID, result)
|
||||
}
|
||||
@@ -701,6 +756,39 @@ func handleToolsCall(ctx *context_service.APIContext, req *MCPRequest) {
|
||||
result, err = toolGetPackageDefaults(ctx, params.Arguments)
|
||||
case "list_repos":
|
||||
result, err = toolListRepos(ctx, params.Arguments)
|
||||
case "list_issues":
|
||||
result, err = toolListIssues(ctx, params.Arguments)
|
||||
case "get_issue":
|
||||
result, err = toolGetIssue(ctx, params.Arguments)
|
||||
// Landing Pages tools
|
||||
case "get_landing_config":
|
||||
result, err = toolGetLandingConfig(ctx, params.Arguments)
|
||||
case "list_landing_templates":
|
||||
result, err = toolListLandingTemplates(ctx, params.Arguments)
|
||||
case "enable_landing_page":
|
||||
result, err = toolEnableLandingPage(ctx, params.Arguments)
|
||||
case "update_landing_brand":
|
||||
result, err = toolUpdateLandingBrand(ctx, params.Arguments)
|
||||
case "update_landing_hero":
|
||||
result, err = toolUpdateLandingHero(ctx, params.Arguments)
|
||||
case "update_landing_pricing":
|
||||
result, err = toolUpdateLandingPricing(ctx, params.Arguments)
|
||||
case "update_landing_comparison":
|
||||
result, err = toolUpdateLandingComparison(ctx, params.Arguments)
|
||||
case "update_landing_features":
|
||||
result, err = toolUpdateLandingFeatures(ctx, params.Arguments)
|
||||
case "update_landing_social_proof":
|
||||
result, err = toolUpdateLandingSocialProof(ctx, params.Arguments)
|
||||
case "update_landing_seo":
|
||||
result, err = toolUpdateLandingSEO(ctx, params.Arguments)
|
||||
case "update_landing_theme":
|
||||
result, err = toolUpdateLandingTheme(ctx, params.Arguments)
|
||||
case "update_landing_stats":
|
||||
result, err = toolUpdateLandingStats(ctx, params.Arguments)
|
||||
case "update_landing_value_props":
|
||||
result, err = toolUpdateLandingValueProps(ctx, params.Arguments)
|
||||
case "update_landing_cta":
|
||||
result, err = toolUpdateLandingCTA(ctx, params.Arguments)
|
||||
default:
|
||||
sendMCPError(ctx, req.ID, -32602, "Unknown tool", params.Name)
|
||||
return
|
||||
@@ -2196,8 +2284,9 @@ func toolListRepos(ctx *context_service.APIContext, args map[string]any) (any, e
|
||||
Page: 1,
|
||||
PageSize: limit,
|
||||
},
|
||||
Actor: ctx.Doer,
|
||||
OwnerID: ownerUser.ID,
|
||||
Private: true,
|
||||
Private: ctx.Doer != nil,
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
Archived: optional.Some(false),
|
||||
})
|
||||
@@ -2229,6 +2318,201 @@ func toolListRepos(ctx *context_service.APIContext, args map[string]any) (any, e
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolListIssues(ctx *context_service.APIContext, args map[string]any) (any, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
if owner == "" || repo == "" {
|
||||
return nil, errors.New("owner and repo are required")
|
||||
}
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
page := 1
|
||||
if p, ok := args["page"].(float64); ok && p > 0 {
|
||||
page = int(p)
|
||||
}
|
||||
|
||||
limit := 20
|
||||
if l, ok := args["limit"].(float64); ok && l > 0 {
|
||||
limit = min(int(l), 100)
|
||||
}
|
||||
|
||||
opts := &issue_model.IssuesOptions{
|
||||
Paginator: &db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: limit,
|
||||
},
|
||||
RepoIDs: []int64{repository.ID},
|
||||
IsPull: optional.Some(false),
|
||||
SortType: "newest",
|
||||
}
|
||||
|
||||
state, _ := args["state"].(string)
|
||||
switch state {
|
||||
case "closed":
|
||||
opts.IsClosed = optional.Some(true)
|
||||
case "all":
|
||||
// no filter
|
||||
default:
|
||||
opts.IsClosed = optional.Some(false)
|
||||
}
|
||||
|
||||
issues, err := issue_model.Issues(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list issues: %w", err)
|
||||
}
|
||||
|
||||
totalCount, err := issue_model.CountIssues(ctx, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to count issues: %w", err)
|
||||
}
|
||||
|
||||
if err := issues.LoadPosters(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to load posters: %w", err)
|
||||
}
|
||||
if err := issues.LoadLabels(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to load labels: %w", err)
|
||||
}
|
||||
|
||||
items := make([]map[string]any, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
labels := make([]string, 0, len(issue.Labels))
|
||||
for _, l := range issue.Labels {
|
||||
labels = append(labels, l.Name)
|
||||
}
|
||||
|
||||
posterName := ""
|
||||
if issue.Poster != nil {
|
||||
posterName = issue.Poster.Name
|
||||
}
|
||||
|
||||
issueState := "open"
|
||||
if issue.IsClosed {
|
||||
issueState = "closed"
|
||||
}
|
||||
|
||||
items = append(items, map[string]any{
|
||||
"number": issue.Index,
|
||||
"title": issue.Title,
|
||||
"state": issueState,
|
||||
"poster": posterName,
|
||||
"labels": labels,
|
||||
"num_comments": issue.NumComments,
|
||||
"created_at": issue.CreatedUnix.AsTime().Format(time.RFC3339),
|
||||
"updated_at": issue.UpdatedUnix.AsTime().Format(time.RFC3339),
|
||||
"url": fmt.Sprintf("%s%s/%s/issues/%d", setting.AppURL, owner, repo, issue.Index),
|
||||
})
|
||||
}
|
||||
|
||||
totalPages := (int(totalCount) + limit - 1) / limit
|
||||
|
||||
return map[string]any{
|
||||
"repository": fmt.Sprintf("%s/%s", owner, repo),
|
||||
"total_count": totalCount,
|
||||
"page": page,
|
||||
"page_size": limit,
|
||||
"total_pages": totalPages,
|
||||
"issues": items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toolGetIssue(ctx *context_service.APIContext, args map[string]any) (any, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
if owner == "" || repo == "" {
|
||||
return nil, errors.New("owner and repo are required")
|
||||
}
|
||||
|
||||
number, ok := args["number"].(float64)
|
||||
if !ok || number < 1 {
|
||||
return nil, errors.New("number is required and must be a positive integer")
|
||||
}
|
||||
|
||||
repository, err := repo_model.GetRepositoryByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repository not found: %s/%s", owner, repo)
|
||||
}
|
||||
|
||||
issue, err := issue_model.GetIssueByIndex(ctx, repository.ID, int64(number))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("issue #%d not found", int64(number))
|
||||
}
|
||||
|
||||
if err := issue.LoadPoster(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to load poster: %w", err)
|
||||
}
|
||||
if err := issue.LoadLabels(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to load labels: %w", err)
|
||||
}
|
||||
|
||||
labels := make([]string, 0, len(issue.Labels))
|
||||
for _, l := range issue.Labels {
|
||||
labels = append(labels, l.Name)
|
||||
}
|
||||
|
||||
posterName := ""
|
||||
if issue.Poster != nil {
|
||||
posterName = issue.Poster.Name
|
||||
}
|
||||
|
||||
state := "open"
|
||||
if issue.IsClosed {
|
||||
state = "closed"
|
||||
}
|
||||
|
||||
result := map[string]any{
|
||||
"number": issue.Index,
|
||||
"title": issue.Title,
|
||||
"state": state,
|
||||
"body": issue.Content,
|
||||
"poster": posterName,
|
||||
"labels": labels,
|
||||
"num_comments": issue.NumComments,
|
||||
"created_at": issue.CreatedUnix.AsTime().Format(time.RFC3339),
|
||||
"updated_at": issue.UpdatedUnix.AsTime().Format(time.RFC3339),
|
||||
"url": fmt.Sprintf("%s%s/%s/issues/%d", setting.AppURL, owner, repo, issue.Index),
|
||||
}
|
||||
|
||||
if issue.IsClosed && !issue.ClosedUnix.IsZero() {
|
||||
result["closed_at"] = issue.ClosedUnix.AsTime().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// Load comments
|
||||
comments, err := issue_model.FindComments(ctx, &issue_model.FindCommentsOptions{
|
||||
IssueID: issue.ID,
|
||||
Type: issue_model.CommentTypeComment,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load comments: %w", err)
|
||||
}
|
||||
|
||||
if len(comments) > 0 {
|
||||
if err := comments.LoadPosters(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to load comment posters: %w", err)
|
||||
}
|
||||
|
||||
commentItems := make([]map[string]any, 0, len(comments))
|
||||
for _, c := range comments {
|
||||
cPoster := ""
|
||||
if c.Poster != nil {
|
||||
cPoster = c.Poster.Name
|
||||
}
|
||||
commentItems = append(commentItems, map[string]any{
|
||||
"id": c.ID,
|
||||
"body": c.Content,
|
||||
"poster": cPoster,
|
||||
"created_at": c.CreatedUnix.AsTime().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
result["comments"] = commentItems
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// validateWorkflowContent runs YAML and workflow-structural validation on raw workflow content.
|
||||
// Returns a list of error strings (empty means valid) and a list of warning strings.
|
||||
func validateWorkflowContent(content []byte) (errs, warnings []string) {
|
||||
|
||||
794
routers/api/v2/mcp_pages.go
Normal file
794
routers/api/v2/mcp_pages.go
Normal file
@@ -0,0 +1,794 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
user_model "code.gitcaddy.com/server/v3/models/user"
|
||||
"code.gitcaddy.com/server/v3/modules/json"
|
||||
pages_module "code.gitcaddy.com/server/v3/modules/pages"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
pages_service "code.gitcaddy.com/server/v3/services/pages"
|
||||
)
|
||||
|
||||
// Landing Pages MCP Tools
|
||||
var mcpPagesTools = []MCPTool{
|
||||
{
|
||||
Name: "get_landing_config",
|
||||
Description: "Get the full landing page configuration for a repository. Returns all sections: brand, hero, pricing, comparison, features, social proof, SEO, and more.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list_landing_templates",
|
||||
Description: "List available landing page templates with display names. Templates include: open-source-hero, saas-conversion, bold-marketing, developer-tool, and more.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "enable_landing_page",
|
||||
Description: "Enable or disable the landing page for a repository. Optionally set a template.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo", "enabled"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"enabled": map[string]any{"type": "boolean", "description": "Enable or disable"},
|
||||
"template": map[string]any{"type": "string", "description": "Template name (e.g., 'saas-conversion', 'open-source-hero')"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_brand",
|
||||
Description: "Update the brand section of a landing page: name, logo URL, tagline, favicon.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"name": map[string]any{"type": "string", "description": "Brand name"},
|
||||
"logo_url": map[string]any{"type": "string", "description": "Logo image URL"},
|
||||
"tagline": map[string]any{"type": "string", "description": "Brand tagline"},
|
||||
"favicon_url": map[string]any{"type": "string", "description": "Favicon URL"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_hero",
|
||||
Description: "Update the hero section: headline, subheadline, CTA buttons, hero image or video URL.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"headline": map[string]any{"type": "string", "description": "Main headline"},
|
||||
"subheadline": map[string]any{"type": "string", "description": "Supporting text"},
|
||||
"image_url": map[string]any{"type": "string", "description": "Hero image URL"},
|
||||
"video_url": map[string]any{"type": "string", "description": "Hero video URL"},
|
||||
"primary_cta_label": map[string]any{"type": "string", "description": "Primary CTA button label"},
|
||||
"primary_cta_url": map[string]any{"type": "string", "description": "Primary CTA button URL"},
|
||||
"secondary_cta_label": map[string]any{"type": "string", "description": "Secondary CTA button label"},
|
||||
"secondary_cta_url": map[string]any{"type": "string", "description": "Secondary CTA button URL"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_pricing",
|
||||
Description: "Update the pricing section with plans. Each plan has a name, price, period, feature list, CTA, and optional 'featured' flag.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo", "plans"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"headline": map[string]any{"type": "string", "description": "Pricing section headline"},
|
||||
"subheadline": map[string]any{"type": "string", "description": "Pricing section subheadline"},
|
||||
"plans": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"name": map[string]any{"type": "string"},
|
||||
"price": map[string]any{"type": "string"},
|
||||
"period": map[string]any{"type": "string"},
|
||||
"features": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
||||
"cta": map[string]any{"type": "string"},
|
||||
"featured": map[string]any{"type": "boolean"},
|
||||
},
|
||||
},
|
||||
"description": "Array of pricing plans",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_comparison",
|
||||
Description: "Update the feature comparison matrix. Define columns (products/tiers) and groups of features with per-column values.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"enabled": map[string]any{"type": "boolean", "description": "Enable comparison section"},
|
||||
"headline": map[string]any{"type": "string", "description": "Section headline"},
|
||||
"subheadline": map[string]any{"type": "string", "description": "Section subheadline"},
|
||||
"columns": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{"type": "string"},
|
||||
"description": "Column headers (e.g., ['Free', 'Pro', 'Enterprise'])",
|
||||
},
|
||||
"groups": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"name": map[string]any{"type": "string"},
|
||||
"features": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"name": map[string]any{"type": "string"},
|
||||
"values": map[string]any{"type": "array", "items": map[string]any{"type": "string"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"description": "Feature groups with per-column values",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_features",
|
||||
Description: "Update the features section with title, description, and optional icon/image for each feature.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo", "features"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"features": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"title": map[string]any{"type": "string"},
|
||||
"description": map[string]any{"type": "string"},
|
||||
"icon": map[string]any{"type": "string"},
|
||||
"image_url": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
"description": "Array of features",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_social_proof",
|
||||
Description: "Update testimonials and client logos for social proof.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"logos": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{"type": "string"},
|
||||
"description": "Client/partner logo URLs",
|
||||
},
|
||||
"testimonials": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"quote": map[string]any{"type": "string"},
|
||||
"author": map[string]any{"type": "string"},
|
||||
"role": map[string]any{"type": "string"},
|
||||
"avatar": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
"description": "Testimonials",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_seo",
|
||||
Description: "Update SEO metadata: title, description, keywords, Open Graph image, Twitter card settings.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"title": map[string]any{"type": "string", "description": "SEO title"},
|
||||
"description": map[string]any{"type": "string", "description": "SEO description"},
|
||||
"keywords": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "SEO keywords"},
|
||||
"og_image": map[string]any{"type": "string", "description": "Open Graph image URL"},
|
||||
"twitter_card": map[string]any{"type": "string", "description": "Twitter card type (summary, summary_large_image)"},
|
||||
"twitter_site": map[string]any{"type": "string", "description": "Twitter @handle"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_theme",
|
||||
Description: "Update the visual theme: primary color, accent color, light/dark/auto mode.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"primary_color": map[string]any{"type": "string", "description": "Primary brand color (hex, e.g., '#512BD4')"},
|
||||
"accent_color": map[string]any{"type": "string", "description": "Accent color (hex)"},
|
||||
"mode": map[string]any{"type": "string", "enum": []string{"light", "dark", "auto"}, "description": "Color mode"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_stats",
|
||||
Description: "Update the stats counters displayed on the landing page. Each stat has a value and label.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo", "stats"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"stats": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"value": map[string]any{"type": "string"},
|
||||
"label": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
"description": "Array of stat counters (e.g., [{value: '15+', label: 'Tools'}])",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_value_props",
|
||||
Description: "Update the value propositions section. Each value prop has a title, description, and icon.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo", "value_props"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"value_props": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"title": map[string]any{"type": "string"},
|
||||
"description": map[string]any{"type": "string"},
|
||||
"icon": map[string]any{"type": "string"},
|
||||
},
|
||||
},
|
||||
"description": "Array of value propositions",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update_landing_cta",
|
||||
Description: "Update the call-to-action section at the bottom of the page with headline, subheadline, and button.",
|
||||
InputSchema: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"owner", "repo"},
|
||||
"properties": map[string]any{
|
||||
"owner": map[string]any{"type": "string", "description": "Repository owner"},
|
||||
"repo": map[string]any{"type": "string", "description": "Repository name"},
|
||||
"headline": map[string]any{"type": "string", "description": "CTA headline"},
|
||||
"subheadline": map[string]any{"type": "string", "description": "CTA subheadline"},
|
||||
"button_label": map[string]any{"type": "string", "description": "Button text"},
|
||||
"button_url": map[string]any{"type": "string", "description": "Button URL"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ── Tool Implementations ──────────────────────────────────
|
||||
|
||||
func toolGetLandingConfig(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
owner, repo, err := resolveOwnerRepo(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoObj, err := getRepoByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config, err := pages_service.GetPagesConfig(ctx, repoObj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get pages config: %w", err)
|
||||
}
|
||||
if config == nil {
|
||||
return map[string]any{"enabled": false, "message": "No landing page configured"}, nil
|
||||
}
|
||||
|
||||
return buildFullResponse(config), nil
|
||||
}
|
||||
|
||||
func toolListLandingTemplates(_ *context.APIContext, _ map[string]any) (any, error) { //nolint:unparam // signature must match tool handler type
|
||||
templates := pages_module.ValidTemplates()
|
||||
displayNames := pages_module.TemplateDisplayNames()
|
||||
|
||||
result := make([]map[string]string, 0, len(templates))
|
||||
for _, t := range templates {
|
||||
result = append(result, map[string]string{
|
||||
"id": t,
|
||||
"name": displayNames[t],
|
||||
})
|
||||
}
|
||||
return map[string]any{"templates": result}, nil
|
||||
}
|
||||
|
||||
func toolEnableLandingPage(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
owner, repo, err := resolveOwnerRepo(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoObj, err := getRepoByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enabled, _ := args["enabled"].(bool)
|
||||
template, _ := args["template"].(string)
|
||||
|
||||
if enabled {
|
||||
if template == "" {
|
||||
template = "open-source-hero"
|
||||
}
|
||||
if !pages_module.IsValidTemplate(template) {
|
||||
return nil, fmt.Errorf("invalid template: %s", template)
|
||||
}
|
||||
if err := pages_service.EnablePages(ctx, repoObj, template); err != nil {
|
||||
return nil, fmt.Errorf("enable pages: %w", err)
|
||||
}
|
||||
return map[string]any{"enabled": true, "template": template}, nil
|
||||
}
|
||||
|
||||
if err := pages_service.DisablePages(ctx, repoObj); err != nil {
|
||||
return nil, fmt.Errorf("disable pages: %w", err)
|
||||
}
|
||||
return map[string]any{"enabled": false}, nil
|
||||
}
|
||||
|
||||
func toolUpdateLandingBrand(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["name"].(string); ok {
|
||||
config.Brand.Name = v
|
||||
}
|
||||
if v, ok := args["logo_url"].(string); ok {
|
||||
config.Brand.LogoURL = v
|
||||
}
|
||||
if v, ok := args["tagline"].(string); ok {
|
||||
config.Brand.Tagline = v
|
||||
}
|
||||
if v, ok := args["favicon_url"].(string); ok {
|
||||
config.Brand.FaviconURL = v
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "brand")
|
||||
}
|
||||
|
||||
func toolUpdateLandingHero(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["headline"].(string); ok {
|
||||
config.Hero.Headline = v
|
||||
}
|
||||
if v, ok := args["subheadline"].(string); ok {
|
||||
config.Hero.Subheadline = v
|
||||
}
|
||||
if v, ok := args["image_url"].(string); ok {
|
||||
config.Hero.ImageURL = v
|
||||
}
|
||||
if v, ok := args["video_url"].(string); ok {
|
||||
config.Hero.VideoURL = v
|
||||
}
|
||||
if v, ok := args["primary_cta_label"].(string); ok {
|
||||
config.Hero.PrimaryCTA.Label = v
|
||||
}
|
||||
if v, ok := args["primary_cta_url"].(string); ok {
|
||||
config.Hero.PrimaryCTA.URL = v
|
||||
}
|
||||
if v, ok := args["secondary_cta_label"].(string); ok {
|
||||
config.Hero.SecondaryCTA.Label = v
|
||||
}
|
||||
if v, ok := args["secondary_cta_url"].(string); ok {
|
||||
config.Hero.SecondaryCTA.URL = v
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "hero")
|
||||
}
|
||||
|
||||
func toolUpdateLandingPricing(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["headline"].(string); ok {
|
||||
config.Pricing.Headline = v
|
||||
}
|
||||
if v, ok := args["subheadline"].(string); ok {
|
||||
config.Pricing.Subheadline = v
|
||||
}
|
||||
if plans, ok := args["plans"].([]any); ok {
|
||||
config.Pricing.Plans = nil
|
||||
for _, p := range plans {
|
||||
if pm, ok := p.(map[string]any); ok {
|
||||
plan := pages_module.PricingPlanConfig{
|
||||
Name: strVal(pm, "name"),
|
||||
Price: strVal(pm, "price"),
|
||||
Period: strVal(pm, "period"),
|
||||
CTA: strVal(pm, "cta"),
|
||||
Featured: boolVal(pm, "featured"),
|
||||
}
|
||||
if features, ok := pm["features"].([]any); ok {
|
||||
for _, f := range features {
|
||||
if s, ok := f.(string); ok {
|
||||
plan.Features = append(plan.Features, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
config.Pricing.Plans = append(config.Pricing.Plans, plan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "pricing")
|
||||
}
|
||||
|
||||
func toolUpdateLandingComparison(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["enabled"].(bool); ok {
|
||||
config.Comparison.Enabled = v
|
||||
}
|
||||
if v, ok := args["headline"].(string); ok {
|
||||
config.Comparison.Headline = v
|
||||
}
|
||||
if v, ok := args["subheadline"].(string); ok {
|
||||
config.Comparison.Subheadline = v
|
||||
}
|
||||
if cols, ok := args["columns"].([]any); ok {
|
||||
config.Comparison.Columns = nil
|
||||
for _, c := range cols {
|
||||
if s, ok := c.(string); ok {
|
||||
config.Comparison.Columns = append(config.Comparison.Columns, pages_module.ComparisonColumnConfig{Name: s})
|
||||
} else if cm, ok := c.(map[string]any); ok {
|
||||
config.Comparison.Columns = append(config.Comparison.Columns, pages_module.ComparisonColumnConfig{
|
||||
Name: strVal(cm, "name"),
|
||||
Highlight: boolVal(cm, "highlight"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if groups, ok := args["groups"].([]any); ok {
|
||||
config.Comparison.Groups = nil
|
||||
for _, g := range groups {
|
||||
if gm, ok := g.(map[string]any); ok {
|
||||
group := pages_module.ComparisonGroupConfig{Name: strVal(gm, "name")}
|
||||
if features, ok := gm["features"].([]any); ok {
|
||||
for _, f := range features {
|
||||
if fm, ok := f.(map[string]any); ok {
|
||||
feature := pages_module.ComparisonFeatureConfig{Name: strVal(fm, "name")}
|
||||
if vals, ok := fm["values"].([]any); ok {
|
||||
for _, v := range vals {
|
||||
if s, ok := v.(string); ok {
|
||||
feature.Values = append(feature.Values, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
group.Features = append(group.Features, feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
config.Comparison.Groups = append(config.Comparison.Groups, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "comparison")
|
||||
}
|
||||
|
||||
func toolUpdateLandingFeatures(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if features, ok := args["features"].([]any); ok {
|
||||
config.Features = nil
|
||||
for _, f := range features {
|
||||
if fm, ok := f.(map[string]any); ok {
|
||||
config.Features = append(config.Features, pages_module.FeatureConfig{
|
||||
Title: strVal(fm, "title"),
|
||||
Description: strVal(fm, "description"),
|
||||
Icon: strVal(fm, "icon"),
|
||||
ImageURL: strVal(fm, "image_url"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "features")
|
||||
}
|
||||
|
||||
func toolUpdateLandingSocialProof(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if logos, ok := args["logos"].([]any); ok {
|
||||
config.SocialProof.Logos = nil
|
||||
for _, l := range logos {
|
||||
if s, ok := l.(string); ok {
|
||||
config.SocialProof.Logos = append(config.SocialProof.Logos, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if testimonials, ok := args["testimonials"].([]any); ok {
|
||||
config.SocialProof.Testimonials = nil
|
||||
for _, t := range testimonials {
|
||||
if tm, ok := t.(map[string]any); ok {
|
||||
config.SocialProof.Testimonials = append(config.SocialProof.Testimonials, pages_module.TestimonialConfig{
|
||||
Quote: strVal(tm, "quote"),
|
||||
Author: strVal(tm, "author"),
|
||||
Role: strVal(tm, "role"),
|
||||
Avatar: strVal(tm, "avatar"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "social_proof")
|
||||
}
|
||||
|
||||
func toolUpdateLandingSEO(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["title"].(string); ok {
|
||||
config.SEO.Title = v
|
||||
}
|
||||
if v, ok := args["description"].(string); ok {
|
||||
config.SEO.Description = v
|
||||
}
|
||||
if v, ok := args["og_image"].(string); ok {
|
||||
config.SEO.OGImage = v
|
||||
}
|
||||
if v, ok := args["twitter_card"].(string); ok {
|
||||
config.SEO.TwitterCard = v
|
||||
}
|
||||
if v, ok := args["twitter_site"].(string); ok {
|
||||
config.SEO.TwitterSite = v
|
||||
}
|
||||
if keywords, ok := args["keywords"].([]any); ok {
|
||||
config.SEO.Keywords = nil
|
||||
for _, k := range keywords {
|
||||
if s, ok := k.(string); ok {
|
||||
config.SEO.Keywords = append(config.SEO.Keywords, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "seo")
|
||||
}
|
||||
|
||||
func toolUpdateLandingTheme(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["primary_color"].(string); ok {
|
||||
config.Theme.PrimaryColor = v
|
||||
}
|
||||
if v, ok := args["accent_color"].(string); ok {
|
||||
config.Theme.AccentColor = v
|
||||
}
|
||||
if v, ok := args["mode"].(string); ok {
|
||||
config.Theme.Mode = v
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "theme")
|
||||
}
|
||||
|
||||
func toolUpdateLandingStats(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stats, ok := args["stats"].([]any); ok {
|
||||
config.Stats = nil
|
||||
for _, s := range stats {
|
||||
if sm, ok := s.(map[string]any); ok {
|
||||
config.Stats = append(config.Stats, pages_module.StatConfig{
|
||||
Value: strVal(sm, "value"),
|
||||
Label: strVal(sm, "label"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "stats")
|
||||
}
|
||||
|
||||
func toolUpdateLandingValueProps(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if vps, ok := args["value_props"].([]any); ok {
|
||||
config.ValueProps = nil
|
||||
for _, v := range vps {
|
||||
if vm, ok := v.(map[string]any); ok {
|
||||
config.ValueProps = append(config.ValueProps, pages_module.ValuePropConfig{
|
||||
Title: strVal(vm, "title"),
|
||||
Description: strVal(vm, "description"),
|
||||
Icon: strVal(vm, "icon"),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "value_props")
|
||||
}
|
||||
|
||||
func toolUpdateLandingCTA(ctx *context.APIContext, args map[string]any) (any, error) {
|
||||
config, repoObj, err := getConfigForUpdate(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v, ok := args["headline"].(string); ok {
|
||||
config.CTASection.Headline = v
|
||||
}
|
||||
if v, ok := args["subheadline"].(string); ok {
|
||||
config.CTASection.Subheadline = v
|
||||
}
|
||||
if v, ok := args["button_label"].(string); ok {
|
||||
config.CTASection.Button.Label = v
|
||||
}
|
||||
if v, ok := args["button_url"].(string); ok {
|
||||
config.CTASection.Button.URL = v
|
||||
}
|
||||
|
||||
return saveAndReturn(ctx, repoObj, config, "cta_section")
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────
|
||||
|
||||
func getConfigForUpdate(ctx *context.APIContext, args map[string]any) (*pages_module.LandingConfig, *repo_model.Repository, error) {
|
||||
owner, repo, err := resolveOwnerRepo(args)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
repoObj, err := getRepoByOwnerAndName(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
config, err := pages_service.GetPagesConfig(ctx, repoObj)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("get config: %w", err)
|
||||
}
|
||||
if config == nil {
|
||||
config = pages_module.DefaultConfig()
|
||||
}
|
||||
|
||||
return config, repoObj, nil
|
||||
}
|
||||
|
||||
func saveAndReturn(ctx *context.APIContext, repo *repo_model.Repository, config *pages_module.LandingConfig, section string) (any, error) {
|
||||
configJSON, _ := json.Marshal(config)
|
||||
hash := pages_module.HashConfig(configJSON)
|
||||
|
||||
existing, _ := repo_model.GetPagesConfigByRepoID(ctx, repo.ID)
|
||||
|
||||
if existing != nil {
|
||||
existing.ConfigJSON = string(configJSON)
|
||||
existing.ConfigHash = hash
|
||||
existing.Template = repo_model.PagesTemplate(config.Template)
|
||||
existing.Enabled = config.Enabled
|
||||
if err := repo_model.UpdatePagesConfig(ctx, existing); err != nil {
|
||||
return nil, fmt.Errorf("save config: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := repo_model.CreatePagesConfig(ctx, &repo_model.PagesConfig{
|
||||
RepoID: repo.ID,
|
||||
Enabled: config.Enabled,
|
||||
Template: repo_model.PagesTemplate(config.Template),
|
||||
ConfigJSON: string(configJSON),
|
||||
ConfigHash: hash,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("create config: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"success": true,
|
||||
"section": section,
|
||||
"message": fmt.Sprintf("Updated %s section", section),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func strVal(m map[string]any, key string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func boolVal(m map[string]any, key string) bool {
|
||||
if v, ok := m[key].(bool); ok {
|
||||
return v
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func resolveOwnerRepo(args map[string]any) (string, string, error) {
|
||||
owner, _ := args["owner"].(string)
|
||||
repo, _ := args["repo"].(string)
|
||||
if owner == "" || repo == "" {
|
||||
return "", "", errors.New("owner and repo are required")
|
||||
}
|
||||
return owner, repo, nil
|
||||
}
|
||||
|
||||
func getRepoByOwnerAndName(ctx *context.APIContext, owner, repo string) (*repo_model.Repository, error) {
|
||||
ownerObj, err := user_model.GetUserByName(ctx, owner)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("owner not found: %s", owner)
|
||||
}
|
||||
repoObj, err := repo_model.GetRepositoryByName(ctx, ownerObj.ID, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("repo not found: %s/%s", owner, repo)
|
||||
}
|
||||
return repoObj, nil
|
||||
}
|
||||
@@ -7,22 +7,39 @@ import (
|
||||
"net/http"
|
||||
|
||||
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
||||
apierrors "code.gitcaddy.com/server/v3/modules/errors"
|
||||
"code.gitcaddy.com/server/v3/modules/git"
|
||||
"code.gitcaddy.com/server/v3/modules/json"
|
||||
pages_module "code.gitcaddy.com/server/v3/modules/pages"
|
||||
api "code.gitcaddy.com/server/v3/modules/structs"
|
||||
"code.gitcaddy.com/server/v3/modules/web"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
pages_service "code.gitcaddy.com/server/v3/services/pages"
|
||||
)
|
||||
|
||||
// PagesConfigResponse represents the pages configuration for a repository
|
||||
type PagesConfigResponse struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PublicLanding bool `json:"public_landing"`
|
||||
Template string `json:"template"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Brand pages_module.BrandConfig `json:"brand"`
|
||||
Hero pages_module.HeroConfig `json:"hero"`
|
||||
SEO pages_module.SEOConfig `json:"seo"`
|
||||
Footer pages_module.FooterConfig `json:"footer"`
|
||||
// PagesFullConfigResponse represents the complete landing page configuration
|
||||
type PagesFullConfigResponse struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PublicLanding bool `json:"public_landing"`
|
||||
Template string `json:"template"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Brand pages_module.BrandConfig `json:"brand"`
|
||||
Hero pages_module.HeroConfig `json:"hero"`
|
||||
Stats []pages_module.StatConfig `json:"stats"`
|
||||
ValueProps []pages_module.ValuePropConfig `json:"value_props"`
|
||||
Features []pages_module.FeatureConfig `json:"features"`
|
||||
SocialProof pages_module.SocialProofConfig `json:"social_proof"`
|
||||
Pricing pages_module.PricingConfig `json:"pricing"`
|
||||
CTASection pages_module.CTASectionConfig `json:"cta_section"`
|
||||
Blog pages_module.BlogSectionConfig `json:"blog"`
|
||||
Gallery pages_module.GallerySectionConfig `json:"gallery"`
|
||||
Comparison pages_module.ComparisonSectionConfig `json:"comparison"`
|
||||
Navigation pages_module.NavigationConfig `json:"navigation"`
|
||||
Footer pages_module.FooterConfig `json:"footer"`
|
||||
Theme pages_module.ThemeConfig `json:"theme"`
|
||||
SEO pages_module.SEOConfig `json:"seo"`
|
||||
Analytics pages_module.AnalyticsConfig `json:"analytics"`
|
||||
Advanced pages_module.AdvancedConfig `json:"advanced"`
|
||||
}
|
||||
|
||||
// PagesContentResponse represents the rendered content for a landing page
|
||||
@@ -32,7 +49,433 @@ type PagesContentResponse struct {
|
||||
Readme string `json:"readme,omitempty"`
|
||||
}
|
||||
|
||||
// GetPagesConfig returns the pages configuration for a repository
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func buildFullResponse(config *pages_module.LandingConfig) *PagesFullConfigResponse {
|
||||
return &PagesFullConfigResponse{
|
||||
Enabled: config.Enabled,
|
||||
PublicLanding: config.PublicLanding,
|
||||
Template: config.Template,
|
||||
Domain: config.Domain,
|
||||
Brand: config.Brand,
|
||||
Hero: config.Hero,
|
||||
Stats: config.Stats,
|
||||
ValueProps: config.ValueProps,
|
||||
Features: config.Features,
|
||||
SocialProof: config.SocialProof,
|
||||
Pricing: config.Pricing,
|
||||
CTASection: config.CTASection,
|
||||
Blog: config.Blog,
|
||||
Gallery: config.Gallery,
|
||||
Comparison: config.Comparison,
|
||||
Navigation: config.Navigation,
|
||||
Footer: config.Footer,
|
||||
Theme: config.Theme,
|
||||
SEO: config.SEO,
|
||||
Analytics: config.Analytics,
|
||||
Advanced: config.Advanced,
|
||||
}
|
||||
}
|
||||
|
||||
func getPagesConfigAPI(ctx *context.APIContext) (*pages_module.LandingConfig, bool) {
|
||||
config, err := pages_service.GetPagesConfig(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.APIErrorWithCode(apierrors.PagesNotConfigured)
|
||||
return nil, false
|
||||
}
|
||||
return config, true
|
||||
}
|
||||
|
||||
func savePagesConfigAPI(ctx *context.APIContext, config *pages_module.LandingConfig) bool {
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return false
|
||||
}
|
||||
|
||||
dbConfig, err := repo_model.GetPagesConfigByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
if repo_model.IsErrPagesConfigNotExist(err) {
|
||||
dbConfig = &repo_model.PagesConfig{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Enabled: config.Enabled,
|
||||
Template: repo_model.PagesTemplate(config.Template),
|
||||
ConfigJSON: string(configJSON),
|
||||
}
|
||||
if err := repo_model.CreatePagesConfig(ctx, dbConfig); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
ctx.APIErrorInternal(err)
|
||||
return false
|
||||
}
|
||||
|
||||
dbConfig.Enabled = config.Enabled
|
||||
dbConfig.Template = repo_model.PagesTemplate(config.Template)
|
||||
dbConfig.ConfigJSON = string(configJSON)
|
||||
if err := repo_model.UpdatePagesConfig(ctx, dbConfig); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func requirePagesAdmin(ctx *context.APIContext) bool {
|
||||
if !ctx.Repo.Permission.IsAdmin() && !ctx.IsUserSiteAdmin() {
|
||||
ctx.APIErrorWithCode(apierrors.PermRepoAdminRequired)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Apply helpers — map API option structs to config structs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func applyCTAButton(dst *pages_module.CTAButton, src *api.PagesCTAButtonOption) {
|
||||
if src == nil {
|
||||
return
|
||||
}
|
||||
if src.Label != nil {
|
||||
dst.Label = *src.Label
|
||||
}
|
||||
if src.URL != nil {
|
||||
dst.URL = *src.URL
|
||||
}
|
||||
if src.Variant != nil {
|
||||
dst.Variant = *src.Variant
|
||||
}
|
||||
}
|
||||
|
||||
func applyBrand(dst *pages_module.BrandConfig, src *api.UpdatePagesBrandOption) {
|
||||
if src.Name != nil {
|
||||
dst.Name = *src.Name
|
||||
}
|
||||
if src.LogoURL != nil {
|
||||
dst.LogoURL = *src.LogoURL
|
||||
}
|
||||
if src.Tagline != nil {
|
||||
dst.Tagline = *src.Tagline
|
||||
}
|
||||
if src.FaviconURL != nil {
|
||||
dst.FaviconURL = *src.FaviconURL
|
||||
}
|
||||
}
|
||||
|
||||
func applyHero(dst *pages_module.HeroConfig, src *api.UpdatePagesHeroOption) {
|
||||
if src.Headline != nil {
|
||||
dst.Headline = *src.Headline
|
||||
}
|
||||
if src.Subheadline != nil {
|
||||
dst.Subheadline = *src.Subheadline
|
||||
}
|
||||
if src.ImageURL != nil {
|
||||
dst.ImageURL = *src.ImageURL
|
||||
}
|
||||
if src.VideoURL != nil {
|
||||
dst.VideoURL = *src.VideoURL
|
||||
}
|
||||
if src.CodeExample != nil {
|
||||
dst.CodeExample = *src.CodeExample
|
||||
}
|
||||
applyCTAButton(&dst.PrimaryCTA, src.PrimaryCTA)
|
||||
applyCTAButton(&dst.SecondaryCTA, src.SecondaryCTA)
|
||||
}
|
||||
|
||||
func applyStats(config *pages_module.LandingConfig, src []api.PagesStatOption) {
|
||||
config.Stats = make([]pages_module.StatConfig, len(src))
|
||||
for i, s := range src {
|
||||
config.Stats[i] = pages_module.StatConfig{Value: s.Value, Label: s.Label}
|
||||
}
|
||||
}
|
||||
|
||||
func applyValueProps(config *pages_module.LandingConfig, src []api.PagesValuePropOption) {
|
||||
config.ValueProps = make([]pages_module.ValuePropConfig, len(src))
|
||||
for i, v := range src {
|
||||
config.ValueProps[i] = pages_module.ValuePropConfig{Title: v.Title, Description: v.Description, Icon: v.Icon}
|
||||
}
|
||||
}
|
||||
|
||||
func applyFeatures(config *pages_module.LandingConfig, src []api.PagesFeatureOption) {
|
||||
config.Features = make([]pages_module.FeatureConfig, len(src))
|
||||
for i, f := range src {
|
||||
config.Features[i] = pages_module.FeatureConfig{Title: f.Title, Description: f.Description, Icon: f.Icon, ImageURL: f.ImageURL}
|
||||
}
|
||||
}
|
||||
|
||||
func applySocial(dst *pages_module.SocialProofConfig, src *api.UpdatePagesSocialOption) {
|
||||
if src.Logos != nil {
|
||||
dst.Logos = *src.Logos
|
||||
}
|
||||
if src.Testimonials != nil {
|
||||
dst.Testimonials = make([]pages_module.TestimonialConfig, len(*src.Testimonials))
|
||||
for i, t := range *src.Testimonials {
|
||||
dst.Testimonials[i] = pages_module.TestimonialConfig{Quote: t.Quote, Author: t.Author, Role: t.Role, Avatar: t.Avatar}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyPricing(dst *pages_module.PricingConfig, src *api.UpdatePagesPricingOption) {
|
||||
if src.Headline != nil {
|
||||
dst.Headline = *src.Headline
|
||||
}
|
||||
if src.Subheadline != nil {
|
||||
dst.Subheadline = *src.Subheadline
|
||||
}
|
||||
if src.Plans != nil {
|
||||
dst.Plans = make([]pages_module.PricingPlanConfig, len(*src.Plans))
|
||||
for i, p := range *src.Plans {
|
||||
dst.Plans[i] = pages_module.PricingPlanConfig{
|
||||
Name: p.Name, Price: p.Price, Period: p.Period,
|
||||
Features: p.Features, CTA: p.CTA, Featured: p.Featured,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyCTASection(dst *pages_module.CTASectionConfig, src *api.UpdatePagesCTAOption) {
|
||||
if src.Headline != nil {
|
||||
dst.Headline = *src.Headline
|
||||
}
|
||||
if src.Subheadline != nil {
|
||||
dst.Subheadline = *src.Subheadline
|
||||
}
|
||||
applyCTAButton(&dst.Button, src.Button)
|
||||
}
|
||||
|
||||
func applyBlog(dst *pages_module.BlogSectionConfig, src *api.UpdatePagesBlogOption) {
|
||||
if src.Enabled != nil {
|
||||
dst.Enabled = *src.Enabled
|
||||
}
|
||||
if src.Headline != nil {
|
||||
dst.Headline = *src.Headline
|
||||
}
|
||||
if src.Subheadline != nil {
|
||||
dst.Subheadline = *src.Subheadline
|
||||
}
|
||||
if src.MaxPosts != nil {
|
||||
dst.MaxPosts = *src.MaxPosts
|
||||
}
|
||||
}
|
||||
|
||||
func applyGallery(dst *pages_module.GallerySectionConfig, src *api.UpdatePagesGalleryOption) {
|
||||
if src.Enabled != nil {
|
||||
dst.Enabled = *src.Enabled
|
||||
}
|
||||
if src.Headline != nil {
|
||||
dst.Headline = *src.Headline
|
||||
}
|
||||
if src.Subheadline != nil {
|
||||
dst.Subheadline = *src.Subheadline
|
||||
}
|
||||
if src.MaxImages != nil {
|
||||
dst.MaxImages = *src.MaxImages
|
||||
}
|
||||
if src.Columns != nil {
|
||||
dst.Columns = *src.Columns
|
||||
}
|
||||
}
|
||||
|
||||
func applyComparison(dst *pages_module.ComparisonSectionConfig, src *api.UpdatePagesComparisonOption) {
|
||||
if src.Enabled != nil {
|
||||
dst.Enabled = *src.Enabled
|
||||
}
|
||||
if src.Headline != nil {
|
||||
dst.Headline = *src.Headline
|
||||
}
|
||||
if src.Subheadline != nil {
|
||||
dst.Subheadline = *src.Subheadline
|
||||
}
|
||||
if src.Columns != nil {
|
||||
dst.Columns = make([]pages_module.ComparisonColumnConfig, len(*src.Columns))
|
||||
for i, c := range *src.Columns {
|
||||
dst.Columns[i] = pages_module.ComparisonColumnConfig{Name: c.Name, Highlight: c.Highlight}
|
||||
}
|
||||
}
|
||||
if src.Groups != nil {
|
||||
dst.Groups = make([]pages_module.ComparisonGroupConfig, len(*src.Groups))
|
||||
for i, g := range *src.Groups {
|
||||
features := make([]pages_module.ComparisonFeatureConfig, len(g.Features))
|
||||
for j, f := range g.Features {
|
||||
features[j] = pages_module.ComparisonFeatureConfig{Name: f.Name, Values: f.Values}
|
||||
}
|
||||
dst.Groups[i] = pages_module.ComparisonGroupConfig{Name: g.Name, Features: features}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyNavigation(dst *pages_module.NavigationConfig, src *api.UpdatePagesNavOption) {
|
||||
if src.ShowDocs != nil {
|
||||
dst.ShowDocs = *src.ShowDocs
|
||||
}
|
||||
if src.ShowAPI != nil {
|
||||
dst.ShowAPI = *src.ShowAPI
|
||||
}
|
||||
if src.ShowRepository != nil {
|
||||
dst.ShowRepository = *src.ShowRepository
|
||||
}
|
||||
if src.ShowReleases != nil {
|
||||
dst.ShowReleases = *src.ShowReleases
|
||||
}
|
||||
if src.ShowIssues != nil {
|
||||
dst.ShowIssues = *src.ShowIssues
|
||||
}
|
||||
}
|
||||
|
||||
func applyFooter(dst *pages_module.FooterConfig, ctaDst *pages_module.CTASectionConfig, src *api.UpdatePagesFooterOption) {
|
||||
if src.Copyright != nil {
|
||||
dst.Copyright = *src.Copyright
|
||||
}
|
||||
if src.ShowPoweredBy != nil {
|
||||
dst.ShowPoweredBy = *src.ShowPoweredBy
|
||||
}
|
||||
if src.Links != nil {
|
||||
dst.Links = make([]pages_module.FooterLink, len(*src.Links))
|
||||
for i, l := range *src.Links {
|
||||
dst.Links[i] = pages_module.FooterLink{Label: l.Label, URL: l.URL}
|
||||
}
|
||||
}
|
||||
if src.Social != nil {
|
||||
dst.Social = make([]pages_module.SocialLink, len(*src.Social))
|
||||
for i, s := range *src.Social {
|
||||
dst.Social[i] = pages_module.SocialLink{Platform: s.Platform, URL: s.URL}
|
||||
}
|
||||
}
|
||||
if src.CTASection != nil {
|
||||
applyCTASection(ctaDst, src.CTASection)
|
||||
}
|
||||
}
|
||||
|
||||
func applyTheme(dst *pages_module.ThemeConfig, src *api.UpdatePagesThemeOption) {
|
||||
if src.PrimaryColor != nil {
|
||||
dst.PrimaryColor = *src.PrimaryColor
|
||||
}
|
||||
if src.AccentColor != nil {
|
||||
dst.AccentColor = *src.AccentColor
|
||||
}
|
||||
if src.Mode != nil {
|
||||
dst.Mode = *src.Mode
|
||||
}
|
||||
}
|
||||
|
||||
func applySEO(dst *pages_module.SEOConfig, src *api.UpdatePagesSEOOption) {
|
||||
if src.Title != nil {
|
||||
dst.Title = *src.Title
|
||||
}
|
||||
if src.Description != nil {
|
||||
dst.Description = *src.Description
|
||||
}
|
||||
if src.Keywords != nil {
|
||||
dst.Keywords = *src.Keywords
|
||||
}
|
||||
if src.OGImage != nil {
|
||||
dst.OGImage = *src.OGImage
|
||||
}
|
||||
if src.UseMediaKitOG != nil {
|
||||
dst.UseMediaKitOG = *src.UseMediaKitOG
|
||||
}
|
||||
if src.TwitterCard != nil {
|
||||
dst.TwitterCard = *src.TwitterCard
|
||||
}
|
||||
if src.TwitterSite != nil {
|
||||
dst.TwitterSite = *src.TwitterSite
|
||||
}
|
||||
}
|
||||
|
||||
func applyAdvanced(dst *pages_module.AdvancedConfig, src *api.UpdatePagesAdvancedOption) {
|
||||
if src.CustomCSS != nil {
|
||||
dst.CustomCSS = *src.CustomCSS
|
||||
}
|
||||
if src.CustomHead != nil {
|
||||
dst.CustomHead = *src.CustomHead
|
||||
}
|
||||
if src.StaticRoutes != nil {
|
||||
dst.StaticRoutes = *src.StaticRoutes
|
||||
}
|
||||
if src.PublicReleases != nil {
|
||||
dst.PublicReleases = *src.PublicReleases
|
||||
}
|
||||
if src.HideMobileReleases != nil {
|
||||
dst.HideMobileReleases = *src.HideMobileReleases
|
||||
}
|
||||
if src.GooglePlayID != nil {
|
||||
dst.GooglePlayID = *src.GooglePlayID
|
||||
}
|
||||
if src.AppStoreID != nil {
|
||||
dst.AppStoreID = *src.AppStoreID
|
||||
}
|
||||
}
|
||||
|
||||
// applyFullConfig applies all non-nil sections from the update option to the config
|
||||
func applyFullConfig(config *pages_module.LandingConfig, form *api.UpdatePagesConfigOption) {
|
||||
if form.Enabled != nil {
|
||||
config.Enabled = *form.Enabled
|
||||
}
|
||||
if form.PublicLanding != nil {
|
||||
config.PublicLanding = *form.PublicLanding
|
||||
}
|
||||
if form.Template != nil {
|
||||
config.Template = *form.Template
|
||||
}
|
||||
if form.Brand != nil {
|
||||
applyBrand(&config.Brand, form.Brand)
|
||||
}
|
||||
if form.Hero != nil {
|
||||
applyHero(&config.Hero, form.Hero)
|
||||
}
|
||||
if form.Stats != nil {
|
||||
applyStats(config, *form.Stats)
|
||||
}
|
||||
if form.ValueProps != nil {
|
||||
applyValueProps(config, *form.ValueProps)
|
||||
}
|
||||
if form.Features != nil {
|
||||
applyFeatures(config, *form.Features)
|
||||
}
|
||||
if form.SocialProof != nil {
|
||||
applySocial(&config.SocialProof, form.SocialProof)
|
||||
}
|
||||
if form.Pricing != nil {
|
||||
applyPricing(&config.Pricing, form.Pricing)
|
||||
}
|
||||
if form.CTASection != nil {
|
||||
applyCTASection(&config.CTASection, form.CTASection)
|
||||
}
|
||||
if form.Blog != nil {
|
||||
applyBlog(&config.Blog, form.Blog)
|
||||
}
|
||||
if form.Gallery != nil {
|
||||
applyGallery(&config.Gallery, form.Gallery)
|
||||
}
|
||||
if form.Comparison != nil {
|
||||
applyComparison(&config.Comparison, form.Comparison)
|
||||
}
|
||||
if form.Navigation != nil {
|
||||
applyNavigation(&config.Navigation, form.Navigation)
|
||||
}
|
||||
if form.Footer != nil {
|
||||
applyFooter(&config.Footer, &config.CTASection, form.Footer)
|
||||
}
|
||||
if form.Theme != nil {
|
||||
applyTheme(&config.Theme, form.Theme)
|
||||
}
|
||||
if form.SEO != nil {
|
||||
applySEO(&config.SEO, form.SEO)
|
||||
}
|
||||
if form.Advanced != nil {
|
||||
applyAdvanced(&config.Advanced, form.Advanced)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET endpoints
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// GetPagesConfig returns the full pages configuration for a repository
|
||||
// GET /api/v2/repos/{owner}/{repo}/pages/config
|
||||
func GetPagesConfig(ctx *context.APIContext) {
|
||||
repo := ctx.Repo.Repository
|
||||
@@ -43,22 +486,11 @@ func GetPagesConfig(ctx *context.APIContext) {
|
||||
|
||||
config, err := pages_service.GetPagesConfig(ctx, repo)
|
||||
if err != nil {
|
||||
ctx.APIErrorNotFound("Pages not configured")
|
||||
ctx.APIErrorWithCode(apierrors.PagesNotConfigured)
|
||||
return
|
||||
}
|
||||
|
||||
response := &PagesConfigResponse{
|
||||
Enabled: config.Enabled,
|
||||
PublicLanding: config.PublicLanding,
|
||||
Template: config.Template,
|
||||
Domain: config.Domain,
|
||||
Brand: config.Brand,
|
||||
Hero: config.Hero,
|
||||
SEO: config.SEO,
|
||||
Footer: config.Footer,
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// GetPagesContent returns the rendered content for a repository's landing page
|
||||
@@ -72,14 +504,12 @@ func GetPagesContent(ctx *context.APIContext) {
|
||||
|
||||
config, err := pages_service.GetPagesConfig(ctx, repo)
|
||||
if err != nil || !config.Enabled {
|
||||
ctx.APIErrorNotFound("Pages not enabled")
|
||||
ctx.APIErrorWithCode(apierrors.PagesNotEnabled)
|
||||
return
|
||||
}
|
||||
|
||||
// Load README content
|
||||
readme := loadReadmeContent(ctx, repo)
|
||||
|
||||
// Build title
|
||||
title := config.SEO.Title
|
||||
if title == "" {
|
||||
title = config.Hero.Headline
|
||||
@@ -91,7 +521,6 @@ func GetPagesContent(ctx *context.APIContext) {
|
||||
title = repo.Name
|
||||
}
|
||||
|
||||
// Build description
|
||||
description := config.SEO.Description
|
||||
if description == "" {
|
||||
description = config.Hero.Subheadline
|
||||
@@ -100,15 +529,265 @@ func GetPagesContent(ctx *context.APIContext) {
|
||||
description = repo.Description
|
||||
}
|
||||
|
||||
response := &PagesContentResponse{
|
||||
ctx.JSON(http.StatusOK, &PagesContentResponse{
|
||||
Title: title,
|
||||
Description: description,
|
||||
Readme: readme,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PUT /config — replace full config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// UpdatePagesConfig replaces the entire landing page configuration
|
||||
// PUT /api/v2/repos/{owner}/{repo}/pages/config
|
||||
func UpdatePagesConfig(ctx *context.APIContext) {
|
||||
if !requirePagesAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
config, ok := getPagesConfigAPI(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, response)
|
||||
form := web.GetForm(ctx).(*api.UpdatePagesConfigOption)
|
||||
|
||||
if form.Template != nil && !pages_module.IsValidTemplate(*form.Template) {
|
||||
ctx.APIErrorWithCode(apierrors.PagesInvalidTemplate)
|
||||
return
|
||||
}
|
||||
|
||||
applyFullConfig(config, form)
|
||||
|
||||
if !savePagesConfigAPI(ctx, config) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PATCH /config — partial merge
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// PatchPagesConfig partially updates the landing page configuration
|
||||
// PATCH /api/v2/repos/{owner}/{repo}/pages/config
|
||||
func PatchPagesConfig(ctx *context.APIContext) {
|
||||
if !requirePagesAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
config, ok := getPagesConfigAPI(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdatePagesConfigOption)
|
||||
|
||||
if form.Template != nil && !pages_module.IsValidTemplate(*form.Template) {
|
||||
ctx.APIErrorWithCode(apierrors.PagesInvalidTemplate)
|
||||
return
|
||||
}
|
||||
|
||||
applyFullConfig(config, form)
|
||||
|
||||
if !savePagesConfigAPI(ctx, config) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Section PUT endpoints
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// UpdatePagesBrand updates the brand section
|
||||
// PUT /api/v2/repos/{owner}/{repo}/pages/config/brand
|
||||
func UpdatePagesBrand(ctx *context.APIContext) {
|
||||
if !requirePagesAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
config, ok := getPagesConfigAPI(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdatePagesBrandOption)
|
||||
applyBrand(&config.Brand, form)
|
||||
|
||||
if !savePagesConfigAPI(ctx, config) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// UpdatePagesHero updates the hero section
|
||||
// PUT /api/v2/repos/{owner}/{repo}/pages/config/hero
|
||||
func UpdatePagesHero(ctx *context.APIContext) {
|
||||
if !requirePagesAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
config, ok := getPagesConfigAPI(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdatePagesHeroOption)
|
||||
applyHero(&config.Hero, form)
|
||||
|
||||
if !savePagesConfigAPI(ctx, config) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// UpdatePagesContentSection updates the content section (blog, gallery, stats, features, nav, etc.)
|
||||
// PUT /api/v2/repos/{owner}/{repo}/pages/config/content
|
||||
func UpdatePagesContentSection(ctx *context.APIContext) {
|
||||
if !requirePagesAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
config, ok := getPagesConfigAPI(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdatePagesContentOption)
|
||||
|
||||
if form.Blog != nil {
|
||||
applyBlog(&config.Blog, form.Blog)
|
||||
}
|
||||
if form.Gallery != nil {
|
||||
applyGallery(&config.Gallery, form.Gallery)
|
||||
}
|
||||
if form.ComparisonEnabled != nil {
|
||||
config.Comparison.Enabled = *form.ComparisonEnabled
|
||||
}
|
||||
if form.Stats != nil {
|
||||
applyStats(config, *form.Stats)
|
||||
}
|
||||
if form.ValueProps != nil {
|
||||
applyValueProps(config, *form.ValueProps)
|
||||
}
|
||||
if form.Features != nil {
|
||||
applyFeatures(config, *form.Features)
|
||||
}
|
||||
if form.Navigation != nil {
|
||||
applyNavigation(&config.Navigation, form.Navigation)
|
||||
}
|
||||
if form.Advanced != nil {
|
||||
applyAdvanced(&config.Advanced, form.Advanced)
|
||||
}
|
||||
|
||||
if !savePagesConfigAPI(ctx, config) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// UpdatePagesComparison updates the comparison section
|
||||
// PUT /api/v2/repos/{owner}/{repo}/pages/config/comparison
|
||||
func UpdatePagesComparison(ctx *context.APIContext) {
|
||||
if !requirePagesAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
config, ok := getPagesConfigAPI(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdatePagesComparisonOption)
|
||||
applyComparison(&config.Comparison, form)
|
||||
|
||||
if !savePagesConfigAPI(ctx, config) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// UpdatePagesSocial updates the social proof section
|
||||
// PUT /api/v2/repos/{owner}/{repo}/pages/config/social
|
||||
func UpdatePagesSocial(ctx *context.APIContext) {
|
||||
if !requirePagesAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
config, ok := getPagesConfigAPI(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdatePagesSocialOption)
|
||||
applySocial(&config.SocialProof, form)
|
||||
|
||||
if !savePagesConfigAPI(ctx, config) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// UpdatePagesPricing updates the pricing section
|
||||
// PUT /api/v2/repos/{owner}/{repo}/pages/config/pricing
|
||||
func UpdatePagesPricing(ctx *context.APIContext) {
|
||||
if !requirePagesAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
config, ok := getPagesConfigAPI(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdatePagesPricingOption)
|
||||
applyPricing(&config.Pricing, form)
|
||||
|
||||
if !savePagesConfigAPI(ctx, config) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// UpdatePagesFooter updates the footer and CTA section
|
||||
// PUT /api/v2/repos/{owner}/{repo}/pages/config/footer
|
||||
func UpdatePagesFooter(ctx *context.APIContext) {
|
||||
if !requirePagesAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
config, ok := getPagesConfigAPI(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdatePagesFooterOption)
|
||||
applyFooter(&config.Footer, &config.CTASection, form)
|
||||
|
||||
if !savePagesConfigAPI(ctx, config) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// UpdatePagesTheme updates the theme and SEO section
|
||||
// PUT /api/v2/repos/{owner}/{repo}/pages/config/theme
|
||||
func UpdatePagesTheme(ctx *context.APIContext) {
|
||||
if !requirePagesAdmin(ctx) {
|
||||
return
|
||||
}
|
||||
config, ok := getPagesConfigAPI(ctx)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*api.UpdatePagesThemeOption)
|
||||
applyTheme(&config.Theme, form)
|
||||
|
||||
if !savePagesConfigAPI(ctx, config) {
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, buildFullResponse(config))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// loadReadmeContent loads the README content from the repository
|
||||
func loadReadmeContent(ctx *context.APIContext, repo *repo_model.Repository) string {
|
||||
gitRepo, err := git.OpenRepository(ctx, repo.RepoPath())
|
||||
@@ -127,7 +806,6 @@ func loadReadmeContent(ctx *context.APIContext, repo *repo_model.Repository) str
|
||||
return ""
|
||||
}
|
||||
|
||||
// Try common README paths
|
||||
readmePaths := []string{
|
||||
"README.md",
|
||||
"readme.md",
|
||||
|
||||
@@ -148,6 +148,7 @@ func CheckAppUpdate(ctx *context.APIContext) {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
latestRelease.Repo = repo
|
||||
|
||||
// Find the appropriate asset for this platform/arch
|
||||
downloadURL, platformInfo := findUpdateAsset(latestRelease, platform, arch)
|
||||
@@ -346,6 +347,7 @@ func ListReleasesV2(ctx *context.APIContext) {
|
||||
// Convert to API format
|
||||
apiReleases := make([]*api.Release, 0, len(releases))
|
||||
for _, release := range releases {
|
||||
release.Repo = repo
|
||||
apiReleases = append(apiReleases, convertToAPIRelease(repo, release))
|
||||
}
|
||||
|
||||
@@ -388,6 +390,7 @@ func GetReleaseV2(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
release.Repo = repo
|
||||
ctx.JSON(http.StatusOK, convertToAPIRelease(repo, release))
|
||||
}
|
||||
|
||||
@@ -436,6 +439,7 @@ func GetLatestReleaseV2(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
release.Repo = repo
|
||||
ctx.JSON(http.StatusOK, convertToAPIRelease(repo, release))
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import (
|
||||
"code.gitcaddy.com/server/v3/routers/private"
|
||||
web_routers "code.gitcaddy.com/server/v3/routers/web"
|
||||
actions_service "code.gitcaddy.com/server/v3/services/actions"
|
||||
ai_service "code.gitcaddy.com/server/v3/services/ai"
|
||||
asymkey_service "code.gitcaddy.com/server/v3/services/asymkey"
|
||||
"code.gitcaddy.com/server/v3/services/auth"
|
||||
"code.gitcaddy.com/server/v3/services/auth/source/oauth2"
|
||||
@@ -160,11 +161,21 @@ func InitWebInstalled(ctx context.Context) {
|
||||
log.Fatal("Plugin migrations failed: %v", err)
|
||||
}
|
||||
|
||||
// Initialize all plugins
|
||||
// Initialize all compiled plugins
|
||||
if err := plugins.InitAll(ctx); err != nil {
|
||||
log.Fatal("Plugin initialization failed: %v", err)
|
||||
}
|
||||
|
||||
// Initialize external plugin manager (Phase 5)
|
||||
pluginCfg := plugins.LoadConfig()
|
||||
if pluginCfg.Enabled && len(pluginCfg.ExternalPlugins) > 0 {
|
||||
extManager := plugins.NewExternalPluginManager(pluginCfg)
|
||||
if err := extManager.StartAll(); err != nil {
|
||||
log.Error("External plugin startup had errors: %v", err)
|
||||
}
|
||||
extManager.StartHealthMonitoring()
|
||||
}
|
||||
|
||||
mustInit(system.Init)
|
||||
mustInitCtx(ctx, oauth2.Init)
|
||||
mustInitCtx(ctx, oauth2_provider.Init)
|
||||
@@ -195,6 +206,7 @@ func InitWebInstalled(ctx context.Context) {
|
||||
mustInit(svg.Init)
|
||||
|
||||
mustInitCtx(ctx, actions_service.Init)
|
||||
mustInitCtx(ctx, ai_service.Init)
|
||||
|
||||
mustInit(repo_service.InitLicenseClassifier)
|
||||
|
||||
|
||||
77
routers/web/admin/ai.go
Normal file
77
routers/web/admin/ai.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
ai_model "code.gitcaddy.com/server/v3/models/ai"
|
||||
"code.gitcaddy.com/server/v3/models/db"
|
||||
ai_module "code.gitcaddy.com/server/v3/modules/ai"
|
||||
"code.gitcaddy.com/server/v3/modules/setting"
|
||||
"code.gitcaddy.com/server/v3/modules/templates"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
)
|
||||
|
||||
const (
|
||||
tplAI templates.TplName = "admin/ai"
|
||||
)
|
||||
|
||||
// AIStatus shows the AI service status admin dashboard
|
||||
func AIStatus(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("admin.ai.title")
|
||||
ctx.Data["PageIsAdminAI"] = true
|
||||
|
||||
// Check sidecar health
|
||||
var sidecarHealthy bool
|
||||
var sidecarVersion string
|
||||
var providerStatus map[string]string
|
||||
|
||||
if setting.AI.Enabled {
|
||||
health, err := ai_module.GetClient().CheckHealth(ctx)
|
||||
if err == nil && health != nil {
|
||||
sidecarHealthy = health.Healthy
|
||||
sidecarVersion = health.Version
|
||||
providerStatus = health.ProviderStatus
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["SidecarHealthy"] = sidecarHealthy
|
||||
ctx.Data["SidecarVersion"] = sidecarVersion
|
||||
ctx.Data["ProviderStatus"] = providerStatus
|
||||
|
||||
// Load global operation stats
|
||||
stats, err := ai_model.GetGlobalOperationStats(ctx)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetGlobalOperationStats", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Stats"] = stats
|
||||
|
||||
// Calculate success rate
|
||||
var successRate float64
|
||||
if stats.TotalOperations > 0 {
|
||||
successRate = float64(stats.SuccessCount) / float64(stats.TotalOperations) * 100
|
||||
}
|
||||
ctx.Data["SuccessRate"] = successRate
|
||||
ctx.Data["TotalTokens"] = stats.TotalInputTokens + stats.TotalOutputTokens
|
||||
|
||||
// Load recent operations (last 20)
|
||||
recentOps, err := db.Find[ai_model.OperationLog](ctx, ai_model.FindOperationLogsOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("FindOperationLogs", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["RecentOps"] = recentOps
|
||||
|
||||
// Pass AI config for display
|
||||
ctx.Data["AIConfig"] = setting.AI
|
||||
|
||||
ctx.HTML(http.StatusOK, tplAI)
|
||||
}
|
||||
@@ -251,6 +251,9 @@ func ChangeConfig(ctx *context.Context) {
|
||||
cfg.Theme.ExploreOrgDisplayFormat.DynKey(): marshalString("list"),
|
||||
cfg.Theme.EnableBlogs.DynKey(): marshalBool,
|
||||
cfg.Theme.BlogsInTopNav.DynKey(): marshalBool,
|
||||
cfg.Theme.ShowFooterPoweredBy.DynKey(): marshalBool,
|
||||
cfg.Theme.ShowFooterLicenses.DynKey(): marshalBool,
|
||||
cfg.Theme.ShowFooterAPI.DynKey(): marshalBool,
|
||||
}
|
||||
|
||||
_ = ctx.Req.ParseForm()
|
||||
|
||||
@@ -81,6 +81,57 @@ func Packages(ctx *context.Context) {
|
||||
ctx.HTML(http.StatusOK, tplPackagesList)
|
||||
}
|
||||
|
||||
// BulkDeletePackages deletes all versions of selected packages
|
||||
func BulkDeletePackages(ctx *context.Context) {
|
||||
packageIDs := ctx.FormStrings("ids[]")
|
||||
|
||||
ids := make([]int64, 0, len(packageIDs))
|
||||
for _, idStr := range packageIDs {
|
||||
var id int64
|
||||
if _, err := fmt.Sscanf(idStr, "%d", &id); err == nil && id > 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
ctx.Flash.Error(ctx.Tr("admin.packages.bulk.no_selection"))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages")
|
||||
return
|
||||
}
|
||||
|
||||
deletedVersions := 0
|
||||
deletedPackages := 0
|
||||
|
||||
for _, packageID := range ids {
|
||||
// Get all versions of this package
|
||||
versions, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
PackageID: packageID,
|
||||
})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
packageDeleted := false
|
||||
for _, pv := range versions {
|
||||
if err := packages_service.RemovePackageVersion(ctx, ctx.Doer, pv); err == nil {
|
||||
deletedVersions++
|
||||
packageDeleted = true
|
||||
}
|
||||
}
|
||||
if packageDeleted {
|
||||
deletedPackages++
|
||||
}
|
||||
}
|
||||
|
||||
if deletedPackages > 0 {
|
||||
ctx.Flash.Success(ctx.Tr("admin.packages.bulk.delete.success", deletedPackages, deletedVersions))
|
||||
} else {
|
||||
ctx.Flash.Warning(ctx.Tr("admin.packages.bulk.delete.none"))
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages")
|
||||
}
|
||||
|
||||
// DeletePackageVersion deletes a package version
|
||||
func DeletePackageVersion(ctx *context.Context) {
|
||||
pv, err := packages_model.GetVersionByID(ctx, ctx.FormInt64("id"))
|
||||
@@ -177,6 +228,41 @@ func BulkAutoMatch(ctx *context.Context) {
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages")
|
||||
}
|
||||
|
||||
// BulkSetPrivate sets/unsets private flag on multiple packages
|
||||
func BulkSetPrivate(ctx *context.Context) {
|
||||
packageIDs := ctx.FormStrings("ids[]")
|
||||
isPrivate := ctx.FormBool("is_private")
|
||||
|
||||
ids := make([]int64, 0, len(packageIDs))
|
||||
for _, idStr := range packageIDs {
|
||||
var id int64
|
||||
if _, err := fmt.Sscanf(idStr, "%d", &id); err == nil && id > 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
ctx.Flash.Error(ctx.Tr("admin.packages.bulk.no_selection"))
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages")
|
||||
return
|
||||
}
|
||||
|
||||
succeeded := 0
|
||||
for _, id := range ids {
|
||||
if err := packages_model.SetPackageIsPrivate(ctx, id, isPrivate); err == nil {
|
||||
succeeded++
|
||||
}
|
||||
}
|
||||
|
||||
if isPrivate {
|
||||
ctx.Flash.Success(ctx.Tr("admin.packages.bulk.private.enabled", succeeded))
|
||||
} else {
|
||||
ctx.Flash.Success(ctx.Tr("admin.packages.bulk.private.disabled", succeeded))
|
||||
}
|
||||
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/-/admin/packages")
|
||||
}
|
||||
|
||||
// SingleAutoMatch automatically matches a single package to a repository
|
||||
func SingleAutoMatch(ctx *context.Context) {
|
||||
packageID := ctx.FormInt64("id")
|
||||
|
||||
@@ -168,6 +168,10 @@ func StandaloneBlogView(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Increment view count
|
||||
_ = blog_model.IncrementBlogPostViewCount(ctx, post.ID)
|
||||
post.ViewCount++
|
||||
|
||||
if err := post.LoadAuthor(ctx); err != nil {
|
||||
ctx.ServerError("LoadAuthor", err)
|
||||
return
|
||||
|
||||
@@ -222,7 +222,7 @@ func home(ctx *context.Context, viewRepositories bool) {
|
||||
ctx.Data["HasMorePublicMembers"] = totalPublicMembers > 12
|
||||
|
||||
// Load organization stats
|
||||
orgStats, err := org_service.GetOrgOverviewStats(ctx, org.ID)
|
||||
orgStats, err := org_service.GetOrgOverviewStats(ctx, org.ID, ctx.Doer)
|
||||
if err != nil {
|
||||
log.Error("GetOrgOverviewStats: %v", err)
|
||||
}
|
||||
|
||||
96
routers/web/org/setting_ai.go
Normal file
96
routers/web/org/setting_ai.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
ai_model "code.gitcaddy.com/server/v3/models/ai"
|
||||
"code.gitcaddy.com/server/v3/modules/setting"
|
||||
"code.gitcaddy.com/server/v3/modules/templates"
|
||||
shared_user "code.gitcaddy.com/server/v3/routers/web/shared/user"
|
||||
"code.gitcaddy.com/server/v3/services/context"
|
||||
)
|
||||
|
||||
const tplSettingsAI templates.TplName = "org/settings/ai"
|
||||
|
||||
// SettingsAI shows the organization AI settings page
|
||||
func SettingsAI(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("org.settings.ai")
|
||||
ctx.Data["PageIsOrgSettings"] = true
|
||||
ctx.Data["PageIsOrgSettingsAI"] = true
|
||||
ctx.Data["AIGlobalEnabled"] = setting.AI.Enabled
|
||||
ctx.Data["AllowAgentMode"] = setting.AI.AllowAgentMode
|
||||
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !setting.AI.Enabled {
|
||||
ctx.HTML(http.StatusOK, tplSettingsAI)
|
||||
return
|
||||
}
|
||||
|
||||
orgSettings, err := ai_model.GetOrgAISettings(ctx, ctx.Org.Organization.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOrgAISettings", err)
|
||||
return
|
||||
}
|
||||
|
||||
if orgSettings == nil {
|
||||
orgSettings = &ai_model.OrgAISettings{}
|
||||
}
|
||||
|
||||
ctx.Data["OrgAISettings"] = orgSettings
|
||||
ctx.Data["HasAPIKey"] = orgSettings.APIKeyEncrypted != ""
|
||||
|
||||
ctx.HTML(http.StatusOK, tplSettingsAI)
|
||||
}
|
||||
|
||||
// SettingsAIPost handles the org AI settings form submission
|
||||
func SettingsAIPost(ctx *context.Context) {
|
||||
if !setting.AI.Enabled {
|
||||
ctx.Flash.Error(ctx.Tr("repo.settings.ai.globally_disabled"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/ai")
|
||||
return
|
||||
}
|
||||
|
||||
org := ctx.Org.Organization
|
||||
|
||||
// Load existing or create new
|
||||
orgSettings, err := ai_model.GetOrgAISettings(ctx, org.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOrgAISettings", err)
|
||||
return
|
||||
}
|
||||
if orgSettings == nil {
|
||||
orgSettings = &ai_model.OrgAISettings{OrgID: org.ID}
|
||||
}
|
||||
|
||||
// Update fields from form
|
||||
orgSettings.Provider = ctx.FormString("provider")
|
||||
orgSettings.Model = ctx.FormString("model")
|
||||
|
||||
// Only update API key if a new one was provided (don't clear existing)
|
||||
apiKey := ctx.FormString("api_key")
|
||||
if apiKey != "" {
|
||||
if err := orgSettings.SetAPIKey(apiKey); err != nil {
|
||||
ctx.ServerError("SetAPIKey", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
orgSettings.MaxOpsPerHour = ctx.FormInt("max_ops_per_hour")
|
||||
orgSettings.AllowedOps = ctx.FormString("allowed_ops")
|
||||
orgSettings.AgentModeAllowed = ctx.FormBool("agent_mode_allowed") && setting.AI.AllowAgentMode
|
||||
|
||||
if err := ai_model.CreateOrUpdateOrgAISettings(ctx, orgSettings); err != nil {
|
||||
ctx.ServerError("CreateOrUpdateOrgAISettings", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
|
||||
ctx.Redirect(ctx.Org.OrgLink + "/settings/ai")
|
||||
}
|
||||
@@ -159,8 +159,12 @@ func getOrCreateProfileRepo(ctx *context.Context, org *organization.Organization
|
||||
// createOrgLicenseFile creates a LICENSE.md file in the profile repo
|
||||
func createOrgLicenseFile(ctx *context.Context, repo *repo_model.Repository, licenseType string) error {
|
||||
// Get license content from templates
|
||||
ownerName := repo.OwnerDisplayName
|
||||
if ownerName == "" {
|
||||
ownerName = repo.OwnerName
|
||||
}
|
||||
licenseContent, err := repo_module.GetLicense(licenseType, &repo_module.LicenseValues{
|
||||
Owner: repo.OwnerName,
|
||||
Owner: ownerName,
|
||||
Email: ctx.Doer.Email,
|
||||
Repo: repo.Name,
|
||||
Year: time.Now().Format("2006"),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@ func AIReviewPullRequest(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
review, err := ai_service.ReviewPullRequest(ctx, pr)
|
||||
review, err := ai_service.ReviewPullRequest(ctx, pr, nil)
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("repo.ai.review_failed", err.Error()))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + ctx.PathParam("index"))
|
||||
@@ -77,7 +77,7 @@ func AITriageIssue(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
triage, err := ai_service.TriageIssue(ctx, issue)
|
||||
triage, err := ai_service.TriageIssue(ctx, issue, nil)
|
||||
if err != nil {
|
||||
ctx.Flash.Error(ctx.Tr("repo.ai.triage_failed", err.Error()))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + ctx.PathParam("index"))
|
||||
@@ -108,7 +108,7 @@ func AISuggestLabels(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
suggestions, err := ai_service.SuggestLabels(ctx, issue)
|
||||
suggestions, err := ai_service.SuggestLabels(ctx, issue, nil)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, map[string]string{
|
||||
"error": err.Error(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user