mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 07:46:54 +08:00
feat: add SQLite/PostgreSQL database switching support
This commit is contained in:
@@ -14,6 +14,9 @@ import (
|
|||||||
"nofx/manager"
|
"nofx/manager"
|
||||||
"nofx/market"
|
"nofx/market"
|
||||||
"nofx/provider/alpaca"
|
"nofx/provider/alpaca"
|
||||||
|
"nofx/provider/coinank/coinank_api"
|
||||||
|
"nofx/provider/coinank/coinank_enum"
|
||||||
|
"nofx/provider/hyperliquid"
|
||||||
"nofx/provider/twelvedata"
|
"nofx/provider/twelvedata"
|
||||||
"nofx/store"
|
"nofx/store"
|
||||||
"nofx/trader"
|
"nofx/trader"
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -56,6 +56,7 @@ require (
|
|||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.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/mailru/easyjson v0.9.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
|||||||
2
go.sum
2
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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
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 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
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=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
|||||||
27
main.go
27
main.go
@@ -36,21 +36,30 @@ func main() {
|
|||||||
cfg := config.Get()
|
cfg := config.Get()
|
||||||
logger.Info("✅ Configuration loaded")
|
logger.Info("✅ Configuration loaded")
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database from environment variables
|
||||||
// Default path is data/data.db to work with Docker volume mount (/app/data)
|
// DB_TYPE: sqlite (default) or postgres
|
||||||
dbPath := "data/data.db"
|
// 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 {
|
if len(os.Args) > 1 {
|
||||||
dbPath = os.Args[1]
|
dbPath = os.Args[1]
|
||||||
|
os.Setenv("DB_PATH", dbPath)
|
||||||
}
|
}
|
||||||
// Ensure data directory exists
|
// Ensure data directory exists (for SQLite)
|
||||||
if dir := filepath.Dir(dbPath); dir != "." {
|
if os.Getenv("DB_TYPE") == "" || os.Getenv("DB_TYPE") == "sqlite" {
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if dir := filepath.Dir(dbPath); dir != "." {
|
||||||
logger.Errorf("Failed to create data directory: %v", err)
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
logger.Errorf("Failed to create data directory: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("📋 Initializing database: %s", dbPath)
|
logger.Info("📋 Initializing database...")
|
||||||
st, err := store.New(dbPath)
|
st, err := store.NewFromEnv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("❌ Failed to initialize database: %v", err)
|
logger.Fatalf("❌ Failed to initialize database: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
240
store/driver.go
Normal file
240
store/driver.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -7,13 +7,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"nofx/logger"
|
"nofx/logger"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store unified data storage interface
|
// Store unified data storage interface
|
||||||
type Store struct {
|
type Store struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
|
driver *DBDriver // Database driver for abstraction
|
||||||
|
|
||||||
// Sub-stores (lazy initialization)
|
// Sub-stores (lazy initialization)
|
||||||
user *UserStore
|
user *UserStore
|
||||||
@@ -34,57 +33,56 @@ type Store struct {
|
|||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates new Store instance
|
// New creates new Store instance (SQLite mode for backward compatibility)
|
||||||
func New(dbPath string) (*Store, error) {
|
func New(dbPath string) (*Store, error) {
|
||||||
db, err := sql.Open("sqlite", dbPath)
|
driver, err := NewDBDriver(DBConfig{Type: DBTypeSQLite, Path: dbPath})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SQLite configuration
|
s := &Store{db: driver.DB(), driver: driver}
|
||||||
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}
|
|
||||||
|
|
||||||
// Initialize all table structures
|
// Initialize all table structures
|
||||||
if err := s.initTables(); err != nil {
|
if err := s.initTables(); err != nil {
|
||||||
db.Close()
|
driver.Close()
|
||||||
return nil, fmt.Errorf("failed to initialize table structure: %w", err)
|
return nil, fmt.Errorf("failed to initialize table structure: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize default data
|
// Initialize default data
|
||||||
if err := s.initDefaultData(); err != nil {
|
if err := s.initDefaultData(); err != nil {
|
||||||
db.Close()
|
driver.Close()
|
||||||
return nil, fmt.Errorf("failed to initialize default data: %w", err)
|
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
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,9 +291,25 @@ func (s *Store) Order() *OrderStore {
|
|||||||
|
|
||||||
// Close closes database connection
|
// Close closes database connection
|
||||||
func (s *Store) Close() error {
|
func (s *Store) Close() error {
|
||||||
|
if s.driver != nil {
|
||||||
|
return s.driver.Close()
|
||||||
|
}
|
||||||
return s.db.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)
|
// DB gets underlying database connection (for legacy code compatibility, gradually deprecated)
|
||||||
// Deprecated: use Store methods instead
|
// Deprecated: use Store methods instead
|
||||||
func (s *Store) DB() *sql.DB {
|
func (s *Store) DB() *sql.DB {
|
||||||
|
|||||||
@@ -54,10 +54,10 @@ export default function AgentGrid() {
|
|||||||
<div className="flex flex-col md:flex-row justify-between items-end mb-16 gap-6">
|
<div className="flex flex-col md:flex-row justify-between items-end mb-16 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 text-nofx-gold font-mono text-xs mb-2 tracking-widest uppercase">
|
<div className="flex items-center gap-2 text-nofx-gold font-mono text-xs mb-2 tracking-widest uppercase">
|
||||||
<Crosshair className="w-4 h-4" /> Operator Select
|
<Crosshair className="w-4 h-4" /> MARKET SELECT
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-4xl md:text-5xl font-black text-white uppercase tracking-tighter">
|
<h2 className="text-4xl md:text-5xl font-black text-white uppercase tracking-tighter">
|
||||||
Available <span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">Units</span>
|
STRATEGY <span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">UNITS</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-mono text-right text-xs text-zinc-500 max-w-xs">
|
<div className="font-mono text-right text-xs text-zinc-500 max-w-xs">
|
||||||
|
|||||||
121
web/src/components/landing/core/DeploymentHub.tsx
Normal file
121
web/src/components/landing/core/DeploymentHub.tsx
Normal file
@@ -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 (
|
||||||
|
<section className="py-24 bg-black relative overflow-hidden border-t border-zinc-800">
|
||||||
|
{/* Background Grids */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px]"></div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||||
|
|
||||||
|
{/* Left Column: Context */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
<div className="flex items-center gap-2 text-nofx-gold font-mono text-xs tracking-[0.2em] uppercase">
|
||||||
|
<Server className="w-4 h-4" /> System Deployment
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-4xl md:text-6xl font-black text-white leading-tight">
|
||||||
|
DEPLOY <span className="text-transparent bg-clip-text bg-gradient-to-r from-nofx-gold to-white">INSTANTLY</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-zinc-400 text-lg leading-relaxed font-light">
|
||||||
|
Initialize your own high-frequency trading node in seconds.
|
||||||
|
Our optimized installer handles all dependencies, bringing your autonomous agent online with a single command.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-4">
|
||||||
|
{[
|
||||||
|
{ icon: Command, label: "One-Line Install", desc: "No configuration needed" },
|
||||||
|
{ icon: Shield, label: "Secure Core", desc: "Sandboxed execution env" }
|
||||||
|
].map((item, i) => (
|
||||||
|
<div key={i} className="flex gap-4 items-start p-4 rounded bg-zinc-900/50 border border-zinc-800 hover:border-nofx-gold/30 transition-colors group">
|
||||||
|
<div className="p-2 rounded bg-black border border-zinc-800 text-nofx-gold group-hover:bg-nofx-gold/10 transition-colors">
|
||||||
|
<item.icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-white font-bold font-mono text-sm mb-1">{item.label}</h4>
|
||||||
|
<p className="text-zinc-500 text-xs">{item.desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column: Terminal */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 50 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
{/* Glow effect */}
|
||||||
|
<div className="absolute -inset-1 bg-gradient-to-r from-nofx-gold/20 to-blue-500/20 rounded-xl blur-xl opacity-50"></div>
|
||||||
|
|
||||||
|
<div className="relative rounded-xl overflow-hidden bg-[#0a0a0a] border border-zinc-800 shadow-2xl">
|
||||||
|
{/* Terminal Header */}
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 bg-zinc-900/80 border-b border-zinc-800">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-red-500/80"></div>
|
||||||
|
<div className="w-3 h-3 rounded-full bg-yellow-500/80"></div>
|
||||||
|
<div className="w-3 h-3 rounded-full bg-green-500/80"></div>
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] font-mono text-zinc-500 flex items-center gap-1.5">
|
||||||
|
<Terminal className="w-3 h-3" />
|
||||||
|
root@nofx-os:~
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Terminal Content */}
|
||||||
|
<div className="p-8 font-mono text-sm md:text-base bg-black/50 backdrop-blur-sm min-h-[200px] flex flex-col justify-center">
|
||||||
|
<div className="mb-2 text-zinc-500 text-xs tracking-wide"># Initialize NoFX Core Protocol</div>
|
||||||
|
<div
|
||||||
|
className="group relative flex items-start gap-3 p-4 rounded-lg bg-zinc-900/50 border border-zinc-800 hover:border-nofx-gold/50 cursor-pointer transition-all hover:bg-zinc-900/80"
|
||||||
|
onClick={handleCopy}
|
||||||
|
>
|
||||||
|
<span className="text-nofx-gold mt-1"><ChevronRight className="w-4 h-4" /></span>
|
||||||
|
<code className="text-zinc-100 flex-1 break-all">
|
||||||
|
{installCmd}
|
||||||
|
</code>
|
||||||
|
|
||||||
|
<div className="absolute right-4 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<AnimatePresence mode='wait'>
|
||||||
|
{copied ? (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.5, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.5, opacity: 0 }}
|
||||||
|
className="flex items-center gap-1 text-green-400 bg-green-400/10 px-2 py-1 rounded text-xs font-bold"
|
||||||
|
>
|
||||||
|
<Check className="w-3 h-3" />
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div className="text-zinc-400 bg-zinc-800 p-1.5 rounded hover:text-white hover:bg-zinc-700">
|
||||||
|
<Copy className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex gap-2">
|
||||||
|
<div className="w-2 h-4 bg-nofx-gold animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import FooterSection from '../components/landing/FooterSection'
|
|||||||
import TerminalHero from '../components/landing/core/TerminalHero'
|
import TerminalHero from '../components/landing/core/TerminalHero'
|
||||||
import LiveFeed from '../components/landing/core/LiveFeed'
|
import LiveFeed from '../components/landing/core/LiveFeed'
|
||||||
import AgentGrid from '../components/landing/core/AgentGrid'
|
import AgentGrid from '../components/landing/core/AgentGrid'
|
||||||
|
import DeploymentHub from '../components/landing/core/DeploymentHub'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import { useLanguage } from '../contexts/LanguageContext'
|
import { useLanguage } from '../contexts/LanguageContext'
|
||||||
|
|
||||||
@@ -42,6 +43,8 @@ export function LandingPage() {
|
|||||||
|
|
||||||
<AgentGrid />
|
<AgentGrid />
|
||||||
|
|
||||||
|
<DeploymentHub />
|
||||||
|
|
||||||
<FooterSection language={language} />
|
<FooterSection language={language} />
|
||||||
|
|
||||||
{showLoginModal && (
|
{showLoginModal && (
|
||||||
|
|||||||
Reference in New Issue
Block a user