package storage import ( "context" "errors" "fmt" "strings" "time" "github.com/jackc/pgx/v5" ) // OllamaConfig is the per-tenant connection configuration for an EXTERNAL, // already-running Ollama server (never installed on the archivdms host — the // URL/port is provided by the tenant admin). It gates the optional 'ollama' // metadata-suggestion provider. BaseURL is an internal network URL, not a // secret, so it is returned to the API as-is (unlike the LDAP bind password). type OllamaConfig struct { TenantID int64 `json:"tenant_id"` Enabled bool `json:"enabled"` BaseURL string `json:"base_url"` Model string `json:"model"` TimeoutSeconds int `json:"timeout_seconds"` UpdatedAt time.Time `json:"updated_at"` } // defaultOllamaTimeoutSeconds is used for rows that predate the column default // or were never configured, so GetOllamaConfig never returns a zero timeout. const defaultOllamaTimeoutSeconds = 30 // initOllamaConfigSchema creates the tenant_ollama_config table. Idempotent, // called from (*Store).initSchema. Documented (not executed) in // migrations/017_tenant_ollama_config.sql. // // No FK enforcement beyond the documented REFERENCES: consistent with the rest // of the schema (tenant_id is a plain BIGINT elsewhere); the PRIMARY KEY gives // the one-row-per-tenant upsert target. func (s *Store) initOllamaConfigSchema(ctx context.Context) error { _, err := s.db.Exec(ctx, ` CREATE TABLE IF NOT EXISTS tenant_ollama_config ( tenant_id BIGINT PRIMARY KEY, enabled BOOLEAN NOT NULL DEFAULT false, base_url TEXT NOT NULL DEFAULT '', model TEXT NOT NULL DEFAULT '', timeout_seconds INT NOT NULL DEFAULT 30, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); `) if err != nil { return fmt.Errorf("storage: create tenant_ollama_config table: %w", err) } return nil } // GetOllamaConfig returns the tenant's Ollama connection config. When no row // exists it returns a zero/default (disabled, empty URL/model, default // timeout) config and NO error — callers treat "not configured" as "disabled". func (s *Store) GetOllamaConfig(ctx context.Context, tenantID int64) (*OllamaConfig, error) { row := s.db.QueryRow(ctx, ` SELECT tenant_id, enabled, base_url, model, timeout_seconds, updated_at FROM tenant_ollama_config WHERE tenant_id = $1`, tenantID) var c OllamaConfig err := row.Scan(&c.TenantID, &c.Enabled, &c.BaseURL, &c.Model, &c.TimeoutSeconds, &c.UpdatedAt) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return &OllamaConfig{ TenantID: tenantID, Enabled: false, TimeoutSeconds: defaultOllamaTimeoutSeconds, }, nil } return nil, fmt.Errorf("storage: get ollama config: %w", err) } if c.TimeoutSeconds <= 0 { c.TimeoutSeconds = defaultOllamaTimeoutSeconds } return &c, nil } // UpsertOllamaConfig creates or updates the tenant's Ollama connection config. // When enabled is true, base_url (http/https prefixed) and model must be set // and timeout_seconds must be in [5,120]; when disabled the fields may be empty // (the switch can be flipped off without wiping the stored URL/model, but the // values are still range-checked when present). func (s *Store) UpsertOllamaConfig(ctx context.Context, tenantID int64, enabled bool, baseURL, model string, timeoutSeconds int) error { baseURL = strings.TrimSpace(baseURL) model = strings.TrimSpace(model) if timeoutSeconds == 0 { timeoutSeconds = defaultOllamaTimeoutSeconds } if timeoutSeconds < 5 || timeoutSeconds > 120 { return fmt.Errorf("Timeout muss zwischen 5 und 120 Sekunden liegen") } if enabled { if baseURL == "" || model == "" { return fmt.Errorf("Server-URL und Modell müssen ausgefüllt sein, um Ollama zu aktivieren") } if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") { return fmt.Errorf("Server-URL muss mit http:// oder https:// beginnen") } } // Normalise the base URL by stripping a trailing slash so the client can // always append "/api/generate" without producing a double slash. baseURL = strings.TrimRight(baseURL, "/") _, err := s.db.Exec(ctx, ` INSERT INTO tenant_ollama_config (tenant_id, enabled, base_url, model, timeout_seconds, updated_at) VALUES ($1, $2, $3, $4, $5, now()) ON CONFLICT (tenant_id) DO UPDATE SET enabled = EXCLUDED.enabled, base_url = EXCLUDED.base_url, model = EXCLUDED.model, timeout_seconds = EXCLUDED.timeout_seconds, updated_at = now()`, tenantID, enabled, baseURL, model, timeoutSeconds) if err != nil { return fmt.Errorf("storage: upsert ollama config: %w", err) } return nil }