feat: registry_api + fn sync — sincronización de registry.db entre PCs
Nuevo sistema para mantener datos no regenerables (proposals, apps, projects, analysis, vaults, pc_locations) sincronizados entre múltiples máquinas via una API HTTP central desplegada en organic-machine.com. - Migración 011: tabla pc_locations (mapa de ubicaciones por PC) - registry/models.go: struct PcLocation - registry/store.go: CRUD PcLocation + helpers de sync - cmd/fn/sync.go: subcomando fn sync (push+pull, status, locations) - bash/functions/infra/setup_registry_api: pipeline de deploy Docker+Traefik - CLAUDE.md: documentación de sync y pc_locations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
-- pc_locations: mapa de ubicaciones por máquina.
|
||||
-- Cada PC registra dónde tiene cada app, analysis, project o vault.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pc_locations (
|
||||
id TEXT PRIMARY KEY,
|
||||
entity_type TEXT NOT NULL CHECK(entity_type IN ('app', 'analysis', 'project', 'vault')),
|
||||
entity_id TEXT NOT NULL,
|
||||
pc_id TEXT NOT NULL,
|
||||
dir_path TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'missing', 'archived')),
|
||||
notes TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(entity_type, entity_id, pc_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pc_locations_pc ON pc_locations(pc_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_pc_locations_entity ON pc_locations(entity_type, entity_id);
|
||||
@@ -224,6 +224,19 @@ type Vault struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// PcLocation maps an entity to a directory path on a specific PC.
|
||||
type PcLocation struct {
|
||||
ID string `json:"id"`
|
||||
EntityType string `json:"entity_type"`
|
||||
EntityID string `json:"entity_id"`
|
||||
PcID string `json:"pc_id"`
|
||||
DirPath string `json:"dir_path"`
|
||||
Status string `json:"status"`
|
||||
Notes string `json:"notes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// GenerateID builds the canonical ID: {name}_{lang}_{domain}
|
||||
func GenerateID(name, lang, domain string) string {
|
||||
return name + "_" + lang + "_" + domain
|
||||
|
||||
@@ -1094,3 +1094,116 @@ func scanProposals(rows interface{ Next() bool; Scan(...any) error }) ([]Proposa
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- PcLocation CRUD ---
|
||||
|
||||
// InsertPcLocation inserts or replaces a pc_location entry.
|
||||
func (db *DB) InsertPcLocation(loc *PcLocation) error {
|
||||
now := time.Now().UTC()
|
||||
if loc.CreatedAt.IsZero() {
|
||||
loc.CreatedAt = now
|
||||
}
|
||||
if loc.UpdatedAt.IsZero() {
|
||||
loc.UpdatedAt = now
|
||||
}
|
||||
if loc.ID == "" {
|
||||
loc.ID = loc.EntityType + "_" + loc.EntityID + "_" + loc.PcID
|
||||
}
|
||||
if loc.Status == "" {
|
||||
loc.Status = "active"
|
||||
}
|
||||
|
||||
_, err := db.conn.Exec(`
|
||||
INSERT OR REPLACE INTO pc_locations (
|
||||
id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
loc.ID, loc.EntityType, loc.EntityID, loc.PcID, loc.DirPath, loc.Status, loc.Notes,
|
||||
loc.CreatedAt.Format(time.RFC3339), loc.UpdatedAt.Format(time.RFC3339),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPcLocationsByPC returns all locations for a given PC.
|
||||
func (db *DB) GetPcLocationsByPC(pcID string) ([]PcLocation, error) {
|
||||
rows, err := db.conn.Query(
|
||||
"SELECT id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at FROM pc_locations WHERE pc_id = ? ORDER BY entity_type, entity_id",
|
||||
pcID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanPcLocations(rows)
|
||||
}
|
||||
|
||||
// GetPcLocationsByEntity returns all PC locations for a given entity.
|
||||
func (db *DB) GetPcLocationsByEntity(entityType, entityID string) ([]PcLocation, error) {
|
||||
rows, err := db.conn.Query(
|
||||
"SELECT id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at FROM pc_locations WHERE entity_type = ? AND entity_id = ? ORDER BY pc_id",
|
||||
entityType, entityID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanPcLocations(rows)
|
||||
}
|
||||
|
||||
// ListAllPcLocations returns all pc_location entries.
|
||||
func (db *DB) ListAllPcLocations() ([]PcLocation, error) {
|
||||
rows, err := db.conn.Query(
|
||||
"SELECT id, entity_type, entity_id, pc_id, dir_path, status, notes, created_at, updated_at FROM pc_locations ORDER BY pc_id, entity_type, entity_id",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanPcLocations(rows)
|
||||
}
|
||||
|
||||
// DeletePcLocationsByPC removes all locations for a given PC.
|
||||
func (db *DB) DeletePcLocationsByPC(pcID string) error {
|
||||
_, err := db.conn.Exec("DELETE FROM pc_locations WHERE pc_id = ?", pcID)
|
||||
return err
|
||||
}
|
||||
|
||||
func scanPcLocations(rows interface{ Next() bool; Scan(...any) error }) ([]PcLocation, error) {
|
||||
var result []PcLocation
|
||||
for rows.Next() {
|
||||
var loc PcLocation
|
||||
var createdAt, updatedAt string
|
||||
err := rows.Scan(
|
||||
&loc.ID, &loc.EntityType, &loc.EntityID, &loc.PcID,
|
||||
&loc.DirPath, &loc.Status, &loc.Notes, &createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanning pc_location: %w", err)
|
||||
}
|
||||
loc.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
loc.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
result = append(result, loc)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- Sync helpers ---
|
||||
|
||||
// AllApps returns all apps (for sync export).
|
||||
func (db *DB) AllApps() ([]App, error) {
|
||||
return db.SearchApps("", "", "")
|
||||
}
|
||||
|
||||
// AllAnalysis returns all analysis entries (for sync export).
|
||||
func (db *DB) AllAnalysis() ([]Analysis, error) {
|
||||
return db.SearchAnalysis("", "", "")
|
||||
}
|
||||
|
||||
// AllProposals returns all proposals (for sync export).
|
||||
func (db *DB) AllProposals() ([]Proposal, error) {
|
||||
return db.ListProposals("", "")
|
||||
}
|
||||
|
||||
// AllVaults returns all vaults (for sync export).
|
||||
func (db *DB) AllVaults() ([]Vault, error) {
|
||||
return db.SearchVaults("", "")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user