hub: unified customer page, blocked status, dashboard merge
- Replace separate config detail and report detail pages with unified
/customers/{id} page showing both config info and live report data
- Add "blocked" status for customers (hidden from dashboard, notifications
suppressed, still accepts reports)
- Dashboard now shows config-only customers as "PENDING" status
- Customers list: all rows link to /customers/{id}, show BLOCKED badge
- New actions: block/unblock, push config to controller, auto-create
config from report data
- /configs/{id} now redirects to /customers/{id}
- Add config-badge CSS classes for MANAGED/MANUAL/BLOCKED badges
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -118,6 +118,9 @@ func (s *Store) migrate() error {
|
||||
// v0.1.8: add controller_url column (idempotent — ignore error if already exists)
|
||||
s.db.Exec("ALTER TABLE reports ADD COLUMN controller_url TEXT")
|
||||
|
||||
// v0.2.1: add status column to customer_configs (idempotent)
|
||||
s.db.Exec("ALTER TABLE customer_configs ADD COLUMN status TEXT NOT NULL DEFAULT 'active'")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -520,6 +523,7 @@ type CustomerConfig struct {
|
||||
RetrievalPassword string
|
||||
APIKey string
|
||||
ConfigJSON string // JSON object with customer-specific override fields
|
||||
Status string // "active" or "blocked"
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -550,11 +554,11 @@ func (s *Store) GetCustomerConfig(customerID string) (*CustomerConfig, error) {
|
||||
var createdAt, updatedAt string
|
||||
err := s.db.QueryRow(`
|
||||
SELECT customer_id, customer_name, domain, email,
|
||||
retrieval_password, api_key, config_json, created_at, updated_at
|
||||
retrieval_password, api_key, config_json, status, created_at, updated_at
|
||||
FROM customer_configs WHERE customer_id = ?`,
|
||||
customerID,
|
||||
).Scan(&cfg.CustomerID, &cfg.CustomerName, &cfg.Domain, &cfg.Email,
|
||||
&cfg.RetrievalPassword, &cfg.APIKey, &cfg.ConfigJSON,
|
||||
&cfg.RetrievalPassword, &cfg.APIKey, &cfg.ConfigJSON, &cfg.Status,
|
||||
&createdAt, &updatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
@@ -571,7 +575,7 @@ func (s *Store) GetCustomerConfig(customerID string) (*CustomerConfig, error) {
|
||||
func (s *Store) ListCustomerConfigs() ([]CustomerConfig, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT customer_id, customer_name, domain, email,
|
||||
retrieval_password, api_key, config_json, created_at, updated_at
|
||||
retrieval_password, api_key, config_json, status, created_at, updated_at
|
||||
FROM customer_configs ORDER BY customer_id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -583,7 +587,7 @@ func (s *Store) ListCustomerConfigs() ([]CustomerConfig, error) {
|
||||
var cfg CustomerConfig
|
||||
var createdAt, updatedAt string
|
||||
if err := rows.Scan(&cfg.CustomerID, &cfg.CustomerName, &cfg.Domain, &cfg.Email,
|
||||
&cfg.RetrievalPassword, &cfg.APIKey, &cfg.ConfigJSON,
|
||||
&cfg.RetrievalPassword, &cfg.APIKey, &cfg.ConfigJSON, &cfg.Status,
|
||||
&createdAt, &updatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -607,11 +611,11 @@ func (s *Store) GetCustomerConfigByAPIKey(apiKey string) (*CustomerConfig, error
|
||||
var createdAt, updatedAt string
|
||||
err := s.db.QueryRow(`
|
||||
SELECT customer_id, customer_name, domain, email,
|
||||
retrieval_password, api_key, config_json, created_at, updated_at
|
||||
retrieval_password, api_key, config_json, status, created_at, updated_at
|
||||
FROM customer_configs WHERE api_key = ?`,
|
||||
apiKey,
|
||||
).Scan(&cfg.CustomerID, &cfg.CustomerName, &cfg.Domain, &cfg.Email,
|
||||
&cfg.RetrievalPassword, &cfg.APIKey, &cfg.ConfigJSON,
|
||||
&cfg.RetrievalPassword, &cfg.APIKey, &cfg.ConfigJSON, &cfg.Status,
|
||||
&createdAt, &updatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
@@ -624,6 +628,26 @@ func (s *Store) GetCustomerConfigByAPIKey(apiKey string) (*CustomerConfig, error
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// SetCustomerConfigStatus sets the status (active/blocked) for a customer config.
|
||||
func (s *Store) SetCustomerConfigStatus(customerID, status string) error {
|
||||
_, err := s.db.Exec(`
|
||||
UPDATE customer_configs SET status = ?, updated_at = datetime('now')
|
||||
WHERE customer_id = ?`,
|
||||
status, customerID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsCustomerBlocked returns true if the customer config has status "blocked".
|
||||
func (s *Store) IsCustomerBlocked(customerID string) bool {
|
||||
var status string
|
||||
err := s.db.QueryRow(
|
||||
"SELECT status FROM customer_configs WHERE customer_id = ?",
|
||||
customerID,
|
||||
).Scan(&status)
|
||||
return err == nil && status == "blocked"
|
||||
}
|
||||
|
||||
// UpdateRetrievalPassword updates the retrieval password for a customer config.
|
||||
func (s *Store) UpdateRetrievalPassword(customerID, newPassword string) error {
|
||||
_, err := s.db.Exec(`
|
||||
|
||||
Reference in New Issue
Block a user