From d547863ebb71bb63083178329194393a4daa03ea Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Thu, 1 Jan 2026 15:25:30 +0800 Subject: [PATCH] feat: add SQLite/PostgreSQL database switching support --- api/server.go | 3 + go.mod | 1 + go.sum | 2 + main.go | 27 +- store/driver.go | 240 ++++++++++++++++++ store/store.go | 90 ++++--- web/src/components/landing/core/AgentGrid.tsx | 4 +- .../components/landing/core/DeploymentHub.tsx | 121 +++++++++ web/src/pages/LandingPage.tsx | 3 + 9 files changed, 442 insertions(+), 49 deletions(-) create mode 100644 store/driver.go create mode 100644 web/src/components/landing/core/DeploymentHub.tsx diff --git a/api/server.go b/api/server.go index 2f63e7c6..ccc16c8f 100644 --- a/api/server.go +++ b/api/server.go @@ -14,6 +14,9 @@ import ( "nofx/manager" "nofx/market" "nofx/provider/alpaca" + "nofx/provider/coinank/coinank_api" + "nofx/provider/coinank/coinank_enum" + "nofx/provider/hyperliquid" "nofx/provider/twelvedata" "nofx/store" "nofx/trader" diff --git a/go.mod b/go.mod index 820e55d4..3c4d11fa 100644 --- a/go.mod +++ b/go.mod @@ -56,6 +56,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 9c9a8987..d4d1591b 100644 --- a/go.sum +++ b/go.sum @@ -133,6 +133,8 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= diff --git a/main.go b/main.go index 56879d33..3350d423 100644 --- a/main.go +++ b/main.go @@ -36,21 +36,30 @@ func main() { cfg := config.Get() logger.Info("✅ Configuration loaded") - // Initialize database - // Default path is data/data.db to work with Docker volume mount (/app/data) - dbPath := "data/data.db" + // Initialize database from environment variables + // DB_TYPE: sqlite (default) or postgres + // For SQLite: DB_PATH (default: data/data.db) + // For PostgreSQL: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, DB_SSLMODE + dbPath := os.Getenv("DB_PATH") + if dbPath == "" { + dbPath = "data/data.db" + } + // For backward compatibility: command line arg overrides env var (SQLite only) if len(os.Args) > 1 { dbPath = os.Args[1] + os.Setenv("DB_PATH", dbPath) } - // Ensure data directory exists - if dir := filepath.Dir(dbPath); dir != "." { - if err := os.MkdirAll(dir, 0755); err != nil { - logger.Errorf("Failed to create data directory: %v", err) + // Ensure data directory exists (for SQLite) + if os.Getenv("DB_TYPE") == "" || os.Getenv("DB_TYPE") == "sqlite" { + if dir := filepath.Dir(dbPath); dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + logger.Errorf("Failed to create data directory: %v", err) + } } } - logger.Infof("📋 Initializing database: %s", dbPath) - st, err := store.New(dbPath) + logger.Info("📋 Initializing database...") + st, err := store.NewFromEnv() if err != nil { logger.Fatalf("❌ Failed to initialize database: %v", err) } diff --git a/store/driver.go b/store/driver.go new file mode 100644 index 00000000..244b18a3 --- /dev/null +++ b/store/driver.go @@ -0,0 +1,240 @@ +// Package store provides database driver abstraction +package store + +import ( + "database/sql" + "fmt" + "os" + "strings" + + _ "github.com/lib/pq" // PostgreSQL driver + _ "modernc.org/sqlite" // SQLite driver +) + +// DBType represents database type +type DBType string + +const ( + DBTypeSQLite DBType = "sqlite" + DBTypePostgres DBType = "postgres" +) + +// DBConfig database configuration +type DBConfig struct { + Type DBType // sqlite or postgres + Path string // SQLite file path (for sqlite) + Host string // PostgreSQL host (for postgres) + Port int // PostgreSQL port (for postgres) + User string // PostgreSQL user (for postgres) + Password string // PostgreSQL password (for postgres) + DBName string // PostgreSQL database name (for postgres) + SSLMode string // PostgreSQL SSL mode (for postgres) +} + +// DBDriver database driver abstraction +type DBDriver struct { + Type DBType + db *sql.DB +} + +// NewDBDriver creates database driver from config +func NewDBDriver(cfg DBConfig) (*DBDriver, error) { + var db *sql.DB + var err error + + switch cfg.Type { + case DBTypeSQLite: + db, err = openSQLite(cfg.Path) + case DBTypePostgres: + db, err = openPostgres(cfg) + default: + return nil, fmt.Errorf("unsupported database type: %s", cfg.Type) + } + + if err != nil { + return nil, err + } + + return &DBDriver{Type: cfg.Type, db: db}, nil +} + +// NewDBDriverFromEnv creates database driver from environment variables +// DB_TYPE: sqlite (default) or postgres +// For SQLite: DB_PATH (default: data/data.db) +// For PostgreSQL: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, DB_SSLMODE +func NewDBDriverFromEnv() (*DBDriver, error) { + dbType := DBType(strings.ToLower(getEnv("DB_TYPE", "sqlite"))) + + switch dbType { + case DBTypeSQLite: + path := getEnv("DB_PATH", "data/data.db") + return NewDBDriver(DBConfig{Type: DBTypeSQLite, Path: path}) + + case DBTypePostgres: + port := 5432 + if p := os.Getenv("DB_PORT"); p != "" { + fmt.Sscanf(p, "%d", &port) + } + return NewDBDriver(DBConfig{ + Type: DBTypePostgres, + Host: getEnv("DB_HOST", "localhost"), + Port: port, + User: getEnv("DB_USER", "postgres"), + Password: os.Getenv("DB_PASSWORD"), + DBName: getEnv("DB_NAME", "nofx"), + SSLMode: getEnv("DB_SSLMODE", "disable"), + }) + + default: + return nil, fmt.Errorf("unsupported DB_TYPE: %s (use 'sqlite' or 'postgres')", dbType) + } +} + +// DB returns underlying database connection +func (d *DBDriver) DB() *sql.DB { + return d.db +} + +// Close closes database connection +func (d *DBDriver) Close() error { + return d.db.Close() +} + +// AutoIncrement returns auto-increment syntax for current database +func (d *DBDriver) AutoIncrement() string { + switch d.Type { + case DBTypePostgres: + return "SERIAL" + default: + return "INTEGER PRIMARY KEY AUTOINCREMENT" + } +} + +// Placeholder returns placeholder for parameterized queries +// SQLite uses ?, PostgreSQL uses $1, $2, etc. +func (d *DBDriver) Placeholder(index int) string { + switch d.Type { + case DBTypePostgres: + return fmt.Sprintf("$%d", index) + default: + return "?" + } +} + +// ConvertPlaceholders converts ? placeholders to database-specific format +func (d *DBDriver) ConvertPlaceholders(query string) string { + if d.Type != DBTypePostgres { + return query + } + + // Convert ? to $1, $2, etc. for PostgreSQL + result := query + index := 1 + for strings.Contains(result, "?") { + result = strings.Replace(result, "?", fmt.Sprintf("$%d", index), 1) + index++ + } + return result +} + +// TableExists checks if a table exists +func (d *DBDriver) TableExists(tableName string) (bool, error) { + var exists bool + var query string + + switch d.Type { + case DBTypePostgres: + query = `SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' AND table_name = $1 + )` + default: + query = `SELECT EXISTS ( + SELECT 1 FROM sqlite_master + WHERE type = 'table' AND name = ? + )` + } + + query = d.ConvertPlaceholders(query) + err := d.db.QueryRow(query, tableName).Scan(&exists) + return exists, err +} + +// UpsertSyntax returns the upsert syntax for current database +// SQLite: INSERT ... ON CONFLICT(...) DO UPDATE SET ... +// PostgreSQL: INSERT ... ON CONFLICT(...) DO UPDATE SET ... +// Both use the same syntax in modern versions +func (d *DBDriver) UpsertSyntax() string { + return "ON CONFLICT" +} + +// openSQLite opens SQLite database +func openSQLite(path string) (*sql.DB, error) { + db, err := sql.Open("sqlite", path) + if err != nil { + return nil, fmt.Errorf("failed to open SQLite database: %w", err) + } + + // SQLite configuration + db.SetMaxOpenConns(1) + db.SetMaxIdleConns(1) + + // Enable foreign key constraints + if _, err := db.Exec(`PRAGMA foreign_keys = ON`); err != nil { + db.Close() + return nil, fmt.Errorf("failed to enable foreign keys: %w", err) + } + + // Use DELETE mode for Docker compatibility + if _, err := db.Exec("PRAGMA journal_mode=DELETE"); err != nil { + db.Close() + return nil, fmt.Errorf("failed to set journal_mode: %w", err) + } + + // Set synchronous=FULL + if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil { + db.Close() + return nil, fmt.Errorf("failed to set synchronous: %w", err) + } + + // Set busy_timeout + if _, err := db.Exec("PRAGMA busy_timeout = 5000"); err != nil { + db.Close() + return nil, fmt.Errorf("failed to set busy_timeout: %w", err) + } + + return db, nil +} + +// openPostgres opens PostgreSQL database +func openPostgres(cfg DBConfig) (*sql.DB, error) { + connStr := fmt.Sprintf( + "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", + cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode, + ) + + db, err := sql.Open("postgres", connStr) + if err != nil { + return nil, fmt.Errorf("failed to open PostgreSQL database: %w", err) + } + + // PostgreSQL configuration + db.SetMaxOpenConns(25) + db.SetMaxIdleConns(5) + + // Test connection + if err := db.Ping(); err != nil { + db.Close() + return nil, fmt.Errorf("failed to connect to PostgreSQL: %w", err) + } + + return db, nil +} + +// getEnv gets environment variable with default value +func getEnv(key, defaultValue string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultValue +} diff --git a/store/store.go b/store/store.go index 6f92edf6..4a3947b5 100644 --- a/store/store.go +++ b/store/store.go @@ -7,13 +7,12 @@ import ( "fmt" "nofx/logger" "sync" - - _ "modernc.org/sqlite" ) // Store unified data storage interface type Store struct { - db *sql.DB + db *sql.DB + driver *DBDriver // Database driver for abstraction // Sub-stores (lazy initialization) user *UserStore @@ -34,57 +33,56 @@ type Store struct { mu sync.RWMutex } -// New creates new Store instance +// New creates new Store instance (SQLite mode for backward compatibility) func New(dbPath string) (*Store, error) { - db, err := sql.Open("sqlite", dbPath) + driver, err := NewDBDriver(DBConfig{Type: DBTypeSQLite, Path: dbPath}) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } - // SQLite configuration - db.SetMaxOpenConns(1) - db.SetMaxIdleConns(1) - - // Enable foreign key constraints - if _, err := db.Exec(`PRAGMA foreign_keys = ON`); err != nil { - db.Close() - return nil, fmt.Errorf("failed to enable foreign keys: %w", err) - } - - // Use DELETE mode (traditional mode) to ensure Docker bind mount compatibility - // Note: WAL mode causes data sync issues on macOS Docker - if _, err := db.Exec("PRAGMA journal_mode=DELETE"); err != nil { - db.Close() - return nil, fmt.Errorf("failed to set journal_mode: %w", err) - } - - // Set synchronous=FULL - if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil { - db.Close() - return nil, fmt.Errorf("failed to set synchronous: %w", err) - } - - // Set busy_timeout - if _, err := db.Exec("PRAGMA busy_timeout = 5000"); err != nil { - db.Close() - return nil, fmt.Errorf("failed to set busy_timeout: %w", err) - } - - s := &Store{db: db} + s := &Store{db: driver.DB(), driver: driver} // Initialize all table structures if err := s.initTables(); err != nil { - db.Close() + driver.Close() return nil, fmt.Errorf("failed to initialize table structure: %w", err) } // Initialize default data if err := s.initDefaultData(); err != nil { - db.Close() + driver.Close() return nil, fmt.Errorf("failed to initialize default data: %w", err) } - logger.Info("✅ Database enabled DELETE mode and FULL sync") + logger.Infof("✅ Database initialized (type: %s)", driver.Type) + return s, nil +} + +// NewFromEnv creates new Store instance from environment variables +// DB_TYPE: sqlite (default) or postgres +// For SQLite: DB_PATH (default: data/data.db) +// For PostgreSQL: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME, DB_SSLMODE +func NewFromEnv() (*Store, error) { + driver, err := NewDBDriverFromEnv() + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + s := &Store{db: driver.DB(), driver: driver} + + // Initialize all table structures + if err := s.initTables(); err != nil { + driver.Close() + return nil, fmt.Errorf("failed to initialize table structure: %w", err) + } + + // Initialize default data + if err := s.initDefaultData(); err != nil { + driver.Close() + return nil, fmt.Errorf("failed to initialize default data: %w", err) + } + + logger.Infof("✅ Database initialized (type: %s)", driver.Type) return s, nil } @@ -293,9 +291,25 @@ func (s *Store) Order() *OrderStore { // Close closes database connection func (s *Store) Close() error { + if s.driver != nil { + return s.driver.Close() + } return s.db.Close() } +// Driver returns database driver for abstraction +func (s *Store) Driver() *DBDriver { + return s.driver +} + +// DBType returns current database type +func (s *Store) DBType() DBType { + if s.driver != nil { + return s.driver.Type + } + return DBTypeSQLite +} + // DB gets underlying database connection (for legacy code compatibility, gradually deprecated) // Deprecated: use Store methods instead func (s *Store) DB() *sql.DB { diff --git a/web/src/components/landing/core/AgentGrid.tsx b/web/src/components/landing/core/AgentGrid.tsx index 33f51ff8..691e7a6e 100644 --- a/web/src/components/landing/core/AgentGrid.tsx +++ b/web/src/components/landing/core/AgentGrid.tsx @@ -54,10 +54,10 @@ export default function AgentGrid() {
- Operator Select + MARKET SELECT

- Available Units + STRATEGY UNITS

diff --git a/web/src/components/landing/core/DeploymentHub.tsx b/web/src/components/landing/core/DeploymentHub.tsx new file mode 100644 index 00000000..1dbf2b0b --- /dev/null +++ b/web/src/components/landing/core/DeploymentHub.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { Terminal, Copy, Check, ChevronRight, Server, Command, Shield } from 'lucide-react' + +export default function DeploymentHub() { + const [copied, setCopied] = useState(false) + const installCmd = "curl -fsSL https://raw.githubusercontent.com/NoFxAiOS/nofx/main/install.sh | bash" + + const handleCopy = () => { + navigator.clipboard.writeText(installCmd) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( +
+ {/* Background Grids */} +
+ +
+
+ + {/* Left Column: Context */} +
+
+ System Deployment +
+ +

+ DEPLOY INSTANTLY +

+ +

+ Initialize your own high-frequency trading node in seconds. + Our optimized installer handles all dependencies, bringing your autonomous agent online with a single command. +

+ +
+ {[ + { icon: Command, label: "One-Line Install", desc: "No configuration needed" }, + { icon: Shield, label: "Secure Core", desc: "Sandboxed execution env" } + ].map((item, i) => ( +
+
+ +
+
+

{item.label}

+

{item.desc}

+
+
+ ))} +
+
+ + {/* Right Column: Terminal */} + + {/* Glow effect */} +
+ +
+ {/* Terminal Header */} +
+
+
+
+
+
+
+ + root@nofx-os:~ +
+
+ + {/* Terminal Content */} +
+
# Initialize NoFX Core Protocol
+
+ + + {installCmd} + + +
+ + {copied ? ( + + + + ) : ( +
+ +
+ )} +
+
+
+
+
+
+
+
+
+
+
+
+ ) +} diff --git a/web/src/pages/LandingPage.tsx b/web/src/pages/LandingPage.tsx index 6b6870b3..1934ad59 100644 --- a/web/src/pages/LandingPage.tsx +++ b/web/src/pages/LandingPage.tsx @@ -5,6 +5,7 @@ import FooterSection from '../components/landing/FooterSection' import TerminalHero from '../components/landing/core/TerminalHero' import LiveFeed from '../components/landing/core/LiveFeed' import AgentGrid from '../components/landing/core/AgentGrid' +import DeploymentHub from '../components/landing/core/DeploymentHub' import { useAuth } from '../contexts/AuthContext' import { useLanguage } from '../contexts/LanguageContext' @@ -42,6 +43,8 @@ export function LandingPage() { + + {showLoginModal && (