chore: bump Go toolchain to 1.26 (TASK-763) (#247)

* chore: bump Go toolchain to 1.26 (TASK-763)

Bump Go from 1.25 to 1.26 across all toolchain pins:

- go.mod — go 1.25.0 → go 1.26.0
- Dockerfile — golang:1.25-alpine → golang:1.26-alpine
- .github/workflows/ci.yml — three setup-go steps (Go, Go-Postgres, E2E jobs)
- .github/workflows/release.yml — release pipeline

No `toolchain` directive: the repo is pre-launch with no external
contributors yet, so we set the floor where we want it (hard requirement).

Verified locally before commit:
- golangci-lint v2.11.4 builds and runs under Go 1.26.2 (pinned in CI)
- golang:1.26-alpine and 1.26.2-alpine images present on Docker Hub
- go build ./... clean
- go vet ./... clean
- go test ./... all pass

Parent: PLAN-644 (OSS Repo Hygiene and Launch Polish).

* chore: gofmt -w under Go 1.26 (TASK-763)

Apply Go 1.26's gofmt to the codebase. ~41 files reformatted, all
struct-tag whitespace realignment — no semantic changes. Verified:

- gofmt -l ./cmd ./internal returns empty after
- go build ./... still clean
- go test ./... still passes (run before commit)

Bundling the gofmt diff with the toolchain bump in the same PR because
the formatting drift is a direct consequence of moving from 1.25 to
1.26; splitting them creates a mandatory two-PR ordering for no value.

Parent: PLAN-644.

* docs: bump documented Go floor to 1.26 (TASK-763)

Match go.mod's hard 1.26.0 requirement in the source-build instructions.
Caught by Codex review round 1 on PR #247.

- README.md:158 — "Go 1.25+" → "Go 1.26+"
- CONTRIBUTING.md:9 — "Go 1.25+" → "Go 1.26+"
This commit is contained in:
xarmian
2026-04-25 11:35:19 -04:00
committed by GitHub
parent f58290272f
commit 157ca4e88f
47 changed files with 182 additions and 181 deletions
+3 -3
View File
@@ -30,7 +30,7 @@ jobs:
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.25"
go-version: "1.26"
- name: Create web build placeholder for embed
run: mkdir -p web/build && echo "placeholder" > web/build/.gitkeep
@@ -99,7 +99,7 @@ jobs:
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.25"
go-version: "1.26"
- name: Create web build placeholder for embed
run: mkdir -p web/build && echo "placeholder" > web/build/.gitkeep
@@ -157,7 +157,7 @@ jobs:
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.25"
go-version: "1.26"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: "1.25"
go-version: "1.26"
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
+1 -1
View File
@@ -6,7 +6,7 @@ Thanks for your interest in contributing to Pad! This guide will help you get se
### Prerequisites
- [Go 1.25+](https://go.dev/dl/)
- [Go 1.26+](https://go.dev/dl/)
- [Node.js 22+](https://nodejs.org/)
- Make
+1 -1
View File
@@ -7,7 +7,7 @@ COPY web/ ./
RUN npm run build
# Stage 2: Build Go binary
FROM golang:1.25-alpine AS go-builder
FROM golang:1.26-alpine AS go-builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
+1 -1
View File
@@ -155,7 +155,7 @@ make build
cp pad ~/.local/bin/ # or /usr/local/bin/
```
Requires Go 1.25+ and Node.js 22+.
Requires Go 1.26+ and Node.js 22+.
The `go install github.com/xarmian/pad/cmd/pad@latest` path is not supported for the full Pad binary, because the web UI must be built and embedded during the source build.
+1 -1
View File
@@ -30,9 +30,9 @@ import (
"github.com/xarmian/pad/internal/config"
"regexp"
"github.com/redis/go-redis/v9"
"github.com/xarmian/pad/internal/billing"
"github.com/xarmian/pad/internal/email"
"github.com/redis/go-redis/v9"
"github.com/xarmian/pad/internal/events"
"github.com/xarmian/pad/internal/logging"
"github.com/xarmian/pad/internal/metrics"
+12 -12
View File
@@ -57,14 +57,14 @@ type serverInfoWorkspace struct {
}
type serverInfoLocal struct {
BindAddr string `json:"bind_addr"`
ServerRunning bool `json:"server_running"`
PID *int `json:"pid,omitempty"`
PIDFile string `json:"pid_file"`
LogFile string `json:"log_file"`
DBPath string `json:"db_path"`
DBExists bool `json:"db_exists"`
DBSizeBytes int64 `json:"db_size_bytes"`
BindAddr string `json:"bind_addr"`
ServerRunning bool `json:"server_running"`
PID *int `json:"pid,omitempty"`
PIDFile string `json:"pid_file"`
LogFile string `json:"log_file"`
DBPath string `json:"db_path"`
DBExists bool `json:"db_exists"`
DBSizeBytes int64 `json:"db_size_bytes"`
}
func infoCmd() *cobra.Command {
@@ -171,11 +171,11 @@ func includeLocalRuntime(cfg *config.Config) bool {
func collectLocalInfo(cfg *config.Config) *serverInfoLocal {
info := &serverInfoLocal{
BindAddr: cfg.Addr(),
BindAddr: cfg.Addr(),
ServerRunning: cli.IsServerRunning(cfg),
PIDFile: cfg.PIDFile(),
LogFile: cfg.LogFile(),
DBPath: cfg.DBPath,
PIDFile: cfg.PIDFile(),
LogFile: cfg.LogFile(),
DBPath: cfg.DBPath,
}
if pid, ok := readPID(cfg.PIDFile()); ok {
+2 -2
View File
@@ -39,9 +39,9 @@ func TestCollectServerInfoRemoteAuthenticated(t *testing.T) {
t.Fatalf("unexpected auth header %q", got)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"authenticated": true,
"authenticated": true,
"setup_required": false,
"auth_method": "password",
"auth_method": "password",
"user": map[string]any{
"id": "user-1",
"email": "dave@example.com",
+1 -1
View File
@@ -1,6 +1,6 @@
module github.com/xarmian/pad
go 1.25.0
go 1.26.0
require (
github.com/BurntSushi/toml v1.6.0
+6 -5
View File
@@ -64,6 +64,7 @@ func TestCancelCustomer_HappyPath_SendsCorrectRequest(t *testing.T) {
// - 403 on cloud_secret mismatch
// - 500 on internal / Stripe failure
// - 503 when Stripe is not configured
//
// All of them must produce a SidecarError — the handler treats every one
// as "abort the delete", regardless of bucket.
func TestCancelCustomer_NonOK_ReturnsSidecarError(t *testing.T) {
@@ -159,11 +160,11 @@ func TestCancelCustomer_UnconfiguredClient_ReturnsError(t *testing.T) {
func TestResolveOutboundSecret(t *testing.T) {
cases := []struct {
name string
explicit string
inboundList string
want string
wantEmpty bool
name string
explicit string
inboundList string
want string
wantEmpty bool
}{
{
name: "explicit wins over inbound",
+2 -2
View File
@@ -15,8 +15,8 @@ import (
// Color definitions for reuse across the CLI.
var (
Bold = color.New(color.Bold)
Dim = color.New(color.Faint)
Bold = color.New(color.Bold)
Dim = color.New(color.Faint)
BoldCyan = color.New(color.Bold, color.FgCyan)
)
+1 -1
View File
@@ -54,7 +54,7 @@ func Defaults() []DefaultCollection {
Type: "select",
Options: []string{"xs", "s", "m", "l", "xl"},
},
},
},
},
Settings: models.CollectionSettings{
Layout: "fields-primary",
+3 -3
View File
@@ -41,9 +41,9 @@ type Config struct {
EmailFromName string `toml:"email_from_name"` // Sender display name (e.g. Pad)
// Cloud mode
CloudSecret string `toml:"cloud_secret"` // Inbound shared secret(s) accepted from pad-cloud. Comma-separated list supports rotation.
CloudSidecarURL string `toml:"cloud_sidecar_url"` // Base URL pad uses to call the pad-cloud sidecar (reverse direction, e.g. Stripe cancel-customer on account delete)
CloudOutboundSecret string `toml:"cloud_outbound_secret"` // Optional: exact secret to send when calling pad-cloud. Falls back to the LAST entry of CloudSecret (the older rotation value, which is what pad-cloud is usually running). See DEPLOY.md "Cloud secret rotation".
CloudSecret string `toml:"cloud_secret"` // Inbound shared secret(s) accepted from pad-cloud. Comma-separated list supports rotation.
CloudSidecarURL string `toml:"cloud_sidecar_url"` // Base URL pad uses to call the pad-cloud sidecar (reverse direction, e.g. Stripe cancel-customer on account delete)
CloudOutboundSecret string `toml:"cloud_outbound_secret"` // Optional: exact secret to send when calling pad-cloud. Falls back to the LAST entry of CloudSecret (the older rotation value, which is what pad-cloud is usually running). See DEPLOY.md "Cloud secret rotation".
// Encryption
EncryptionKey string `toml:"encryption_key"` // 32-byte hex-encoded AES-256 key for encrypting sensitive fields
+1 -1
View File
@@ -39,7 +39,7 @@ const (
// Default replay buffer settings.
const (
DefaultReplayBufferSize = 1024 // max events to retain per workspace
DefaultReplayBufferSize = 1024 // max events to retain per workspace
DefaultReplayMaxAge = 5 * time.Minute // discard events older than this
)
+2 -2
View File
@@ -35,8 +35,8 @@ type RedisBus struct {
// Track which workspace channels we're subscribed to in Redis,
// so we subscribe/unsubscribe as local SSE clients come and go.
wsCounts map[string]int // workspace → local subscriber count
wsSubs map[string]*redisSub // workspace → active Redis subscription
wsCounts map[string]int // workspace → local subscriber count
wsSubs map[string]*redisSub // workspace → active Redis subscription
// Monotonic sequence counter for event IDs (local to this instance).
seq atomic.Int64
+2 -2
View File
@@ -14,8 +14,8 @@ type InstrumentedBus struct {
inner events.EventBus
metrics *Metrics
mu sync.Mutex
workspaces map[chan events.Event]string // channel → workspaceID for gauge decrement
mu sync.Mutex
workspaces map[chan events.Event]string // channel → workspaceID for gauge decrement
}
// NewInstrumentedBus wraps an EventBus with Prometheus instrumentation.
+5 -5
View File
@@ -72,11 +72,11 @@ func TestRegisterDBCollector(t *testing.T) {
}
expected := map[string]bool{
"pad_db_open_connections": false,
"pad_db_idle_connections": false,
"pad_db_in_use_connections": false,
"pad_db_wait_count_total": false,
"pad_db_wait_duration_seconds_total": false,
"pad_db_open_connections": false,
"pad_db_idle_connections": false,
"pad_db_in_use_connections": false,
"pad_db_wait_count_total": false,
"pad_db_wait_duration_seconds_total": false,
}
for _, f := range families {
+19 -19
View File
@@ -9,25 +9,25 @@ var ValidActions = []string{
// Audit action constants for auth/admin events
const (
ActionLogin = "login"
ActionLoginFailed = "login_failed"
ActionLogout = "logout"
ActionBootstrap = "bootstrap"
ActionRegister = "register"
ActionPasswordChanged = "password_changed"
ActionPasswordReset = "password_reset"
ActionTokenCreated = "token_created"
ActionTokenRevoked = "token_revoked"
ActionTokenRotated = "token_rotated"
ActionTOTPEnabled = "totp_enabled"
ActionTOTPDisabled = "totp_disabled"
ActionMemberInvited = "member_invited"
ActionMemberRemoved = "member_removed"
ActionRoleChanged = "role_changed"
ActionSettingsChanged = "settings_changed"
ActionOAuthLogin = "oauth_login"
ActionOAuthLoginFailed = "oauth_login_failed"
ActionPlanChanged = "plan_changed"
ActionLogin = "login"
ActionLoginFailed = "login_failed"
ActionLogout = "logout"
ActionBootstrap = "bootstrap"
ActionRegister = "register"
ActionPasswordChanged = "password_changed"
ActionPasswordReset = "password_reset"
ActionTokenCreated = "token_created"
ActionTokenRevoked = "token_revoked"
ActionTokenRotated = "token_rotated"
ActionTOTPEnabled = "totp_enabled"
ActionTOTPDisabled = "totp_disabled"
ActionMemberInvited = "member_invited"
ActionMemberRemoved = "member_removed"
ActionRoleChanged = "role_changed"
ActionSettingsChanged = "settings_changed"
ActionOAuthLogin = "oauth_login"
ActionOAuthLoginFailed = "oauth_login_failed"
ActionPlanChanged = "plan_changed"
ActionPasswordResetByAdmin = "password_reset_by_admin"
ActionUserDisabled = "user_disabled"
ActionUserEnabled = "user_enabled"
+1 -1
View File
@@ -24,7 +24,7 @@ type AgentRole struct {
// AgentRoleCreate is the input for creating a new agent role.
type AgentRoleCreate struct {
Name string `json:"name"`
Slug string `json:"slug,omitempty"` // auto-generated from name if empty
Slug string `json:"slug,omitempty"` // auto-generated from name if empty
Description string `json:"description,omitempty"`
Icon string `json:"icon,omitempty"`
Tools string `json:"tools,omitempty"`
+10 -10
View File
@@ -5,14 +5,14 @@ import "time"
type FieldDef struct {
Key string `json:"key"`
Label string `json:"label"`
Type string `json:"type"` // text, number, select, multi_select, date, checkbox, url, relation
Type string `json:"type"` // text, number, select, multi_select, date, checkbox, url, relation
Options []string `json:"options,omitempty"`
TerminalOptions []string `json:"terminal_options,omitempty"` // for select fields: which options represent a terminal/finalized state
Default any `json:"default,omitempty"`
Required bool `json:"required,omitempty"`
Computed bool `json:"computed,omitempty"`
Collection string `json:"collection,omitempty"` // for relation type
Suffix string `json:"suffix,omitempty"` // for number type display
Collection string `json:"collection,omitempty"` // for relation type
Suffix string `json:"suffix,omitempty"` // for number type display
}
type CollectionSchema struct {
@@ -21,15 +21,15 @@ type CollectionSchema struct {
// QuickAction defines a prompt template that can be triggered from the UI.
type QuickAction struct {
Label string `json:"label"` // display label for the button
Prompt string `json:"prompt"` // prompt template with {ref}, {title}, {status}, etc.
Scope string `json:"scope"` // "item" or "collection"
Icon string `json:"icon,omitempty"` // optional emoji/icon
Label string `json:"label"` // display label for the button
Prompt string `json:"prompt"` // prompt template with {ref}, {title}, {status}, etc.
Scope string `json:"scope"` // "item" or "collection"
Icon string `json:"icon,omitempty"` // optional emoji/icon
}
type CollectionSettings struct {
Layout string `json:"layout,omitempty"` // fields-primary, content-primary, balanced
DefaultView string `json:"default_view,omitempty"` // list, board, table
Layout string `json:"layout,omitempty"` // fields-primary, content-primary, balanced
DefaultView string `json:"default_view,omitempty"` // list, board, table
BoardGroupBy string `json:"board_group_by,omitempty"`
ListSortBy string `json:"list_sort_by,omitempty"`
ListGroupBy string `json:"list_group_by,omitempty"`
@@ -45,7 +45,7 @@ type Collection struct {
Icon string `json:"icon"`
Description string `json:"description"`
Schema string `json:"schema"` // JSON string in DB, parsed via methods
Settings string `json:"settings"` // JSON string in DB
Settings string `json:"settings"` // JSON string in DB
Prefix string `json:"prefix"`
SortOrder int `json:"sort_order"`
IsDefault bool `json:"is_default"`
+16 -16
View File
@@ -39,14 +39,14 @@ type Document struct {
}
type DocumentCreate struct {
Title string `json:"title"`
Content string `json:"content,omitempty"`
DocType string `json:"doc_type,omitempty"`
Status string `json:"status,omitempty"`
Tags string `json:"tags,omitempty"`
Pinned bool `json:"pinned,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
Source string `json:"source,omitempty"`
Title string `json:"title"`
Content string `json:"content,omitempty"`
DocType string `json:"doc_type,omitempty"`
Status string `json:"status,omitempty"`
Tags string `json:"tags,omitempty"`
Pinned bool `json:"pinned,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
Source string `json:"source,omitempty"`
}
type DocumentUpdate struct {
@@ -63,14 +63,14 @@ type DocumentUpdate struct {
}
type QuickSave struct {
Title string `json:"title"`
Content string `json:"content"`
DocType string `json:"doc_type,omitempty"`
Status string `json:"status,omitempty"`
Tags string `json:"tags,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
Source string `json:"source,omitempty"`
ChangeSummary string `json:"change_summary,omitempty"`
Title string `json:"title"`
Content string `json:"content"`
DocType string `json:"doc_type,omitempty"`
Status string `json:"status,omitempty"`
Tags string `json:"tags,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
Source string `json:"source,omitempty"`
ChangeSummary string `json:"change_summary,omitempty"`
}
type DocumentListParams struct {
+1 -1
View File
@@ -2,7 +2,7 @@ package models
// WorkspaceExport is the complete portable representation of a workspace.
type WorkspaceExport struct {
Version int `json:"version"` // Export format version (1)
Version int `json:"version"` // Export format version (1)
ExportedAt string `json:"exported_at"`
Workspace WorkspaceExportMeta `json:"workspace"`
Collections []CollectionExport `json:"collections"`
+9 -9
View File
@@ -47,17 +47,17 @@ type Item struct {
AgentRoleName string `json:"agent_role_name,omitempty"`
AgentRoleSlug string `json:"agent_role_slug,omitempty"`
AgentRoleIcon string `json:"agent_role_icon,omitempty"`
CollectionSlug string `json:"collection_slug,omitempty"`
CollectionName string `json:"collection_name,omitempty"`
CollectionIcon string `json:"collection_icon,omitempty"`
CollectionPrefix string `json:"collection_prefix,omitempty"`
CollectionSlug string `json:"collection_slug,omitempty"`
CollectionName string `json:"collection_name,omitempty"`
CollectionIcon string `json:"collection_icon,omitempty"`
CollectionPrefix string `json:"collection_prefix,omitempty"`
// Parent link (populated by enrichItemForResponse / enrichItemsWithParent)
ParentLinkID string `json:"parent_link_id,omitempty"`
ParentRef string `json:"parent_ref,omitempty"`
ParentTitle string `json:"parent_title,omitempty"`
ParentSlug string `json:"parent_slug,omitempty"`
ParentCollectionSlug string `json:"parent_collection_slug,omitempty"`
ParentLinkID string `json:"parent_link_id,omitempty"`
ParentRef string `json:"parent_ref,omitempty"`
ParentTitle string `json:"parent_title,omitempty"`
ParentSlug string `json:"parent_slug,omitempty"`
ParentCollectionSlug string `json:"parent_collection_slug,omitempty"`
// HasChildren is true if this item has child items linked to it.
// Populated by enrichment, not stored in the DB.
+1 -1
View File
@@ -12,7 +12,7 @@ const (
ItemLinkTypeSplitFrom = "split_from"
ItemLinkTypeSupersedes = "supersedes"
ItemLinkTypeImplements = "implements"
ItemLinkTypeParent = "parent"
ItemLinkTypeParent = "parent"
)
var itemLinkTypeAliases = map[string]string{
+6 -6
View File
@@ -6,15 +6,15 @@ import "time"
// Tokens are hashed at rest; the raw token is returned only once on creation.
type ShareLink struct {
ID string `json:"id"`
TokenHash string `json:"-"` // Never serialized
Token string `json:"token,omitempty"` // Only set on creation response
TargetType string `json:"target_type"` // "item" or "collection"
TokenHash string `json:"-"` // Never serialized
Token string `json:"token,omitempty"` // Only set on creation response
TargetType string `json:"target_type"` // "item" or "collection"
TargetID string `json:"target_id"`
WorkspaceID string `json:"workspace_id"`
Permission string `json:"permission"` // "view" or "edit"
Permission string `json:"permission"` // "view" or "edit"
CreatedBy string `json:"created_by"`
PasswordHash *string `json:"-"` // Never serialized
HasPassword bool `json:"has_password"` // Derived: password_hash IS NOT NULL
PasswordHash *string `json:"-"` // Never serialized
HasPassword bool `json:"has_password"` // Derived: password_hash IS NOT NULL
ExpiresAt *time.Time `json:"expires_at,omitempty"`
MaxViews *int `json:"max_views,omitempty"`
RequireAuth bool `json:"require_auth"`
+12 -12
View File
@@ -9,21 +9,21 @@ import (
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Username string `json:"username"` // Unique handle; empty until set
Username string `json:"username"` // Unique handle; empty until set
Name string `json:"name"`
PasswordHash string `json:"-"` // Never serialized
Role string `json:"role"` // "admin" or "member"
PasswordHash string `json:"-"` // Never serialized
Role string `json:"role"` // "admin" or "member"
AvatarURL string `json:"avatar_url,omitempty"`
TOTPSecret string `json:"-"` // Never serialized
TOTPSecret string `json:"-"` // Never serialized
TOTPEnabled bool `json:"totp_enabled"`
RecoveryCodes string `json:"-"` // Never serialized
Plan string `json:"plan"` // "free", "pro", or "self-hosted"
RecoveryCodes string `json:"-"` // Never serialized
Plan string `json:"plan"` // "free", "pro", or "self-hosted"
PlanExpiresAt string `json:"plan_expires_at,omitempty"`
StripeCustomerID string `json:"-"` // Never serialized
StripeCustomerID string `json:"-"` // Never serialized
PlanOverrides string `json:"plan_overrides,omitempty"` // JSON overrides for per-user limits
OAuthProviders string `json:"-"` // JSON array of linked providers, e.g. ["github","google"]
PasswordSet bool `json:"password_set"` // True if the user explicitly set a password (vs. OAuth placeholder hash)
DisabledAt string `json:"disabled_at,omitempty"` // Non-empty = account disabled
OAuthProviders string `json:"-"` // JSON array of linked providers, e.g. ["github","google"]
PasswordSet bool `json:"password_set"` // True if the user explicitly set a password (vs. OAuth placeholder hash)
DisabledAt string `json:"disabled_at,omitempty"` // Non-empty = account disabled
LastActiveAt string `json:"last_active_at,omitempty"` // Last authenticated API request
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -69,8 +69,8 @@ type UserCreate struct {
Email string `json:"email"`
Username string `json:"username,omitempty"` // Optional; auto-generated if empty
Name string `json:"name"`
Password string `json:"password"` // Plaintext, will be hashed
Role string `json:"role,omitempty"` // Defaults to "member"
Password string `json:"password"` // Plaintext, will be hashed
Role string `json:"role,omitempty"` // Defaults to "member"
}
// UserUpdate is the input for updating user profile fields.
+10 -10
View File
@@ -3,19 +3,19 @@ package models
import "time"
type Workspace struct {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
OwnerID string `json:"owner_id,omitempty"` // User ID of workspace owner
OwnerUsername string `json:"owner_username,omitempty"` // Populated by JOIN (not stored)
IsGuest bool `json:"is_guest,omitempty"` // True when user has grants but no membership
Description string `json:"description"`
Settings string `json:"settings"` // JSON
SortOrder int `json:"sort_order"`
Context *WorkspaceContext `json:"context,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
Description string `json:"description"`
Settings string `json:"settings"` // JSON
SortOrder int `json:"sort_order"`
Context *WorkspaceContext `json:"context,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}
type WorkspaceCreate struct {
-1
View File
@@ -60,4 +60,3 @@ func TestDecodeJSONWithLimit_CustomCap(t *testing.T) {
t.Fatal("expected 256 KiB cap to reject 1 MiB body, got nil")
}
}
@@ -9,11 +9,11 @@ func TestNormalizeRecoveryCode(t *testing.T) {
tests := []struct {
in, want string
}{
{"ABCDEFGHIJKLMNOP", "ABCDEFGHIJKLMNOP"}, // already normalized
{"abcdefghijklmnop", "ABCDEFGHIJKLMNOP"}, // lowercase → upper
{"ABCD-EFGH-IJKL-MNOP", "ABCDEFGHIJKLMNOP"}, // dashes stripped
{"ABCDEFGHIJKLMNOP", "ABCDEFGHIJKLMNOP"}, // already normalized
{"abcdefghijklmnop", "ABCDEFGHIJKLMNOP"}, // lowercase → upper
{"ABCD-EFGH-IJKL-MNOP", "ABCDEFGHIJKLMNOP"}, // dashes stripped
{" abcd efgh ijkl mnop ", "ABCDEFGHIJKLMNOP"}, // whitespace stripped
{"ABcd-EFgh\nIJkl", "ABCDEFGHIJKL"}, // mixed + newline
{"ABcd-EFgh\nIJkl", "ABCDEFGHIJKL"}, // mixed + newline
{"", ""},
}
for _, tt := range tests {
+4 -4
View File
@@ -10,11 +10,11 @@ import (
// Known platform setting keys. Values are stored in the platform_settings table.
const (
settingEmailProvider = "email_provider" // "maileroo" or empty
settingEmailProvider = "email_provider" // "maileroo" or empty
settingMailerooAPIKey = "maileroo_api_key"
settingEmailFrom = "email_from" // Sender address
settingEmailFromName = "email_from_name" // Sender display name
settingPlatformName = "platform_name" // Instance name (default: "Pad")
settingEmailFrom = "email_from" // Sender address
settingEmailFromName = "email_from_name" // Sender display name
settingPlatformName = "platform_name" // Instance name (default: "Pad")
// Token policy settings
settingTokenDefaultExpiryDays = "token_default_expiry_days" // Default: 90
+8 -8
View File
@@ -343,10 +343,10 @@ func (s *Server) handleAdminResetPassword(w http.ResponseWriter, r *http.Request
}))
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"method": "temporary_password",
"temp_password": tempPassword,
"message": "Temporary password generated. The user's existing sessions have been invalidated.",
"ok": true,
"method": "temporary_password",
"temp_password": tempPassword,
"message": "Temporary password generated. The user's existing sessions have been invalidated.",
})
}
@@ -550,10 +550,10 @@ func (s *Server) handleAdminStats(w http.ResponseWriter, r *http.Request) {
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"users": userCount,
"users_by_plan": planCounts,
"workspaces": len(workspaces),
"cloud_mode": s.cloudMode,
"users": userCount,
"users_by_plan": planCounts,
"workspaces": len(workspaces),
"cloud_mode": s.cloudMode,
})
}
+8 -8
View File
@@ -15,14 +15,14 @@ import (
// Dashboard response types
type DashboardResponse struct {
Summary DashboardSummary `json:"summary"`
ActiveItems []DashboardActiveItem `json:"active_items"`
ActivePlans []DashboardPlan `json:"active_plans"`
StarredItems []DashboardActiveItem `json:"starred_items,omitempty"`
ByRole []store.RoleBreakdown `json:"by_role,omitempty"`
Attention []DashboardAttention `json:"attention"`
RecentActivity []DashboardActivity `json:"recent_activity"`
SuggestedNext []DashboardSuggestion `json:"suggested_next"`
Summary DashboardSummary `json:"summary"`
ActiveItems []DashboardActiveItem `json:"active_items"`
ActivePlans []DashboardPlan `json:"active_plans"`
StarredItems []DashboardActiveItem `json:"starred_items,omitempty"`
ByRole []store.RoleBreakdown `json:"by_role,omitempty"`
Attention []DashboardAttention `json:"attention"`
RecentActivity []DashboardActivity `json:"recent_activity"`
SuggestedNext []DashboardSuggestion `json:"suggested_next"`
}
type DashboardActiveItem struct {
+3 -3
View File
@@ -14,9 +14,9 @@ import (
// rbacTestEnv holds everything needed for RBAC tests:
// a server with an admin, workspace, and users with different roles.
type rbacTestEnv struct {
srv *Server
wsSlug string
ownerToken string
srv *Server
wsSlug string
ownerToken string
editorToken string
viewerToken string
}
+1 -1
View File
@@ -122,7 +122,7 @@ func TestTrustedProxyRealIP_TrustedPeer_XRealIPUsed(t *testing.T) {
mw := TrustedProxyRealIP(cidrs)(next)
req := httptest.NewRequest("GET", "/", nil)
req.RemoteAddr = "10.0.0.5:12345" // in trusted CIDR
req.RemoteAddr = "10.0.0.5:12345" // in trusted CIDR
req.Header.Set("X-Real-IP", "198.51.100.7")
mw.ServeHTTP(httptest.NewRecorder(), req)
+3 -3
View File
@@ -16,9 +16,9 @@ func TestSecurityHeaders(t *testing.T) {
headers := map[string]string{
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
"X-Frame-Options": "DENY",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
}
for name, expected := range headers {
-1
View File
@@ -463,4 +463,3 @@ func (s *Store) ResolveAgentRoleID(workspaceID, idOrSlug string) (string, error)
}
return role.ID, nil
}
+3 -3
View File
@@ -197,9 +197,9 @@ func TestConcurrentMixedReadWrite(t *testing.T) {
)
var (
readOps, writeOps atomic.Int64
readErrs, writeErrs atomic.Int64
wg sync.WaitGroup
readOps, writeOps atomic.Int64
readErrs, writeErrs atomic.Int64
wg sync.WaitGroup
)
deadline := time.Now().Add(duration)
-1
View File
@@ -593,4 +593,3 @@ func scanDocuments(rows *sql.Rows) ([]models.Document, error) {
}
return docs, rows.Err()
}
-1
View File
@@ -235,4 +235,3 @@ func (s *Store) DeleteStarsForItem(itemID string) error {
}
return nil
}
+3 -2
View File
@@ -1451,8 +1451,9 @@ func scanCollectionDoneFilters(rows *sql.Rows) []collectionDoneFilter {
// for GetAllItemProgress).
//
// Expression shape:
// ((<alias>.collection_id = ? AND LOWER(COALESCE(<field_A>, '')) IN (?,?)) OR
// (<alias>.collection_id = ? AND LOWER(COALESCE(<field_B>, '')) IN (?,?)))
//
// ((<alias>.collection_id = ? AND LOWER(COALESCE(<field_A>, '')) IN (?,?)) OR
// (<alias>.collection_id = ? AND LOWER(COALESCE(<field_B>, '')) IN (?,?)))
//
// The `<field_X>` JSON extract uses scalar text extraction; this works
// because DoneFieldKey in the models package only resolves done fields to
+1 -1
View File
@@ -50,7 +50,7 @@ var DefaultProLimits = PlanLimits{
type LimitResult struct {
Allowed bool `json:"allowed"`
Feature string `json:"feature"`
Limit int `json:"limit"` // -1 means unlimited
Limit int `json:"limit"` // -1 means unlimited
Current int `json:"current"`
Plan string `json:"plan"`
}
+1 -1
View File
@@ -355,7 +355,7 @@ func (s *Store) Search(params SearchParams) (*SearchResponse, error) {
// Direct ref hits (e.g. searching "TASK-5") are prepended to results
// and always appear first. They occupy slots on page 0; on subsequent
// pages we exclude them and adjust the FTS offset accordingly.
refHits := results // save ref results before FTS
refHits := results // save ref results before FTS
refCount := len(refHits)
refIDs := make(map[string]bool, refCount)
for _, r := range refHits {
+2 -2
View File
@@ -70,7 +70,7 @@ func TestSearchQualityComparison(t *testing.T) {
wsIDs[name] = ws.ID
coll, err := s.CreateCollection(ws.ID, models.CollectionCreate{
Name: "Tasks",
Name: "Tasks",
Schema: `{"fields":[{"key":"status","label":"Status","type":"select","options":["open","done"],"default":"open","required":true},{"key":"priority","label":"Priority","type":"select","options":["low","medium","high"]}]}`,
})
if err != nil {
@@ -193,7 +193,7 @@ func TestSearchEdgeCases(t *testing.T) {
ws := createTestWorkspace(t, s, "search-edge")
coll, err := s.CreateCollection(ws.ID, models.CollectionCreate{
Name: "Items",
Name: "Items",
Schema: `{"fields":[{"key":"status","label":"Status","type":"select","options":["open","done"],"default":"open","required":true}]}`,
})
if err != nil {
+3
View File
@@ -41,9 +41,11 @@ func (s *Store) DB() *sql.DB { return s.db }
// locked, retry for up to 5 seconds before returning SQLITE_BUSY. The
// pragma is applied per-connection by the driver, so every pool member
// inherits it.
//
// - `_pragma=foreign_keys(on)`: foreign-key enforcement is per-connection
// in SQLite. Setting it via the DSN guarantees ALL pool members enforce
// them, not just the one that received a `db.Exec("PRAGMA ...")` call.
//
// - `_txlock=immediate`: makes every `db.Begin()` issue `BEGIN IMMEDIATE`
// instead of the default `BEGIN DEFERRED`. With deferred mode, a
// transaction starts holding only a SHARED lock and tries to upgrade
@@ -79,6 +81,7 @@ func (s *Store) DB() *sql.DB { return s.db }
// model the realistic fallout is small (link tables already cascade),
// but the integrity check `PRAGMA foreign_key_check` can surface
// any pre-existing offenders.
//
// - `journal_mode=WAL` is set via Exec below; WAL is a database-level
// setting (stored in the file header), so it persists across
// connections after the first one applies it.
+4 -4
View File
@@ -238,8 +238,8 @@ func TestSQLiteConcurrentWritersNoBusy(t *testing.T) {
done.Add(1)
go func() {
defer done.Done()
ready.Done() // signal "I'm at the gate"
release.Wait() // park here until main thread releases
ready.Done() // signal "I'm at the gate"
release.Wait() // park here until main thread releases
for op := 0; op < opsPerWorker; op++ {
_, err := s.CreateItem(ws.ID, coll.ID, models.ItemCreate{
Title: fmt.Sprintf("concurrent-%d-%d", idx, op),
@@ -251,8 +251,8 @@ func TestSQLiteConcurrentWritersNoBusy(t *testing.T) {
}
}()
}
ready.Wait() // every worker has called ready.Done() (best-effort gate)
release.Done() // release them all at once
ready.Wait() // every worker has called ready.Done() (best-effort gate)
release.Done() // release them all at once
done.Wait()
close(errCh)
+3 -3
View File
@@ -30,9 +30,9 @@ type WebhookPayload struct {
// Dispatcher sends webhook HTTP POST notifications for workspace events.
type Dispatcher struct {
store WebhookStore
client *http.Client
SkipSSRF bool // Skip SSRF validation (for tests only)
store WebhookStore
client *http.Client
SkipSSRF bool // Skip SSRF validation (for tests only)
}
// NewDispatcher creates a Dispatcher with the given store.
+1 -1
View File
@@ -16,7 +16,7 @@ type mockStore struct {
mu sync.Mutex
hooks []models.Webhook
failures map[string]bool // id -> last failed state
updated chan string // signals when UpdateWebhookFailure is called
updated chan string // signals when UpdateWebhookFailure is called
}
func newMockStore(hooks []models.Webhook) *mockStore {