mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-08 00:02:09 +00:00
Migration 000032 + repo + endpoints for arbitrary string tags on
workers. The fixed attributes (worker_type, free_tier, risk_pool)
cover the dimensions assignment logic uses. Tags cover everything
else admins want to group by: region (eu-west, fra), provider
(hetzner, ovh), role (warmup-only, burst-capacity), customer cohort —
whatever.
Schema:
- worker_tags(worker_id, tag) composite PK
- tag VARCHAR(64), lowercase + dashed via CHECK constraint
- ON DELETE CASCADE so deleting a worker drops its tags
Endpoints:
- GET /admin/workers/tags list distinct tags (autocomplete)
- PUT /admin/workers/:id/tags replace tag set; normalizes input
Repo:
- GetWorkerTags / SetWorkerTags / ListAllWorkerTags
- HydrateWorkerTags batch-loads tags onto a slice of workers in one
round-trip so the dashboard list doesn't do N+1 queries
PUT is transactional (delete + bulk insert) so the list view never
catches a worker mid-tag-swap. Auto-derived "smart" labels
(tier:free, pool:risky, state:error) are NOT stored — those are
computed client-side from the worker row so they stay in sync with
the source attributes automatically. Next commit wires the UI.
194 lines
7.9 KiB
Go
194 lines
7.9 KiB
Go
package models
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// WorkerType represents the type of worker
|
|
type WorkerType string
|
|
|
|
const (
|
|
WorkerTypeShared WorkerType = "shared"
|
|
WorkerTypeDedicated WorkerType = "dedicated"
|
|
)
|
|
|
|
// WorkerRiskPool buckets shared workers by acceptable mailbox risk level.
|
|
// Dedicated workers don't use it (one customer per worker — no
|
|
// cross-tenant contamination risk).
|
|
type WorkerRiskPool string
|
|
|
|
const (
|
|
WorkerRiskPoolClean WorkerRiskPool = "clean"
|
|
WorkerRiskPoolRisky WorkerRiskPool = "risky"
|
|
WorkerRiskPoolQuarantine WorkerRiskPool = "quarantine"
|
|
)
|
|
|
|
type Worker struct {
|
|
ID uuid.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
Notes string `json:"notes"`
|
|
IPAddr string `json:"ip_addr"`
|
|
Active bool `json:"active"`
|
|
FreeTier bool `json:"free_tier"`
|
|
WorkerType WorkerType `json:"worker_type"`
|
|
AccountCount int `json:"account_count"`
|
|
RiskPool WorkerRiskPool `json:"risk_pool"`
|
|
|
|
// SSH management (none of these expose secret material — the encrypted
|
|
// private key is fetched separately via GetWorkerSSHCredentials).
|
|
SSHHost string `json:"ssh_host,omitempty"`
|
|
SSHPort int `json:"ssh_port,omitempty"`
|
|
SSHUser string `json:"ssh_user,omitempty"`
|
|
SSHPublicKey string `json:"ssh_public_key,omitempty"`
|
|
SSHHostFingerprint string `json:"ssh_host_fingerprint,omitempty"`
|
|
InstallState WorkerInstallState `json:"install_state"`
|
|
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
|
LastError string `json:"last_error,omitempty"`
|
|
|
|
// Profile assignment. Nil means "use backend env defaults".
|
|
ProfileID *uuid.UUID `json:"profile_id,omitempty"`
|
|
ConfigAppliedAt *time.Time `json:"config_applied_at,omitempty"`
|
|
|
|
// Image tag the worker is currently running, captured on every successful
|
|
// Update. Used for the "v1.2.3 → v1.2.4" badge in the dashboard.
|
|
ImageVersion string `json:"image_version,omitempty"`
|
|
|
|
// Admin-applied free-form tags (eu-west, hetzner, warmup-only, ...).
|
|
// Auto-derived "smart" labels (tier:free, pool:risky, state:error) are
|
|
// computed client-side from the worker row and never stored here.
|
|
Tags []string `json:"tags,omitempty"`
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// WorkerInstallState mirrors the worker_install_state enum.
|
|
type WorkerInstallState string
|
|
|
|
const (
|
|
WorkerInstallStatePending WorkerInstallState = "pending"
|
|
WorkerInstallStateProvisioning WorkerInstallState = "provisioning"
|
|
WorkerInstallStateInstalled WorkerInstallState = "installed"
|
|
WorkerInstallStateError WorkerInstallState = "error"
|
|
WorkerInstallStateUninstalling WorkerInstallState = "uninstalling"
|
|
WorkerInstallStateUninstalled WorkerInstallState = "uninstalled"
|
|
)
|
|
|
|
// WorkerSSHCredentials carries the encrypted private key alongside the
|
|
// connection info. Only the orchestrator should ever fetch this; the field is
|
|
// never serialised to admin clients.
|
|
type WorkerSSHCredentials struct {
|
|
WorkerID uuid.UUID
|
|
SSHHost string
|
|
SSHPort int
|
|
SSHUser string
|
|
SSHPublicKey string
|
|
SSHPrivateKeyEncrypted string
|
|
SSHHostFingerprint string
|
|
}
|
|
|
|
type UpdateWorker struct {
|
|
IPAddr *string `json:"ip_addr"`
|
|
Active *bool `json:"active"`
|
|
WorkerType *WorkerType `json:"worker_type,omitempty"`
|
|
}
|
|
|
|
// DedicatedWorkerAssignment represents a dedicated worker assignment to a user
|
|
type DedicatedWorkerAssignment struct {
|
|
ID uuid.UUID `json:"id"`
|
|
WorkerID uuid.UUID `json:"worker_id"`
|
|
UserID uuid.UUID `json:"user_id"`
|
|
SubscriptionID uuid.UUID `json:"subscription_id"`
|
|
AssignedAt time.Time `json:"assigned_at"`
|
|
ReleasedAt *time.Time `json:"released_at,omitempty"`
|
|
}
|
|
|
|
type WorkerStatus string
|
|
|
|
const (
|
|
WorkerStatusOffline WorkerStatus = "offline"
|
|
WorkerStatusLoading WorkerStatus = "loading"
|
|
WorkerStatusOnline WorkerStatus = "online"
|
|
)
|
|
|
|
type SendEmail struct {
|
|
TaskID uuid.UUID `json:"task_id" avro:"task_id"`
|
|
EmailID uuid.UUID `json:"email_id" avro:"email_id"`
|
|
UserID uuid.UUID `json:"user_id" avro:"user_id"`
|
|
To []string `json:"to" avro:"to"`
|
|
Cc []string `json:"cc" avro:"cc"`
|
|
Bcc []string `json:"bcc" avro:"bcc"`
|
|
Subject string `json:"subject" avro:"subject"`
|
|
BodyS3Key string `json:"body_s3_key" avro:"body_s3_key"`
|
|
MessageID string `json:"message_id" avro:"message_id"`
|
|
InReplyTo string `json:"in_reply_to,omitempty" avro:"in_reply_to"`
|
|
Parent *EmailParent `json:"parent,omitempty" avro:"parent"`
|
|
IsWarmup bool `json:"is_warmup" avro:"is_warmup"`
|
|
TrackingInfo *TrackingInfo `json:"tracking_info,omitempty" avro:"tracking_info"`
|
|
WarmupToken string `json:"warmup_token,omitempty" avro:"warmup_token"`
|
|
UnsubscribeURL string `json:"unsubscribe_url,omitempty" avro:"unsubscribe_url"`
|
|
}
|
|
|
|
// TrackingInfo contains tracking configuration for campaign emails
|
|
type TrackingInfo struct {
|
|
OpenTracking bool `json:"open_tracking" avro:"open_tracking"`
|
|
LinkTracking bool `json:"link_tracking" avro:"link_tracking"`
|
|
TrackingDomain string `json:"tracking_domain" avro:"tracking_domain"`
|
|
}
|
|
|
|
// EmailSendError contains detailed error information for failed email sends
|
|
type EmailSendError struct {
|
|
Code string `json:"code" avro:"code"`
|
|
Type string `json:"type" avro:"type"`
|
|
Message string `json:"message" avro:"message"`
|
|
ResolveMethod string `json:"resolve_method" avro:"resolve_method"`
|
|
UserVisible bool `json:"user_visible" avro:"user_visible"`
|
|
UserTitle string `json:"user_title,omitempty" avro:"user_title"`
|
|
UserMessage string `json:"user_message,omitempty" avro:"user_message"`
|
|
ActionRequired string `json:"action_required,omitempty" avro:"action_required"`
|
|
}
|
|
|
|
// SendEmailResult is the result from worker after sending email
|
|
type SendEmailResult struct {
|
|
TaskID uuid.UUID `json:"task_id" avro:"task_id"`
|
|
Success bool `json:"success" avro:"success"`
|
|
MessageID string `json:"message_id,omitempty" avro:"message_id"`
|
|
ProviderMsgID string `json:"provider_msg_id,omitempty" avro:"provider_msg_id"`
|
|
SentAt time.Time `json:"sent_at,omitempty" avro:"sent_at"`
|
|
Error *EmailSendError `json:"error,omitempty" avro:"error"`
|
|
LegacyErrorMsg string `json:"legacy_error,omitempty" avro:"legacy_error"` // Deprecated: use Error instead
|
|
}
|
|
|
|
type AddWorkerEmailGoogleData struct {
|
|
LastHistoryID uint64 `json:"last_history_id" avro:"last_history_id"`
|
|
Token *oauth2.Token `json:"token" avro:"token"`
|
|
}
|
|
|
|
type AddWorkerEmailSmtpImapData struct {
|
|
Mailboxes []Mailbox `json:"mailboxes" avro:"mailboxes"`
|
|
Token *oauth2.Token `json:"token" avro:"token"`
|
|
Credentials *SmtpImap `json:"credentials" avro:"credentials"`
|
|
}
|
|
|
|
type AddWorkerEmail struct {
|
|
ID uuid.UUID `json:"id" avro:"id"`
|
|
UserID uuid.UUID `json:"user_id" avro:"user_id"`
|
|
ImapSync bool `json:"imap_sync" avro:"imap_sync"`
|
|
Email string `json:"email" avro:"email"`
|
|
FirstName string `json:"first_name" avro:"first_name"`
|
|
LastName string `json:"last_name" avro:"last_name"`
|
|
Type InboxProvider `json:"type" avro:"type"`
|
|
Google *AddWorkerEmailGoogleData `json:"google" avro:"google"`
|
|
SmtpImap *AddWorkerEmailSmtpImapData `json:"smtp_imap" avro:"smtp_imap"`
|
|
|
|
Cfg oauth2.Config `json:"-" avro:"-"`
|
|
}
|
|
|
|
type RemoveWorkerEmail struct {
|
|
UserID string `json:"user_id" avro:"user_id"`
|
|
EmailID string `json:"email_id" avro:"email_id"`
|
|
}
|